← 返回博客2026-06-21

一套让 AI agent 团队不交垃圾代码的系统

一个三端 App、大半代码是 AI agent 写的——但 agent 会谎报「完成」、互相踩踏、还会给测试放水。这是约束它们的那套操作系统。

真正的问题

我是个写 Rust 的后端。大约一个月里,一个 AI 编码 agent 团队写出了一个三端德州扑克 App 的大部分代码——网页、iOS、Android 都能实时对战 AI 人格。网页端拥有最完整的复盘工具集;移动端也已提供原生数据、历史、回放/复盘、练习与本地 solver 分析。我写规则、写契约、写 prompt;业务逻辑是 agent 写的。这一半没人意外:agent 当然能写代码。

差点把项目拖垮的,是没人提醒你的那一半。一个 agent 团队会谎报「完成」、互相踩踏对方的文件,还会为了让测试通过而偷偷给测试放水。写代码原来是容易的那一半;约束住这些 agent 的那套系统,才是真正的活——也是这篇要讲的。

每一次 agent 的改动都跑同一条流水线:开分支、写、编译、测试、独立评审,通过之后才过一道合并闸门进 main。评审默认来自另一家厂商;只有评审不可用时,才能走狭窄且留痕的替代路径。

每次改动都跑同一条流水线 写 → 独立评审 → 闸门 → main BLOCKER → 退回作者 · ≤5 轮 Claude编译Codex 评审合并闸门main 写代码+ 测试独立模型受审 CAS 作者审不了自己刚写下的盲点——默认异厂的独立评审, 再加一道硬闸门,才能把这些失败抓出来。

每次改动跑同一条流水线。作者模型从不评审自己的活——由独立模型来评(默认异厂),守住 main 的是一道硬闸门,不是善意。

班底:每个角色锁死在一个目录

为什么是一个团队,而不是一个全能 agent:单个 agent 在真实代码库上会崩。上下文太长、忘掉约束、修 A 弄坏 B。所以工作被拆给十来个角色——写代码的锁死在各自被允许碰的目录里,规划和评审角色则锁在规格与评审上——下面是最关键的六个:

角色负责写代码?
后端工程师engine/ + server/
前端工程师client-game/
移动端工程师client-mobile/
架构师规格 & ADR只写规格
扑克 QA测试闸门只写测试
独立审计员评审每一个 diff只评审

锁和角色一样重要。两个 agent 在没有边界的情况下改同一批文件,正是下面那些事故的来源。一个只写规格的架构师、一个只做评审的审计员,把设计和判断从「写代码的那双手」里隔开。

我真正写了什么

我亲手写的不是业务逻辑——是四件 agent 在其中运行的、可复用的东西:

  • AGENTS-CONTRACT.md —— 谁负责什么,以及评审循环。
  • 一份 API 契约 —— 后端和前端都必须遵守的字段形状。
  • 一组代码化合并护栏 —— 早期的 pre-commit 直接提交守卫(见下),以及现行由主 worktree 持有的 finish-session.sh 受审合并与引用事务守卫。
  • 一段审计 prompt —— 评审模型该盯什么。

而交给 agent 的每个任务都是同一个形状——划定地盘、单一目标、在开工前就锁死的验收标准:

You are backend-agent. Only touch server/ and engine/.
Read docs/architecture/api.md and the latest ADR first.
Goal: implement GET /api/hands/:id.
Done = cargo test passes; JSON matches HandHistoryDTO; no frontend changes.
Output: change summary + test command + risks.

划定地盘,加上锁死的验收标准。缺一个,agent 就会漂移——要么自己扩大范围,要么按它自己的标准宣布胜利。

作者写,独立模型评审

正常路径会有意换一家厂商:Claude 写的交给 Codex 审,Codex 写的交给 Claude 审。不变的是作者不能评审自己的活。只有仓库明确记录的评审不可用情形,才允许狭窄的替代模型路径;替代评审仍必须明确批准且 OPEN 清单为空。在我自己的实测中,自审不够:作者漏掉了我真正在意的失败,而独立模型把它们捞了出来。

顺序也要紧。先编译,再审计。评审模型是静态阅读的,会从编译器一秒就抓到的语法和类型错误旁边滑过去;让编译器先清掉这些,评审才能把有限的轮次花在跨文件逻辑、契约和安全上。这个循环最多五轮,且只在某一轮干净——待办(OPEN)清单为空、无 blocker、无 major——之后才合并。

