Technical Writing

Nine revisions of a 148px orb: what you can't state in full, AI can't get right

A stronger visual model, a deeper skill, the industry's finest prototype in hand — none of it changes one thing: narration is lossy, and the model can only reconstruct the part I actually said. Nine revisions didn't buy me a prettier orb. They bought me a spec that didn't exist on day one.

Only the Chinese original exists for this one. The text below is that original.

Vox 的唤醒球是一个 148px 的透明置顶窗口,一直挂在桌面上,负责表达六个状态,并且承载 shell.run 的确认卡 —— 这条命令要不要在你机器上跑。

两天,九版。渲染代码是编程 agent 写的,方向、否决与验收口径是我的。

九版之后最值得记下来的不是那几条设计规则,是一件更根上的事:被浪费掉的轮次里,绝大多数 不是它画错了,是我说漏了。它每一版都精确地实现了我说出口的那句话,而那句话本身不够。

换更强的模型不解决这件事

先把结论放前面,因为后面每一节都是它的证据。

我脑子里那颗球是一个高维的东西:轮廓、时间上的起伏、材质的成因、各个量之间怎么耦合、 以及每个元素在报告什么状态。我发出去的是几十个词。模型拿到的是这几十个词,它重建出来的 一定是同时满足这几十个词的无穷多个东西里的一个。

它挑中我那一个的概率,跟它有多强关系不大,跟我给了多少条约束关系很大。换一个更强的视觉 模型,是把那无穷多个里的某一个画得更精致 —— 像素变好,靶心没有靠近。技能同理:技能能提 高符合我这句话的那批东西的平均质量,不能替我把那句话说全。

那手上有一张行业顶级的原型图呢?会好,但不解决。原型图给的是结果,不是成因。能产生同一 帧画面的成因有无穷多个,而模型必须挑一个。这一条后面有一次很难看的实测,就在脊线含量那 一节:我把参考素材的 270 帧全部提取成参数、七项逐帧对照、其中一项数字精确对上,渲染出来 还是错的。

最麻烦的是第三层:我不知道我哪个词是含糊的。统一这个词我用了三轮才发现它在我嘴里同时指 两件事,而那三轮里它每次都在同一个误读上做正确的增量。说不全不是懒,是我看不见自己说漏 了哪一块 —— 在脑子里那一块从来不需要说。

所以可执行的部分只剩一个判据:一句要求能不能执行,看我能不能写出决定它通过与否的那句断 言。写不出来就等于没说。要有生命呼吸感 —— 写不出来;稳态呼吸摆动 ≥1.8× —— 写得出来。亮 片要统一,写不出来;六个元素角间距全部 60.00°,写得出来。剩下的部分只能靠否决往回收敛, 而否决必须被留下来。

前三代白改了:构成比材质先被读到

第一代是粉彩流体玻璃的宠物球,带眼睛、腮红、会眨眼。第二代换成驻波核,一条波。第三代换 成光学核心 —— 一对精密相机镜头,理由是这颗球要承载确认卡,可爱的人格与那份职责冲突。

三代换的是材质与颜色,轮廓一直没动:两个圆孔加下缘一条细节带。而 148px 下最先被读到的正 是构成。七格基线渲染出来全部读作面孔,浅色桌面上是明确的卡通脸 —— 两颗眼珠加一条嘴。

回头看这三代白改的原因很干净:我说的是换个气质,而气质在我脑子里是连着轮廓的,在那句话 里没有。它没有理解错,它把我给出的唯一那一维 —— 材质 —— 换到了极致。

中间有一次自以为解决了。下缘那条刻度带原本沿球体弧度走,弧线加上方两片镜头就是一张笑脸 嘴,于是把它改成水平直线,还写下一条硬要求:基线必须水平。换掉的只是笑脸,得到的是一张 木然的脸 —— 只要上方有一对孔,下方任何一条横向细节都会读作嘴。

