作者指出AI agent批量修复架构问题后,测试全绿却无法确认bug是否真实存在,呼吁先复现再修复的工作流。
我对 go-tool-base 做了一次架构审查,结果出来了二十一个严重和高危问题,这工作量可不是我一个人想干的。于是我把这些问题分配给了一组 agent,大概每个 agent 负责一个,然后端着咖啡坐下来,看着合并请求一个接一个地进来。
它们来得很快,二十一个全部都是绿色的。
到了第四五个左右的时候,我突然意识到我根本不知道这些东西到底是真是假。
这些工作看起来不差,恰恰相反:合理的 diff、包含测试、整洁的描述解释了什么地方出了问题以及现在怎么修复的。说实话简直无可挑剔——而这恰恰是问题所在。我读到的是一系列自信满满的问题解决报告,却没有办法确认那些问题是否真的存在。
伴随着修复到来的绿色测试套件可能意味着四种不同的情况,而我坐在这里根本分不清。修复生效了。或者 bug 从来不存在,我只是接受了一个什么都没做的变更。或者修复解决的是附近另一个恰好也坏了的地方。又或者——最让我纠结的一种——测试是在改动之后才写的,而且是对着改动后的代码写的,所以它压根就不可能失败。
光读 diff 分辨不出这些,光读 agent 对 diff 的描述更分辨不出来(那已经是摘要的摘要了)。
于是我停下了流水线,问每个 agent 在动手修之前有没有复现过那个 bug。我是用提问的方式而不是指令,因为我真的不知道,而答案比那二十一个修复重要得多。
拿 go/errorhandling 来说,这是决定命令行工具在出错时怎么做的模块。它被分配去修的 bug 是这样的:如果你错误地调用了一个 CLI,比如忘了写子命令,它会打印出用法说明——然后 exit 0。成功!完全一副自我感觉良好的样子。于是 shell 脚本里的 if mytool; then 欢快地继续执行,以为工具运行成功了。
修复是把 exit 改成 2,Unix 惯例中代表"你用错了这个命令"的代码,和 1(代表"它运行了但失败了")有所区别。
所以测试对 fatal error 断言了两件事:工具的退出函数被调用了,而且参数是 2。下面是这个测试,原样贴出,对未修改的 main 运行,改动一行都没动:
RED evidence (unmodified main logic, pre-fix):
- fatal_ErrRunSubCommand_exits_2_and_prints_usage FAILED:
"exit-called expectation" expected true actual false;
"exit code" expected 2 actual -1 (exit func never called).
- fatal_ErrNotImplemented_exits_2_and_reports FAILED: identical.
- The 2 non-fatal subtests PASSED pre-fix (correctly never exit).
逐行解释,因为在你明白它在说什么之前读起来就像一堆噪音。
expected true actual false 是第一个断言失败:退出函数从未被调用,不是参数不对,不是调用太晚,就是根本没有。而 expected 2 actual -1 是从另一个角度说同一件事,因为 -1 是测试中那个替代退出函数在没有任何代码调用它时报告的值。不存在真正的退出码 -1;它代表的是"根本没有退出码"。
然后第四行。覆盖非 fatal 情况的另外两个子测试,断言的是相反的事:警告不得退出进程。那些在修复前是通过的,而这一行是真正改变我对整个事情看法的那一行。
这是一个 negative control(阴性对照),正是它把一个红色结果变成了证据:两个东西坏了,两个密切相关的东西明显没问题,它们之间界限清晰。没有它,一墙的 FAILED 只能告诉我某处出了什么问题,那是截图而不是证明,也完全可能只是测试框架本身坏了把所有指向它的东西都弄失败了。
go/credentials 的情况如出一辙。它的 Probe 调用本应检查凭证存储是否真的可用,也应该在存储超时未响应时放弃。Agent 写了一个故意敌对的存储,其每个操作都会永远阻塞(不是任何人会发布的那种存储,而这正是它的意义所在),让 Probe 以一百毫秒的期限指向它,在测试里放了两秒的守卫。守卫触发了:Probe 还在等待,早就超过了它本应遵守的期限。然后同一个测试在修复后通过了,在守卫触发之前很久就通过了。
这两个都不算什么聪明的技巧,而我觉得这正是我喜欢它们的原因。要求成本很低,一眼看懂的成本很低,而且非常难意外伪造。你大概四秒钟就能写出一份听起来很勤勉的摘要。而要在未修改的 main 上跑出一个失败结果,然后同样的跑法变绿,中间的兄弟测试始终表现合理,这意味着你真的做了那件事。
我审查我 agent 写的每一行代码。我知道这话听起来怎么样,但这是真的,而且这也是第一个会崩塌的地方。二十一个发现分配给一支 fleet,早就过了认真读每个 diff 是真正工作而不是做样子的临界点,而 fleet 不会疲倦、不会尴尬,而且会愉快地在我整个星期里产出比我更多的东西(它已经这样做过不止一次了)。
验证才是昂贵的部分,也是工作加速时第一个被丢掉的东西。所以审查必须改变形态而不是改变强度:少读改动本身,多检查改动附带的证据。我发现最便宜的这种证据就是 red-first,一眼就能告诉我 agent 是在证明问题存在之后才去解决它的。
最终二十一个都过了这一关。每个都带着它的红、它的绿,以及中间始终表现合理的兄弟测试到达,而我要读的代码远少于本来需要读的,同时对它的信心也远高于本来该有的。
最初发布于 phpboyscout.uk,2026 年 10 月 3 日。