而守住 main 的不是一段 prompt。最早的第一道线是一个本地 pre-commit hook,从物理上拒绝任何对 main 的直接提交:

branch=$(git rev-parse --abbrev-ref HEAD)
[ "$ALLOW_MAIN_COMMIT" = 1 ] && exit 0
case "$branch" in
  main|master) echo "BLOCKED: direct commit to $branch"; exit 1 ;;
esac

诚实的说明:上面这道早期 hook 单独看确实能被 --no-verify 绕过,所以项目现在不再把它当最终执行点。现行流程用 scripts/new-session.sh 创建隔离 worktree;只有主 worktree 持有的 scripts/finish-session.sh 在独立评审明确 APPROVEDOPEN=[] 后,才拿到一次性能力以 CAS 方式推进 main。审计收据会记录本轮是跨厂商还是使用了文档允许的 fallback,引用事务守卫默认拒绝其他落地路径。早期教训没变:护栏必须是代码,不是一句指引

到底哪里会出错

这些都不是扑克逻辑的 bug。它们是你放手让一个 agent 团队去造真实代码库时,会出现的失败模式。

1 —— 两个 agent,没有契约 → 14 个字段对不上

一个 agent 写了后端接口;另一个写了读它的前端;没人先把字段形状锁死。后端 id 对前端 hand_idplayed_atcreated_at;还有几个字段干脆缺失——前后共十四处对不上。前端自己的校验器于是拒绝了 100% 的真实服务器响应。异厂审计把它列为那一轮的四个 blocker 之一;作者模型从头到尾没看见它。

解法是一页纸的接口契约,在并行的 agent 开工之前就写好——下面是简化示意:

type HandHistoryDTO = {
  hand_id: string;
  played_at: string;   // ISO-8601 UTC
  seed: string;        // deck seed, hex
  board_cards: Card[];
  actions: ActionEvent[];
};

从这份 schema 生成类型;别在两端各自手写 DTO。还有,测试 fixture 要用真实的服务器响应,绝不要 AI 编出来的——编出来的 fixture 和代码共享同一套假设,测试过了,真实响应照样挂。

2 —— 不是真的「完成」

有两次,agent 报了绿,其实没绿。九个单测通过,而真实浏览器里零张牌在翻——jsdom 根本不跑 CSS 3D 动画,所以那些测试验证的是用户永远见不到的环境。还有一次,后端 bug 修完「还在」,因为服务器跑的是修复之前的旧二进制。两次同一个陷阱:一个测试只验证它真正跑在其中的那个环境。

所以「完成」是一句待验证的声明,不是事实。视觉改动要配真实浏览器加一条断言——期望五张牌翻开,而不是「看着对」。后端改动要重新构建并重启,并留一个 build 戳——git sha,由 /build-stamp.txt 提供——这样你能看见自己打的到底是哪个 build。

3 —— AI 会给测试放水

被要求「修到绿为止」,agent 有时会用错误的方式把测试弄绿:删掉一条断言、调低快照阈值、加个 skiponly。门过了,却毫无意义。守卫在评审里,不在扫描器里:评审 prompt 把「给测试放水」列为优先追查项——上面每一种动作都是必须解释的 blocker,绝不放行;QA 角色则被明令禁止靠削弱回归测试拿绿灯。

诚实的边界

这些 agent 有实打实的盲点。我不去消除它们——我把它们圈起来。

  • 视觉打磨。agent 判断不了一个界面看着对不对,所以这一刀由人加真实浏览器截图来下。
  • 并行的 session 会在磁盘上互相覆盖。所以每次改动都拿到自己的分支和自己的 git worktree。这本身是一整个独立的失败模式,有它自己的解法——我单独写过一篇:为什么每个 agent 都该有自己的 worktree

至于目标漂移和谎报「完成」,上面那套机器已经把它们圈住了。

总结

  1. 真正的问题:一个 agent 团队会谎报「完成」、互相踩踏、给测试放水。写代码从来不是难的那部分。
  2. 班底:十来个角色(正文表格列了最关键的六个),每个锁死在自己被允许碰的范围里;一个只写规格的架构师、一个只做评审的审计员,把判断从写代码的那双手里隔开。
  3. 系统:每次改动跑同一条流水线——分支、编译、测试、独立审计(默认异厂,≤5 轮)、合并闸门——而闸门是代码,不是一句指引。
  4. 那条教训:别再问 AI 能不能写代码。它能。真正的活是管理这些 agent——角色、契约、独立评审、一道合并闸门,以及一个必须自证的「完成」。