第四代才动对地方:删掉第二片镜头,删掉整条下缘带,规则改成球内只有一个几何中心,任何元 素不得成对出现。两个等大等高的圆就是一张脸,这是空想性错视,不是风格问题,换材质换不掉。

同一代里又犯了一个同类错误:删掉面孔的同时,给球加了 12 条黄铜刻线当读数环。渲染出来读 作老怀表的表盘、音响的旋钮 —— 而刻度是测量与调节的符号,这颗球既不测量也不能调节。它给 出的是错误的交互暗示,这是可用性缺陷,不是审美偏好。病根记在决策表里:不能用某种配色, 不等于必须继续用上一代的材质。

我要生命呼吸感,是一句不能执行的话

这是说不全的最纯形态:我给出的是一个感觉,而感觉没有单位。

我说过两次同一句话,离生命呼吸感太远。第一次它调了参数,第二次它把硬边换成柔边。两次都 不对,因为当时够不够活只能靠肉眼吵 —— 而肉眼吵架的结局一定是调参数。

改法是把它换成两个可复算的数:跑满一个呼吸周期,逐帧读 canvas 像素,量亮度总和的摆动倍 数,和亮度加权等效半径的摆动幅度。

第一次测出来的答案很直接:呼吸幅度只有 ±2–7%,整体摆动 1.08× / ±1.5%。这是生命波动不够 的量化答案,不是感觉问题 —— 它只有一个活物该有的一半。现在的读数是 idle 2.60× / ±9.1%、 speaking 3.24× / ±10.6%。

这两个数真正的价值不在数字本身,在于生命感从此可回归测试:掉回 1.1× 量级就是退回一张贴 图,而那正是我两次判断都指出过的东西。

呼吸这件事上还有一次纠错值得单独记。第四代做过一轮大幅减法,删了十项:眨眼、点击飞散粒 子、常驻旋转、148px 下根本看不见却每帧在算的彩虹镀膜环……判据是项目宪法第四条:动画必须 携带信息,报告不了状态、数据或进度的删掉。其中常驻呼吸缩放那一项删错了。呼吸携带的信息 是它活着、它在待命,对一个语音助手来说 presence 本身就是要传达的状态。删掉之后球读作一张 贴图。

为了防止这条被反向滥用成什么动效都能留,裁决表里给它配了判据:呼吸必须长在 AI 本体上, 不能长在背景或装饰层上;幅度与快慢必须按态不同,且是可断言的纯函数 —— 待机 0.85 rad/s 像睡着的人、思考 2.30 像在忙、异常 3.10 像喘、闸门 0 完全不呼吸。一套参数通吃六态,它就 退回装饰了。

手里有顶级素材也不行:标量对上了不等于结构对上了

这一节回答开头那个问题 —— 手上有一份现成的、做得极好的参考动效,是不是就不用叙述了。

我把那段素材的 270 帧全部提取成参数,逐帧对照 —— 运动学量照抄,配色也按逐帧取样重配。 两边用同一套代码读回来,比七项:能量、等效半径、核亮度比、横向亮度梯度占比、加权 RGB 质 心、脊线含量、角向主谐波。

这已经是叙述能站到的相当靠前的位置了:七个带小数点的数,逐帧可比。

其中有一项叫脊线含量,素材 0.076,我的实现 0.022。为了补上,我让它沿花瓣迎光那条长边描 一道亮线 —— 脊线含量立刻到 0.095,数字对上了。

并排渲染出来是一圈白色线框,每片花瓣都被描了边,读作矢量花瓣。素材的脊线来自弯曲面自己 的明暗渐变,不是轮廓线。

原型图和参考素材给的是结果,不是成因。0.095 这个数有无穷多种画法,描边是其中最省的一种, 模型没有理由不选它。我当时也没有说过这不能是一条线 —— 因为在我脑子里它从来就不是一条线, 不需要说。不需要说的那一部分,正是叙述里丢掉的那一部分。

