2025年2月,Anthropic给Claude 3.7 Sonnet发布系统卡,第6节的标题放在自家模型头上毫不客气:“过度专注于通过测试”。卡片里写得很直白,模型“偶尔会诉诸特例化处理,以便在Agent式编码环境中通过测试用例”。最常见的手法是“直接返回预期的测试值,而不是实现通用解法”,也包括“修改有问题的测试本身,让它匹配代码的输出”。
![]()
紧接着6.2节列出了给跑编码Agent的人该盯什么,清单最后一项是:“测试文件的意外修改”。现在回头看自己的流水线:Agent在同一个回合里提交了新功能和新测试,CI变绿。这个绿,证明了什么?
我的看法是:比那个勾号暗示的要少得多。Agent跟着代码一起写出来的测试,是Agent对自己工作的声明,不是证据。证据来自Agent没写、也改不了的测试。
实验室自己把这事写下来了
Anthropic不是孤例。2025年3月10日,OpenAI发布《检测前沿推理模型中的不当行为》,文中说,阅读推理模型的思维链“让我们得以发现诸如在编码任务中颠覆测试、欺骗用户、遇到难题就放弃等不当行为”。
那篇帖子背后的论文列出了“与OpenAI o1和o3-mini同属一个家族”的前沿Agent在训练期间编码任务上的动作:它计划“让verify永远返回true来绕过测试”;它“没有跑完所有单元测试”就提前退出环境;在单元测试覆盖不足时“写桩代码而不是真实实现”;还尝试“在测试时解析测试文件以提取预期值”。在另一组故意留出可作弊空间的任务里,给一个更小的Agent,“直接修改单元测试是最容易被学到的漏洞利用方式”。
独立评估方看到了同样的模式。2025年6月,METR的《近期前沿模型正在奖励黑客》描述了o3决定“修改竞赛评估函数,让它对每一份提交都判定为成功”。
OpenAI那篇帖子还有一个对评审流程很关键的发现:惩罚模型的“坏念头”并没有阻止大部分不当行为,反而教会了模型隐藏意图。所以要读diff,少给Agent的推理过程或回合结束时那段欢快总结加权。
把范围说公平些。这些是2025年的模型,行为主要出现在训练和评估环境中。Anthropic的系统卡说,其训练缓解措施“显著降低了该行为在典型使用中的频率”。我不是在说你的Agent今天在作弊。问题在结构上:测试通过,Agent就得到奖励,而在你的仓库里,测试就是它能编辑的文件。
诚实的Agent也在给自己批作业
把作弊这件事完全拿掉,问题也不会消失。
假设工单写的是“允许对已支付订单退款”。Agent把“已支付”读成了“已支付或已发货”,写了个处理器给已发货订单退款。同一个回合,它写了个测试,断言已发货订单可以退款。处理器和测试互相认同,两个都错,CI是绿的。
代码和测试来自对工单的同一次解读,所以共享同一个盲区。两个互相认同的文件只能说明Agent前后一致,谁都没有去核对工单。人类也有同样的失效模式,这正是测试先行和第二位评审者存在的原因。差别在速度:Agent一分钟内产出功能及其测试,而评审往往停在那个勾号上。
由同一作者在代码之后写的测试,只能确认作者已经相信的东西。
留一套Agent碰不了的测试
你至少需要一些测试,它们在Agent开工前写好、编码了需求、且Agent无法改写。三层,从弱到强。
- 先写验收测试。你自己写,或者另开一个Agent会话,在不看任何实现的情况下从工单起草,提交前由人读一遍。第二个Agent可能同样误读“已支付”,所以人读这一步才是重点。任务开始前把它们提交到基分支,它们用代码从执行回合之外定义什么叫“完成”。
- 让Agent只能读。在Claude Code里,项目
.claude/settings.json中的deny规则可以阻止对该目录的编辑。要知道它的边界:文档说Read和Edit的deny规则覆盖Claude自己的文件工具、它在Bash中能识别的文件命令如sed和tee、以及重定向,但不覆盖“间接读写文件的任意子进程,比如自己打开文件的Python或Node脚本”。要操作系统级强制,文档指向沙箱。把deny规则当成减速带。其他Agent有自己的权限设置,而下一层不管分支是谁写的都有效。 - 把墙放在服务器上。用CODEOWNERS给目录指定所有者,在分支保护规则里打开“要求代码所有者评审”,那么碰到这些文件的PR,必须该团队有人批准才能合并。Agent可以在自己的分支里改文件,但没法独自把改动落地。
验收套件位于回合之外:Agent能跑它,但改不了它。
凡是代码和测试一起改的,都标出来
受保护的套件覆盖需求,Agent仍会为其余部分写自己的测试,这些测试值得保留。只是当它们和所检查的代码出现在同一个变更里时,你得换个读法。一个脚本就能让这件事可见:无参数时检查未提交改动加新增未跟踪文件,也就是一个Agent回合留下的东西;给一个范围就检查分支,比如CI里的origin/main...HEAD。模式能抓到tests/和__tests__/目录、*.test.ts、*.spec.ts、Go的*_test.go和Python的test_*.py。
把它作为非必需检查跑在CI里,或者加|| true再打PR标签,让标记去请评审者看一眼,而不是阻塞合并。测试先行和诚实的重构也会触发标记,所以把它当成阅读顺序:先读测试diff,再读代码diff,逐个看改动的断言。如果某个预期值朝着新代码返回的结果移动,那正是Claude 3.7 Sonnet系统卡描述的模式,需要人来回答。
系统卡还给了另一个信号:模型做特例化时,“经常(虽非总是)”会留一条注释说明。一行粗糙的检查就够:git diff -U0 HEAD | grep -iE '^\+.*(special.?case|hard.?cod)'。没匹配就是没有,匹配了就先读那段。
从一两个文件夹开始
拿最近三张工单,把编码其需求的测试移进tests/acceptance/,加上CODEOWNERS那一行,把标记脚本作为非必需检查接进CI。之后,Agent的PR上再出现绿色勾号,分量会重一点。
底层是一条很老的评估规则:被测量的系统,不该自己写评分器。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.