实战篇 05:Hooks:代码验收
一句话总结:模型说“改好了”不算数,程序自己跑一遍测试,没通过就把失败输出塞回循环接着修。
本实战新增: 用第 04 章的 Stop Hook(on_stop)在 Agent 准备结束时运行验收命令。Agent Loop 一行没改。
主要对应理论篇:第 04 章 Hooks 的 Stop 节点。第 17 章 Goal Loop 也讨论任务何时完成,但用另一个模型评审目标;本实战用测试命令检查代码结果。
先看结构
模型这一轮没要工具,想结束
↓
这次任务改过文件吗? ── 没改 ──→ 结束(聊天、提问不验收)
│
改过
↓
程序运行验收命令(默认 python3 -m unittest)
↓
退出码 0? ── 是 ──→ 页面显示“验收通过”,结束
│
否
↓
失败输出变成一条 [验收] 消息 → 回到模型,接着修三个出口,保证它不会无限转下去:
- 验收通过;
- 上次失败后,模型没有再改文件(它自己也没办法了);
- 自动重试满 3 次,停下来交给你。
和第 17 章有什么不同
| 第 17 章 Goal Loop | 本实战 | |
|---|---|---|
| 谁判断完成 | 另一个模型读对话记录 | 程序直接跑命令看退出码 |
| 依据 | 对话里写了什么 | 真实的测试结果 |
| 适合 | 很难写成命令的目标 | 能用一条命令验证的目标 |
能写成命令的验收,就别交给模型判断。命令不会被说服,也不会“觉得差不多了”。
用生活例子理解
装修师傅说“水管修好了”,你不会只听这一句,而是拧开水龙头看看漏不漏。漏了就叫他回来接着修,不漏才付钱。
on_stop 就是那个拧水龙头的动作。
跑起来
demo/ 里有一个故意留了两个 bug 的 pricing.py,还有 4 个测试,其中 3 个会失败。
uv run python labs/01-mini-coding-agent/server.py --lab 05会自动挂上 verify 扩展,并把工作区切到 demo/。页面上会显示下面这几个示例,点一下就能填进输入框。
打开 http://127.0.0.1:8765。先说一句“你好”,你会发现不会触发验收。然后试试:
pricing.py 里有 bug,帮我修一下改完以后,时间线上会出现一行验收结果。模型第一次没修全时,你会看到“验收没通过”,接着它会继续修。
再试一个验收和你的要求冲突的情况:
pricing.py 里的 apply_discount 有 bug,只修它,别动别的函数实测的过程是:模型只修了 apply_discount;验收发现 order_total 还有问题,把失败输出送了回去;模型说明“修它超出了你允许的范围”,没有再改;程序看到它没再改文件,于是停下来交给你。
想换成别的验收命令,用环境变量:
VERIFY_COMMAND="python3 -m pytest -q" uv run python labs/01-mini-coding-agent/server.py --workspace 你的项目 --ext verifyAgent 会真的改掉
demo/pricing.py。想重新练习,运行git checkout labs/05-verify/demo恢复。
代码里值得看的四处
1. 什么时候才验收。 只看这次任务里成功执行过的 write 和 edit。被你拒绝的、报错的都不算,因为文件没变。
bash 也不算,哪怕是非只读命令。你只说“跑一下测试看看”时,模型运行的测试命令就是非只读的;算进来的话,验收会推着它去修 bug,这已经超出了你的要求。代价是:模型用 sed -i 这类命令改的文件不会触发验收。
2. 验收插进来的消息带 [验收] 前缀。 这样程序能分清哪条是你说的、哪条是验收说的,“这次任务”才能从你最后一次发言算起。
3. 反馈必须写明来历。 接口里没有专门给程序用的角色,反馈只能以 role=user 送回去。第一版只写了“请继续修复”,实测时模型把它当成了你的新指示,推翻了你说的“别动别的函数”。现在反馈里写明“这条来自程序的自动验收,不是用户的新要求”,system prompt 里也写了同样的规则。这和第 09 章说的“旧记忆不能偷偷变成新命令”是同一个问题。
4. 验收命令不走授权。 这条命令是启动的人写的,不是模型提的,所以不用弹窗。执行时仍然复用 bash 工具的执行器,一样有超时限制,也一样不把 API Key 传给子进程。
今天只记住
“模型想停”和“任务完成”之间,要隔着一次程序自己做的检查。
想一想
如果模型发现测试很难通过,就去把测试改掉了,验收会通过吗?你会怎么防住这件事?
参考思路(先自己想一想,再展开)
会通过,因为验收只看退出码。system prompt 里那句“不要去改测试”只是提醒,拦不住模型。
真正拦得住的办法要放在 Harness 里。比如写一个自定义 Tool.confirm_prompt,让改测试文件的 write / edit 直接报错;或者在 on_stop 里检查这次任务有没有动过 test_*.py,动过就不算通过。