亮线删掉,脊线含量退回 0.026,作为已知落差留着。新规则写进文档并挂在改动触发条件那一节: 新增任何贴合度标量,必须同时给并排渲染。数是叙述,并排渲染是把成因也塞进通道里的唯一便 宜办法。

同类的第二例更反直觉:角向主谐波的命中率从 46% 掉到 26%,而视觉明显变好。原因是片体排布 从等角改成不规则之后,这个 argmax 本身就不稳定了 —— 而等角那一版并排渲染读作风车。命中率 下降与视觉变好同时发生,说明它已经不是一个有效判据,不是保真度退步。

看起来像取舍的东西,先问它是不是键错了量

这条出现了三次,是九版里最贵的一条。三次都不是审美问题,是它把我的话正确执行到了一个错误 的量上。

第一次。上一代的文档里写着一句读起来很专业的话:静态贴合与动态起伏二选一,体积光和玻璃盘 把等效半径钉死了。根因不是取舍 —— 那两项画的是固定大小的盘。改成可见半径随聚合度长之后, 两边同时变好:等效半径的逐帧平均绝对差 0.085 → 0.038,listening 的半径摆动 ±4.0% → ±8.1%。

第二次。高聚合时整颗球烧成一团白,于是给单片透明度加了一道随聚合度反向的补偿。结果能量摆 动从 4.5× 崩到 1.33×,球又退回一张贴图。补偿咬的是这一帧有多聚合 —— 而呼吸让聚合度涨的时 候,透明度正好同步跌,一涨一跌相消。改成咬这一态有多聚合,也就是不含呼吸的目标值,静态过 曝照旧被压住,呼吸完全不受影响。

第三次。listening 的聚合度基准 0.902 加呼吸幅度 0.165 等于 1.067,上界被 clamp01 削平, 一轮里约 23% 的时间卡在 1.0 不动。把基准压到 1−amp 之后,逐帧对照的能量形状从 0.211 直 接回到 0.151。

三次的共同点是:把缺陷写成取舍,会让它永远不被修。上一代的文档正是这么写的,而它读起来完 全像一份成熟的工程判断。这件事比听起来更严重 —— 下一个 agent 读到的是我的文档,不是我的脑 子。文档里那句话写歪了,它就会非常靠得住地把歪的东西实现好。

顺着这条还查出一件更难看的事:动效包络那道门本身是靠错误度量才通过的。采样例程在切态之后 立刻开始读数,把 400ms 的状态过渡算进了呼吸摆动,所以记下来的 4.23× 虚高一倍以上。加 140 帧预热之后稳态只有 1.80×。判据必须跟着改,不能留一条靠错误度量才通过的门。现在它分两层: 每个呼吸态稳态 ≥1.8×,保真度看逐帧对照的能量形状,不看单态摆动。

同一处被否三次,就该回去查自己的词

思考态的形态我否了三次。这三轮是整篇文章的核心证据,因为它三次都没有做错。

我第一句话是这么说的 —— 为什么思考界面它那些亮片不能化为统一的形态在球内进行旋转。

它把六片收拢到同一朝向、叠成一叠,不对。它加上收拢的过程,把一帧之内的跳变改成 700ms 的插 值,过程对了,终态还是不对。它改成各片缩小、合成一个亮团沿轨道公转,仍然不对。

第三轮我把话说破 —— 通过形态转化变成独立互不重叠的独立元素进行环绕。

统一这个词是 uniform,各片长得一样;不是 merged,并成一个。我用了三轮才发现问题在我这个词 上,而这三轮里它每次都在同一个误读上做正确的增量:收拢得更平滑、合得更漂亮,每一版都比上 一版更好地实现了我说出口的那句话。换个更强的模型,只会让它合得更好看。

更难受的是答案一直摆在并排渲染的第二格里:素材的散开相就是一圈分离的亮片,等效半径 0.680、 全轮最大值、中心全暗 —— 而我们三轮都在往球心收。素材就摆在那儿,我自己也没读出来。

