假成功有四种形状:给 AI 写自动化工具的自检清单

写完《计数器停在 0》那篇,我停下来把最近三篇捋了一遍。

《我给自己写的工具,连着骗了我七次》讲的是我给自己的脚本造了一个「已验证」标记。《三套 API 都告诉我成功了》讲的是从操作系统一路追到浏览器内部,才找到一个没发出去的字符。《计数器停在 0》讲的是 DOM 读回逐字全对,而框架压根不知情。

三篇完全不同的故障,三条完全不同的排查路径。

我重新读了一遍,发现它们在说同一件事,只是没有人给它起个名字。

这篇我把它整理成清单。资源型,你可以直接抄走用。

先说结论

所有「返回成功但实际没做」的故障,根因都是同一个:你在自己这一端验收,而真相在下一端。

调用返回了成功,这个返回值是你这一端生成的。它描述的是「我做了我要做的动作」,不是「目标变成了我想要的样子」。

这两句话在绝大多数情况下重合,重合到你会觉得它们是同一句话。

所以你顺手用「我调用了吗」当验收判据,然后一直没事。

直到有一天它不重合。

四种形状

从外到内,一条因果链。上一层不成立,下一层的验证全是空的。

形状一:没到

表现:调用返回成功,目标那边一次都没发生。

这是最干净的一种,因为它至少还诚实地报错了 ------ 不,它连这个都不报。它返回的就是成功。

《三套 API》那篇追的就是这个。Input.insertText 返回正常,Chrome 里也确实有事件发出,页面上就是没有那个字。我从操作系统一路量到浏览器进程内部,才在最后一层发现那个字符压根没进编辑器的输入通道。

探针:在终点挂计数器,不在起点问返回值。

dart 复制代码
// 错:问起点
const ok = await sendEvent();
assert(ok === true);   // 这里的 true 只说明我调用了

// 对:在终点数落地次数
let landed = 0;
document.addEventListener('beforeinput', () => landed++, true);
// ... 跑操作 ...
assert(landed > 0);    // 这里的 landed 才是真相

判据从「我做了」挪到「目标那边收到了几次」。听起来是废话,但这两种信号在形状一里给出的答案正好相反。

形状二:到了,但没变

表现:事件被受理,处理器跑完了,产出为零。

这一层比形状一难查,因为每一个返回值都在说成功。

我灌 HTML 到腾讯云编辑器的时候遇到过:

json 复制代码
{"defaultPrevented": true, "grew": false}

defaultPrevented: true 意思是事件没有被拦截,是被正常处理了。处理器跑了。然后 grew: false ------ 内容一个字都没长。

如果我不看 grew 这个字段,只看「事件成功派发、没被阻止」,我会判定成功。

探针:断言产出量,不断言事件状态。

scss 复制代码
const before = measure();
dispatchPaste(html);
const after = measure();
assert(after !== before);   // 关键字段

grew 是我自己加的字段。当时我只是想留个日志,后来它救了我一次。给关键操作加一个「产出量」字段,比加十个「状态 OK」字段有用。

形状三:变了,但没进模型

表现:DOM 投影对了,框架内存里的值是空的。

《计数器停在 0》整篇讲的就是这一层。

我灌了 112 个字,textarea.value 逐字比对全对,一个不差。平台计数器:0/120。

不矛盾。value 是 DOM 属性,是我自己写进去的。框架有自己的模型,写 DOM 不会自动改它。这两个值同时成立,而且它们本来就该同时成立。

同一层还有一个更阴的变体:两次写入叠加。

同一篇文章里我往作者框填名字,先用一种方式,发现计数器是 0,判断没生效,换另一种方式又来了一遍。结果框里是:

复制代码
ikalus1988ikalus1988

因为第一次其实也写进去了。在这一层,「没生效」和「生效了但没进模型」是同一件事。

探针:用框架自己画的反馈,不用 DOM 读回。

字数计数器、脏标记(那个小圆点)、预览渲染 ------ 这些是框架主动画的实时反馈。它们不动,等于框架没收到。

比起逐字比对 value,计数器的信息量小得多。但它问的是正确的问题。

形状四:进了模型,没落库

表现:内存里是对的,服务端是空的。

公众号那次最典型。我点「发表」,页面直接跳走了,地址栏变成了一个我没见过的 URL,弹窗说「已保存」。

三个信号,全是成功语气。

发表记录纹丝不动。

探针:重新拉取服务端状态,不看任何本地反馈。

不是弹窗,不是 toast,不是页面跳转,不是 URL 里多出来的 id。是下一次从服务端拉回来的那份数据。

四层是一条链

这四种形状不是并列的分类,是一条因果链:

复制代码
没到  →  到了没变  →  变了没进模型  →  进了模型没落库
动作    事件         DOM              服务端

你在第二层验收,第二层的故障对你完全隐形,而第三、四层的故障全部长得像成功。

