项目级 AI Agent harness:约束放在 hook 和测试里,不放在 prompt 里
英文版:English
前提
AI agent 通常不是以崩溃或拒绝的方式出错,而是在没做完的时候说「做完了」。汇报写得流畅、具体,偏偏在你没检查的地方错。如果失败模式是这样,那么控制就不能放在指令文本里。prompt 是对一个刚刚证明过「会说你想听的话」的系统提出的请求。控制必须放在 agent 的自信够不到的地方:在 harness 里运行的 hook,以及要么通过、要么不通过的测试。
整件事围绕这一句话来搭。结果是,四个月里 agent 承担了重建一套数据平台的大部分敲键盘的工作(一个数据库 fork、一个计算平台仓库、一个 Helm chart 仓库),平台已上线生产。「大部分敲键盘」要说准:三个仓库 566+ 次提交,其中八成以上带 Claude 的 co-author trailer。这衡量的是有多少东西经过了 agent,不是 agent 做了多少思考。设计、做什么的取舍、一个结果值不值得信,留在我这里。
三个机制,以及各自为什么在那个位置
改动前的准入门禁(scope gate)。 中大型改动,在有一份已确认的 scope 文档之前,agent 不能写受保护路径:目标、不做什么、验收标准、允许碰哪些路径。它由 pre-tool hook 硬拦,不是靠客气地请求。重点不在流程。开工前先写下「不做什么」,是抓住 agent 正要去解决一个相邻的、更有意思的问题的最便宜办法。
我还得说它哪里做错了,这才是有用的部分。这道门最初是全局默认开启的。实际用起来,多数时候是为了解锁写权限而点过去,并没有澄清任何东西:一个仓库里两天攒了 15 个已确认的 scope,其中 11 个从未走完 review。一个会被人点穿的控制比没有更糟,因为它制造了「已经 review 过」的假象。我把它改成了 opt-in,只在出错代价高的路径上加摩擦。管得更紧并不更好。
记录意图的审计。 每条会改动状态的命令都带一行 # INTENT: 注释:观察到什么、得出什么结论、预期什么效果,由 hook 连同当时的 git ref 一起写进只追加的日志。另有一条规则:改动命令之后必须跟一条验证命令,验证失败就停。日志回答「改了什么」,意图那一行回答「它当时为什么觉得这样做是对的」,后一个问题事后光看 diff 谁也还原不出来。
确定性的 e2e 验收。 平台有 250+ 个端到端用例。它们是确定性的:同样的输入、同样的判定,判定通过与否的环节里没有模型。真正消除那个失败模式的是这个机制。agent 可以在总结里一整天写「全绿」,但它没法靠措辞让一个失败的测试通过。
一个被抓到的例子
我让一个审计子 agent 比较两个文件,它回报「完全相同」。直接 diff 一下,结果是 96 行对 115 行。这个子 agent 并没有通常意义上的撒谎,它给了一个自信、格式完整的答案,却没有做这个结论所需要的比较。如果我照那份总结行事,其中一个文件会被当成重复件归档,而它带着另一个文件没有的逻辑。
这件事有用,不只是吓人,是因为它改变了做法。我不再把子 agent 的「一样」「完整」「已验证」当作结果,而是要求交出证据产物:diff 本身、退出码、测试输出。凡是结论要紧的地方,现在都有一个不用问 agent 就能跑的检查。这是整个项目的通用形式:把「agent 说」换成「产物显示」。
哪些是公开的,哪些不是
harness 本身是公开的:管边界和记录的 hook、管 scope 和 review 的 skill、以及一个能看到哪些 session 在等人、各自做完了什么的看板,在 GitHub:https://github.com/shaorui0/agent-harness。这个仓库展示的是控制机制,不展示它所作用的平台工作;e2e 套件和提交历史在公司私有仓库里。我宁愿把这点说明白,也不想让公开链接给人一种「数字可以从这里核对」的错觉。
我仍然不会声称的
- 提交数和 co-author 比例统计的是 commit,不是有效工作量。一行修复上的 trailer 和一个大改动上的 trailer 各算一次。
- harness 降低「自信地错」的概率,没有消除它。测试只能抓到它覆盖的东西。上面子 agent 那个例子,是因为我碰巧跑了一次 diff 才抓到的,这正是不舒服的地方:这类说法里有一部分没有被检查过。
- 「四个月、已上线生产」描述的是平台。测试和 hook 是我信任这条路径的理由,但我没法单独测出比不用 agent 快了多少,也不做这个声称。