教训写成一条规则挂进决策表:同一处被否三次,就不是实现不到位,是需求读错了。第三轮该做的 不是再调一次参数,是回去问统一这个词到底指什么。

方向改对之后还剩一个纯粹的量的问题,同样值得记。我把保留多少片间差异写成 1 − 0.94·ring, 稳态还剩 6%;慢摆写成 ×(1 − 0.92·ring),还剩 8%。这两个残量分别扰动各片的大小、朝向、形 状与呼吸相位,所以六片的角距本来就不可能相等,最近的一对必然先贴上。两处改成精确归零之后 实测:六个元素的中心半径全部 0.560、角间距全部 60.00°。一条可断言的几何性质,不能留一个看 起来无害的残量 —— 那点残量会把它变成不可断言的。

叙述有损,而且不持久

九版里有三版是同一天换的,而那一天我一条决策记录都没留。第二天的后果很具体:仓库里两处文 档还在描述两代前的驻波核,连六态弧长 115.61 这种数都还写着,而它引用的那个采样函数在当时的 渲染器里早已不存在。

于是把审美从对话里搬进文件:宪法(原则与冲突时的优先级链)、视觉词汇(每个产品的物理参照物 与硬约束)、状态语言(六态的契约边界)、反模式裁决表、决策记录、状态读数。规则放在工作区根 目录,由一份刻意保持很短的路由文件按改动等级决定读哪几份 —— 全文导入等于每次对话都付一遍 这份文档的钱,与按需加载正好相反。

决策记录的格式是固定的五栏:决定 / 理由 / 备选与被否原因 / 预期效果 / 来源。

第三栏是它真正干活的地方,也是整套东西里唯一在正面对抗叙述有损的部分。既然收敛只能靠否决, 否决就必须能被攒起来。没有它,下一个 agent 会重新提一遍已经被否过的方案,而且理由听起来同 样有道理 —— 加个渐变光晕试试,在没有记录的世界里永远是一个新想法。

第五栏管另一件事:来源必须标明是用户明说、代码注释推断、本次实测,还是文档记载,而且推断不 得写成明说。这一栏挡掉的是把自己上一轮的猜测当成我的要求,然后在它上面继续盖三层 —— 那正 是有损叙述放着不管就会自己长出来的东西。

顺带说一句我现在还不能声称的部分:上面所有读数的证据等级都是 SIM —— 对照页渲染、逐帧像素 度量、检测器零命中。这颗球还没有在真机的透明置顶窗口上验过,而柔边依赖的 ctx.filter = blur() 在老 WebView2 上不支持时是被静默忽略的。那一项排在待验清单第一位。

九版不是它的失败次数

还有一头我一开始也没想明白:我脑子里那个东西,在第一天并不存在。

它不是一张完成的图,是一组约束加一团感觉。独立互不重叠的元素环绕这句话,是在看到它合成一 个亮团之后我才有的;呼吸必须长在本体上这条,是在我自己删错一次之后才有的;球内不得有成对 元素这条,得先有七格基线渲染出一张卡通脸。我是靠看自己否掉了什么,才知道自己要什么。

所以按我脑子里的样子生成这个要求,两头都不成立:通道有损,源头也没定。九版不是它画不对的 次数,是那份规格被写出来的过程。规格现在存在了 —— 存在于裁决表、那两个呼吸读数和 60.00° 里,不在我脑子里。

收成一句

技能、视觉模型、原型图,优化的都是通道的另一端 —— 而丢失发生在我这一端,再强的重建也补不 回我根本没发出去的那部分。我这一端只有三件事能做:把好看换成两个能回答的问题,这个元素在 报告什么、这个数是多少;给每个新增的数配一张并排渲染;把否决攒下来。

最后一条最短也最省钱:同一处被否三次的时候别再调参数了,回去查自己的词。