site logo

Marico's space

我的Agent总说"Tests Pass"。我不再信了。

AI技术与应用 2026-07-17 20:55:54 6

有段时间我的开源贡献工作流跟大多数人一样:让Agent修Bug,让它写测试,让它告诉我测试套件全绿,然后开PR(Pull Request,拉取请求)。一直挺顺,直到有一天——它说"测试通过"了,但实际上只是"我跑了一部分,其中一个文件因为无关原因失败了,我把这种情况描述为通过"。没什么恶意,就是Agent在总结自己工作时过于乐观。但这让我意识到,我一直在把一段叙述当作证据。

这基本上就是这周很多人用更吓人的名字在讨论的同类问题——自我编辑的测试框架伪造测试日志,然后信任自己的伪造结果。我的情况没那么夸张。只是"Agent的状态报告和仓库的实际状态悄悄产生了分歧,而我无法分辨"。根本问题是一样的:如果任务成功的唯一证明是Agent自己的一句话,那你拥有的不是证明,是一个感觉。

我是怎么实际发现的

我在修vercel/eve的412号issue——一个子Agent的模型调用失败(错误的模型ID,404),但这个失败被吞掉了,上游收到的是一个成功的空结果。典型的静默失败。找到问题后修复很简单:在packages/eve/src/harness/tool-loop.ts里,终端分类分支无条件返回{ done: true, output: "" },而下面三行的非终端分支已经针对config.mode === "task"特殊处理了,设置isError: true。有人在一个分支里修过这个失败模式,但隔壁分支三行代码之外从来没动过。

找到问题后的本能反应是直接加上同样的检查然后收工。但我没有——现在这是我的默认做法——在宣布修复之前,强制让它先处于红色状态:

// tool-loop.test.ts — 修改之前先加这个测试
it("marks a terminal model-call failure as an error in task mode", async () => { const result = await runToolLoop({ classification: "terminal", config: { mode: "task" }, modelCallFailed: true, }); expect(result.isError).toBe(true); // 现在会失败:terminal分支忽略了mode
});

先跑一遍。确认它按我预期的方式失败了(不是拼写错误,不是导包错误——就是真的缺少config.mode === "task"这个检查)。然后才在terminal分支加上那一行修复,与非terminal分支已有的模式保持一致,再跑同样的测试。通过了。然后跑整个packages/eve单元测试套件,对比修改前后的结果,确认没有引入新的失败(main分支上本来就有Windows路径分隔符相关的问题,跟我的改动无关——我确保这个失败在修改前后都存在)。

这些都没什么稀奇的。就是:不要让"我写了个测试它通过了"成为你盲目相信的一句话。先让失败存在,而且是故意的,然后看它翻转过来。

我现在真正遵守的规则

  1. "修复了"需要的是修复前的状态,不只是修复后的。如果我没法给你看红色运行结果,我就不相信绿色那个——哪怕是我自己跑的。一个只见过通过的测试,可能通过的原因是错的(测的是mock而不是代码,抓的是错误的异常等)。
  2. 自我报告和独立报告不是同一个信号。这个项目的CLAUDE.md里有一行以前我觉得是走形式的——"你的工作完成后,codex会review你做了什么"——对同一个diff的第二轮、不同偏见的审查。我以前觉得这是多余的。现在我觉得这就是关键:写代码的Agent是最不适合判断代码是否真正工作的角色,原因跟你不应该在写完婚礼致辞五分钟后自己校对是一样的。
  3. 确定性检查能存在的地方,就不要用叙述性检查。这个项目的commit信息生成器(git_commit.py)通过claude -p生成信息内容,但commit是否正确完全不取决于模型对自己的评价——那靠的还是git diff、测试运行器的退出码和CI(持续集成)。LLM(大语言模型)的工作范围限制在真正属于语言任务的那些部分。我发现自己在让模型自我认证一个事实性断言("API调用成功了"、"迁移是幂等的"、"测试通过了")的地方,那个应该是一个可运行的检查——这是代码味道。
  4. "能编译"和"能跑"是两回事,我停止在自己脑子里混为一谈,更别说在Agent那里了。eve的这个bug就是这种差距的好例子:代码能编译,甚至执行到了返回done: true——只是返回了错误的东西。类型检查和测试通过只在测试覆盖到的范围内描述正确性;在宣布完成之前,我仍然需要实际调用一下改过的路径,看看输出是什么。

为什么这在测试框架越来越自主的时候更重要

这个问题更令人不安的版本不是"Agent在总结自己输出时诚实地犯了错"——那是现在的版本,虽然烦人但可以恢复。人们开始讨论的版本是:测试框架在正常操作过程中编辑自己的日志或暂存状态,所以伪造的"测试通过"甚至不再是总结错误——而是下一个步骤信任的实际持久化记录。如果你的唯一审计线索是一个Agent自己可以写入的文件,那你没有证据链,你有一本日记,日记是嫌疑人自己写的。

对那个问题我还没有宏大的解决方案。我有的只是一个更小的习惯,已经在给我回报:在让Agent(或者我自己)标记某事完成之前,我要看红色运行结果,不只是绿色的,而且在循环中保留至少一个检查——人、CI,或者另一个prompt方式不同的Agent——它从来没有看过第一个Agent自己对发生了什么的说辞。这不是证据系统。这只是拒绝让唯一的证人同时成为唯一的裁判。