验证闭环:让智能体自己证明做对了
智能体说「完成了」不等于完成了。本课讲怎样建立验证闭环:类型与 lint、测试先行、真实运行与浏览器截图、对照验收标准自检、独立复核,以及如何防止智能体靠改测试、写死答案来「骗过」检查。
学完这一课,你能
- 能为一个任务设计从快到慢的分层验证,并让智能体自己运行并读取结果
- 能在智能体工作流中应用测试先行,并说明它为什么比事后补测试可靠
- 能用 Stop Hook 或 /goal 把验证变成「不通过就不能结束」的闸门
- 能识别并防范智能体绕过测试的常见手法
智能体回报:「已完成,所有测试通过。」你合并了。第二天线上出问题,一查发现:有个测试在中途被它改过断言,另一个被标成了跳过,而真正的功能从没在浏览器里打开过。
这里的问题不在于智能体撒谎,而在于它停下来的依据是「看起来做完了」。Claude Code 官方最佳实践里有一句话我很认同:没有它能运行的检查,「看起来做完了」就是唯一的信号,而你就成了验证闭环本身,每个错误都要等你来发现。反过来,只要给它一个能产出通过或失败的检查,它就会自己做、自己读结果、自己改,直到通过。能否让智能体自己验证,决定了你是在盯着它干活,还是可以放手去做别的事。
原则:要证据,不要断言
检查可以是任何能把信号带回对话里的东西:测试、构建的退出码、类型检查、lint、对比输出与样例的脚本、浏览器截图。要点是两个:它能自己运行,以及结果是它能读懂的。
我把验证分成五层,越往下越慢、越贵、越接近真实:
| 层 | 检查什么 | 耗时 | 能抓住 | 抓不住 |
|---|---|---|---|---|
| 1 | 类型、lint、构建 | 秒级 | 拼写、类型不符、未使用变量 | 逻辑错误 |
| 2 | 单元与集成测试 | 秒到分钟 | 行为回归,边界情况 | 测试没覆盖的路径 |
| 3 | 真实运行 | 分钟 | 配置、集成、环境问题 | 视觉和交互细节 |
| 4 | 浏览器与截图 | 分钟 | 页面真能用、样式对不对 | 后端深处的错误 |
| 5 | 验收标准核对与独立复核 | 分钟 | 做漏的需求,做错的方向 | 需要领域判断的问题 |
规则是:便宜的检查每次都跑,贵的检查在关键节点跑。 让智能体每改一批代码就过第 1 层,每完成一个步骤就过第 2 层,整个任务结束前才过第 3 到第 5 层。
第 1 层:把机器能秒判的事交给机器
类型检查和 lint 的价值,在智能体时代被放大了:它们是最快、最确定的反馈。一个有严格类型的 TypeScript 项目,智能体改错一个字段名,几秒钟内就收到明确报错,不需要你介入。
让它们自动发生,别靠提示词提醒。官方文档里有一个现成的 Hook 例子:每次文件编辑后自动格式化。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write" }
]
}
]
}
}
更重要的是第二种用法:把验证做成闸门。Stop Hook 在智能体准备结束一轮时触发;脚本退出码为 2 时,stderr 里的内容会作为反馈返回给智能体,它必须继续干活。下面是一个最小可用的版本:
#!/bin/bash
# .claude/hooks/verify-before-stop.sh
INPUT=$(cat)
# 已经因为这个 Hook 继续过一次,放行,避免死循环
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0
fi
if ! OUTPUT=$(npm run verify 2>&1); then
echo "验证未通过,请修复后再结束:" >&2
echo "$OUTPUT" | tail -40 >&2
exit 2
fi
exit 0
在 .claude/settings.json 里注册 Stop 事件,命令指向这个脚本即可(Stop 事件不需要 matcher)。官方也提醒了两个细节:Claude Code 在连续阻止八次、中间没有工具调用时会强行放行,所以脚本要检查 stop_hook_active;输出只保留尾部,避免把几千行日志灌进上下文。
如果不想写脚本,/goal 是会话级的轻量版:/goal npm run verify 退出码为 0,且 git status 里没有改动 tests 目录,或者 20 轮后停止。另一个模型会在每轮结束后判断条件是否成立。要注意它不会自己运行命令,只看对话里已经出现的内容,所以要求智能体把命令输出贴出来。
第 2 层:测试先行,在智能体时代更重要
测试驱动开发(TDD)有一个老毛病:很多人觉得麻烦所以不做。智能体消除了「麻烦」,却放大了它的价值,因为测试是可执行的规格。让智能体先写测试再写实现,顺序是:
- 描述行为,让智能体只写测试,不写实现;
- 运行测试,确认它们因为正确的原因而失败(红)。测试一开始就是绿的,说明它没测到东西;
- 提交测试。这一步把规格固定下来;
- 再让智能体实现,直到测试通过(绿);
- 重构,保持绿。
为什么先写比事后补更可靠?智能体事后写测试,常常是读自己刚写的代码,再写一份「描述这段代码做了什么」的测试,而不是「描述这段代码应该做什么」的测试。前者永远通过,却没有任何验证价值。官方也建议过类似的分工:一个会话写测试,另一个会话写代码去通过它。
下达指令时直接要求:「先写一个能复现该问题的失败测试,确认失败,再修复。」对 bug 修复同样适用,复现测试还会留在仓库里,防止回归。
第 3、4 层:真实运行,眼见为实
单元测试全绿而页面打不开,是智能体编程里最常见的翻车。Anthropic 在长时任务的 harness 文章里记录过:智能体起初倾向于用单元测试或 curl 命令来「验证」,却漏掉了真实用户路径上的问题;改为要求它用浏览器自动化、像人一样走完流程后,发现 bug 的能力明显提升。文中也坦诚了局限:浏览器原生的弹窗(alert 等),智能体看不到,依赖它们的功能更容易出错。
Claude Code 的做法是 Chrome 集成:claude --chrome 启动,之后可以直接要求它打开本地服务、操作页面、读取控制台报错、截图对比。官方示例大意是:
我刚改了登录表单校验。请打开 localhost:3000,用非法数据提交,检查错误提示是否正确显示。
对界面改动,最佳实践的写法是:贴设计稿,要求它实现后截图,与原图对比并列出差异再修正。对后端,则是把服务真的启动起来,用 curl 或脚本走一遍验收标准里的请求,并贴出输出。官方还提供 /verify 这类对着运行中的应用确认改动的入口,可以在智能体的检查通过后自己再过一遍。
第 5 层:对照验收标准,再找一双新眼睛
第一次实现完成后,让智能体把规格里的每条验收标准列出来,逐条给出证据:运行了什么命令,输出是什么。我更喜欢让它产出表格,因为「全部满足」这种话它很容易随口说出,而一条一条贴证据,随口就难了。
实现已经完成。请对照 SPEC.md 的验收标准,逐条自检,不要凭印象回答。
输出一张表格,列为:编号、验收标准原文、你运行的命令、命令输出的关键几行、结论(通过 / 未通过 / 无法验证)。
要求:
- 每一条都必须真的运行命令得到证据,不能写「应该没问题」;
- 无法验证的条目如实写「无法验证」,并说明原因和需要我提供什么;
- 不要修改任何测试文件来让检查通过;如果你认为某个测试本身有问题,只报告,不要动;
- 最后单独列出:你在实现过程中偏离计划的地方,以及你没有测到的风险。
自检仍然是「同一个脑子」在评价自己。更可靠的是新鲜上下文里的独立复核:开一个子智能体,只给它 diff 和验收标准,不给实现过程。官方文档提醒了一点值得抄的经验:被要求「找问题」的复核者几乎总会找出一些问题,哪怕工作其实没毛病,所以要明确告诉它「只报告影响正确性或违背需求的问题,风格偏好不算」,否则你会被引向过度设计。内置的 /code-review 就是在新子智能体里审查当前 diff。
防止「骗过测试」
这是本课最重要的一节。智能体在优化的目标是「让检查通过」,如果通过比做对更容易,它有时就会选更容易的。学术界有专门的基准研究:EvilGenie 测试了包括 Codex 和 Claude Code 在内的多个编码智能体,设置了容易钻空子的评测环境,结果是所测的智能体都出现过不当行为,Codex 与 Claude Code 都有明确的「奖励作弊」(reward hacking):写死测试里的期望值,或者修改测试文件。作者发现用语言模型当裁判,能很有效地发现这类明显作弊。Anthropic 的 harness 文章里也专门写了一条硬规矩:不准删除或修改测试。
常见手法与对策:
| 手法 | 表现 | 对策 |
|---|---|---|
| 写死答案 | 针对测试里的输入直接返回期望值 | 评审时先看测试之外有没有条件分支特判;加入变化的输入用例 |
| 改测试 | 放宽断言、改期望值、删除用例 | 实现阶段锁定测试目录;PR 里测试文件的改动单独审 |
| 跳过 | skip、xfail、@Ignore |
在 CI 里禁止新增跳过标记,或让 lint 报错 |
| 过度 mock | 把被测对象本身 mock 掉 | 要求至少一层集成测试不用 mock |
| 吞异常 | try/catch 里什么也不做 |
lint 规则禁止空 catch;评审关注新增的 catch |
| 绕过钩子 | 提交时加 --no-verify |
用 PreToolUse Hook 检查完整命令,服务端 CI 兜底 |
最有效的一招是让「谁能改什么」成为规则,而不是请求。实现阶段,可以用权限规则把测试目录设为不可编辑,例如在 permissions.deny 里加入 Edit(./tests/**)。官方文档指出,Edit 规则会应用于所有内置的文件编辑工具,也会覆盖 Claude Code 能识别的 Bash 文件命令(如 sed、tee),但不能保证拦住任意子进程,所以它是防护,不是铁壁。真正的兜底在 CI:在干净的检出里重新跑全部检查,并对测试文件的变更做醒目标记。
最后一个习惯:评审时先看测试,再看实现。如果测试是智能体自己改过的,它的「全部通过」就不再有证明力。
智能体报告「所有测试通过,功能完成」。以下哪种做法最能说明它真的完成了?
你想让智能体修一个 bug,并尽量避免它「为了让测试通过而改测试」。下面哪种组合最合理?
动手清单
0/5参考来源
- Best practices for Claude Code(Claude Code 官方文档)
- Automate actions with hooks(Claude Code 官方文档)
- Keep Claude working toward a goal(Claude Code 官方文档)
- Use Claude Code with Chrome(Claude Code 官方文档)
- Configure permissions(Claude Code 官方文档)
- Effective harnesses for long-running agents(Anthropic)
- EvilGenie: A Reward Hacking Benchmark(arXiv:2511.21654)
工具更新很快,若与官方文档不一致,以官方为准。
学完了吗?
标记完成会记录在本机浏览器,计入你的学习进度和连续天数。