更麻烦的是:第一层故障会让后面三层的信号全变成 0,而 0 看起来像「没数据」,不像「上游没通」。

所以排查顺序必须从外往里。你不能先在服务端验收,然后倒推中间哪一环断了 ------ 因为服务端那个 0 有四种完全不同的成因。

发布前自检清单

这份清单我给每个要提交的字段都过一遍。不是每个字段都有探针,没探针的字段我宁可不要。

一、判据的方向

  • 我的判据来自比动作更远的一端吗?
  • 我用的是返回值,还是目标状态?
  • 如果返回值是 true,但目标状态我没查过 ------ 这一项算没过。

二、探针和故障点错维

  • 我的探针测的这一维,和可能出故障的这一维,是同一维吗?
  • 如果是同一维,那它就是双重失败:坏了也测不出来。

这条是整个清单里最贵的一条。我在《计数器停在 0》里付过一次学费:DOM 读回在「内容对不对」上精确到字,在「框架知不知道」上是彻底瞎的。而故障点恰好整整齐齐躺在它盲的那一维上。

  • 这个探针是别人画的 吗(平台的计数器、服务端的列表),还是我画的(我写进去的 DOM、我打的日志、我造的状态字段)?
  • 我自己画的探针,会不会和我的错误是同一个来源?

三、每层至少一个探针

  • 终点副作用计数(形状一)
  • 产出量差值断言(形状二)
  • 框架原生回显(形状三)
  • 服务端重新拉取(形状四)

四层里我这次只有第三层的探针是对的。前两层是运气,第四层我压根没想到。

四、换方法之前

  • 换方法前先清空,并断言它真的空了。
  • 断言是读回读出来的,还是我推断出来的?

「再试一次」「换条路走」不能当补救用。**第一次没成功,不代表第一次什么都没做。**在形状三那一层,第一次做的是一半,你得先把这一半擦掉。

五、发布前的总闸

  • 每一个要提交的字段,我都能说出它的探针在哪吗?
  • 有任何字段我不能确定它是否生效 ------ 不发。

这条不是严谨,是经验。不确定不是「大概率没问题」,不确定就是不知道,而不知道的东西没有默认值。

一条反直觉的推论

写完这份清单我意识到一件事。

「加更多自检」本身不是解法。你能加的探针数量受限于你能观测到的维度,而故障点可以正好落在你某个观测不到的维度里。

所以真正能提高通过率的,不是「检查得更细」,是换一个维度去检查。

《三套 API》那篇我说过一句话:唯一真正把中文送进编辑器的路,恰恰是唯一不发按键信号的路 ------ 验证手段和正确解法,在结构上互斥。

这句话在《计数器停在 0》里递归了一遍:不是「输入方案」和「验证信号」互斥,是「我用来验收的那个方法」和「这次真正出问题的那一维」互斥。

两次的教训完全一样:当你的解法和你的判据共用同一个盲区时,你做得越干净,错得越自信。

绕开的办法只有一个:**判据必须由第三方提供,而且必须来自比动作更远的一端。**平台画的计数器、平台返回的列表、平台的状态标签。

自己画的判据,在最难的那一类故障上,是负资产。

收尾

这三篇文章我前后写了三个晚上,写的时候每次都以为自己弄明白了。

回头看,三次犯的是同一个错的三层。

我把它们整理成了上面这份清单。它不完整,四层只是我踩过的形状,你可能会碰到第五种、第六种。

但有一条我觉得是稳的:

只要你是在自己这一端验收,你就在赌重合。

重合了九十九次,输的那一次不会提前通知你。

相关推荐
johnsmithCA2 小时前
给搜索的法规条文做了个「版本核验」流程:怎么确认手上的条文还是现行有效版
人工智能·自动化运维
考虑考虑14 天前
nohup启动java程序
后端·自动化运维
东莞市云毅网络有限公司14 天前
PDF 表格抽取的三条技术路线对比:规则、库解析与版面识别
python·sqlite·自动化运维·geo·数据监测
东莞市云毅网络有限公司17 天前
用标准库做网页正文抽取:从 HTML 到结构化字段的轻量实现
python·sqlite·自动化运维·geo·数据监测
考虑考虑18 天前
docker compose V2版本新属性
运维·后端·自动化运维
东莞市云毅网络有限公司20 天前
用 SQLite FTS5 给企业知识库做问答检索原型:中文二元切分与 BM25 排序
python·sqlite·自动化运维·geo·数据监测
考虑考虑21 天前
cmd局部设置java变量
运维·后端·自动化运维
宋均浩22 天前
重跑率 31% → 6%,变更失败率 12% → 4%:CI/CD 流水线治理实战的 5 个配置
ci/cd·自动化运维·devops
考虑考虑23 天前
docker compose环境变量替换
运维·后端·自动化运维