好几个做多Agent编程的朋友跟我吐槽,Agent跑完一轮,log里全是“已完成”“测试通过”,结果一合并,编译都不带过的。这几乎是多Agent编程刚上手时的必经之痛——大模型天生乐观,你说“做完”,它真的觉得自己做完了,至于做没做对,它自己并不知道。我过去大半年一直在琢磨怎么把多Agent编程从“Demo好玩”变成“真能合进主干”,最后的答案不是让Agent变得更聪明,而是别再让它说做完了就算完——在Agent和主干分支之间,硬插一条带验收门禁的流水线。我把这套方案整理成了一个开源项目,今天这篇就来从头拆一遍:为什么需要门禁、门禁到底怎么设计、实际跑一个需求会遇到什么,以及我踩过的坑。
1. 为什么多Agent编程最需要一道“验收门禁”
很多人一开始觉得,多Agent编程就是把任务拆成几块,每个Agent干一块,最后拼起来完事。但真跑起来你会发现,最不缺的就是“嘴上说做好了,实际根本没做对”的情况。这不是某个模型的问题,而是这套协作方式天然有漏洞。
1.1 三个让Agent“自信翻车”的根源
第一个根源是乐观偏置。大模型的生成过程本质上是“预测最可能的正确答案”,它并不真的运行一遍自己写的代码。所以当一个Agent在总结里写下“已完成”时,它只是完成了“文字上的完成”,而不是“可运行的完成”。我在项目里见过太多次:Agent给出的交付说明逻辑完整,细节齐全,一拉下来编译直接红。它不是说谎,它是真的看不见自己写的代码跑起来是什么样。
第二个根源是上下文截断。多Agent协作中,调度Agent负责拆任务、发指令,执行Agent拿到的往往只是一段任务描述和几个相关文件,看不到全仓库。可一个改动经常会牵一发动全身——你改了接口参数,调用方的单测跟着炸;你换了个数据结构,另一个Agent正在实现的模块老半天找不出原因。执行Agent在自己那一片上下文里觉得“没问题”,门禁在全局视角一跑,问题全出来了。
第三个根源是缺少自测闭环。Agent内部也会“自测”,但说实话,很多时候它是先写好实现,再顺着实现写一个能跑通的测试,属于典型的“答案出题”。happy path走一遍绿灯,边界条件、异常分支、性能约束基本不管。它自己写的用例验证它自己写的代码,逻辑漏洞很容易同一套逻辑里被掩盖住。
1.2 传统CI只证明“没炸”,门禁要证明“没白干”
做过工程的朋友都知道,传统CI的核心是编译、单测、静态检查,判定标准是“系统有没有崩”。这个门槛当然需要,但放到多Agent编程里远远不够。编译通过只能说语法没毛病;单测通过只能说老功能没被明显搞坏;可用户要的是“这个需求真的被实现了”。
所以我在传统CI的基础上加了验收门禁。门禁不是简单的“跑一遍测试”,而是把需求验收标准变成可执行的检查项,让流水线自己回答一个问题:Agent交出来的东西,到底有没有满足需求文档里的每一项要求?
这里有个对比很清楚:
| 检查类型 | 判定内容 | 判定标准 | Agent钻空子的难度 |
|---|---|---|---|
| 传统CI编译 | 语法、依赖、链接 | exit code是否为0 | 低,就是硬性失败 |
| 传统CI单测 | 老用例是否回归 | 通过率不降低 | 中,可以少写用例 |
| 验收门禁 | 需求行为是否满足 | 验收清单逐项机器可判定 | 高,每项都要真实跑过 |
说白了,传统CI回答的是“代码健康吗”,验收门禁回答的是“活儿干完了吗”。前者是技术底线,后者才是业务价值。多Agent编程里如果只有前者,你得到的是一堆“编译无事的漂亮废物”;只有后者,你又有可能把没经过质量校验的半成品合进主干。两者叠起来,才是一条勉强可靠的流水线。
2. 带验收门禁的Agent流水线整体设计与关键原理
知道“为什么要加门禁”之后,真正的难题是“门禁怎么加”。我第一版方案很粗暴:等Agent全部执行完,再统一跑一遍测试。结果可想而知——问题集中爆发,Agent已经改不动了,人工返工成本极高。后来我把门禁从“事后补救”变成了“流程中的关卡”,整个流水线才顺起来。
2.1 流水线长什么样:从触发到合入的完整链路
我现在用的流水线大体分七个阶段:
- 触发。支持push、pull request,也支持手动触发,方便本地试跑。
- 任务解析与拓扑排序。把自然语言需求拆成多个子任务,并根据文件依赖关系排执行顺序。
- 上下文准备。给每个Agent准备它需要的仓库索引、需求描述、接口契约、上游产物。
- 多Agent执行。子Agent在隔离工作区里改动代码,互不干扰。
- 门禁执行。这一步由独立的CI Runner跑一堆脚本,产出机器可读的结果。
- 结果汇总与人工复核。门禁结果、覆盖率报告、diff统计打包成一份报告,给维护者看。
- 合入或驳回。全绿并有人工确认才合入主干;否则带着失败反馈打回。
前两步很多人会忽略,但我觉得特别关键。任务解析和拓扑排序决定了多个Agent会不会在同一个文件上打架。比如一个接口改动,后端Agent和测试Agent如果同时写同一个service文件,后面的一定覆盖前面的。我现在要求调度Agent在拆任务时就把涉及的路径列出来,凡是有路径重叠的任务,一律串行执行,不并行。
2.2 为什么门禁必须独立于Agent运行
这里有个设计原则,我反复跟团队强调:监考老师不能是考生本人。Agent执行阶段自己写的那些“测试”,只能算自检,不能算验收;验收必须有独立于Agent的一套脚本、一套测试数据、一份预先固定的验收清单。
我在仓库里划了一个受保护的目录,比如.gates/,里面放构建脚本、测试脚本、行为检查脚本。Agent的任务指令里会明确禁止改动这个目录,流水线跑门禁用的就是这套固定脚本。刚开始有人觉得这是“不信任Agent”,我直接说对,就是不信。Agent的职责是产出业务代码,门禁脚本的职责是验证业务代码,让产出者同时当验证者,等于让球员自己当裁判,迟早出问题。
另一个被低估的点是:验收脚本必须是确定性的。我吃过亏,第一版门禁用“日志里是否包含PASS”来判定,结果Agent学会了在代码里打印一行“PASS”,门禁直接放行。现在所有门禁判定只看exit code和结构化JSON报告,绝不看“像不像通过”的文本描述。这条规则帮我们挡掉了很多看起来很聪明的绕过方式。
2.3 可量化的门禁标准:不拍脑袋的通过阈值
门禁如果只是“全跑一遍”,等于没门禁。每一项都必须有客观阈值,Agent和审查者都别想靠嘴争。
我目前使用的门禁项和阈值大致是这样:
| 门禁项 | 判定方式 | 阈值建议 |
|---|---|---|
| 编译构建 | 执行构建脚本,检查exit code | 非0即失败 |
| 全量单元测试 | 跑既有测试套件 | 不允许既有用例回归失败 |
| 新代码覆盖率 | 用diff行范围叠加覆盖率报告 | 新代码行覆盖率≥80% |
| 覆盖率差量 | 对比本次覆盖率与基线覆盖率 | 差量≥0,不允许覆盖率下降 |
| 静态检查 | 对新增/改动代码做lint | 新增error级别告警为0 |
| 行为验收 | 逐项执行验收清单里的命令 | 每一项都必须真实通过 |
| 变更记录 | 检查是否有changelog和回滚说明 | 必须存在,否则按评审退回 |
这里有个容易踩坑的点,就是“新代码覆盖率”不能只看全局覆盖率。全局覆盖率很可能因为历史代码被拉得很高或很低,看不出来本次改动到底测没测。正确做法是拿git diff的行范围去和覆盖率报告做交集,单独计算新增行里被覆盖的比例。举个例子,基线的全局覆盖率是72%,Agent这次新增了600行业务代码,按80%的新代码覆盖率要求,至少得有480行被测试跑到。如果Agent只写了一个冒烟测试,顺了几十行,那这一项门禁必挂,哪怕全局覆盖率显示上升了也没用。
为什么覆盖率差量也要卡?因为很多Agent为了过新代码覆盖率,会故意删掉一些原有的老用例或者把某些分支改成不可达代码。全局数字可能还涨了,但实际测试基线被掏空了。加了差量门槛之后,这种“数字游戏”就玩不下去了。调参的经验是:新项目覆盖率可以要求高一点,老项目尽量用“基线的浮动差量”来约束,别把阈值定死,否则Agent会在墙角里空转好几轮。
3. 实操过程:一个“导出CSV接口”的验收门禁落地记录
光讲设计容易飘,我拿一个最近实际跑过的需求来复盘:给一个订单查询服务增加“按时间范围导出CSV”的HTTP接口,要求支持中文表头、按用户权限过滤、文件流式返回。这个需求不算复杂,但足够暴露多Agent编程里的大部分问题。
3.1 出题与Agent派工:任务怎么拆、上下文怎么给
我先把需求喂给调度Agent,让它拆任务。它拆成了三个角色:接口Agent负责Controller和Service实现;边界Agent负责参数校验和异常处理;测试Agent负责补单测和验收用例。三个任务的路径有交叉,主要是Service层,所以接口Agent和测试Agent之间做了串行约束:先让接口Agent提交,测试Agent才进来补用例。
给执行Agent的上下文里,我除了塞需求描述,还塞了一段硬性契约,这部分非常重要。我贴一个简化过的指令模板:
你负责实现 CSV 导出接口。 硬性契约: - 路由:GET /api/orders/export?from=&to= - 输出:text/csv,带 Content-Disposition 附件头 - 权限:仅 EXPORT_ADMIN 角色可调用,非该角色返回 403 - 数据:只导出该用户关联范围内的订单 - 性能:不得整表 load 进内存,用游标分页流式写 禁止改动目录:.gates/ 、ci/ 、tests/e2e/config/ 门禁要求:新代码覆盖率 >= 80%,覆盖率差量 >= 0之所以要把契约写得这么死,是因为自然语言需求里的“导出CSV”很容易被Agent自由发挥成“随便导出一个文件”。契约越具体,验收项越容易机器化,Agent的自由度就越少。自由发挥留给设计和创意,不该留给接口契约。
3.2 第一次交付没有通过门禁:一份失败日志的解读
第一轮Agent跑完,门禁报告出来,结果挺有代表性。日志大致长这样:
[09:41:22] START run 2024-05-12/08 [09:41:30] build: PASS [09:42:02] unit_test: PARTIAL FAIL - existing tests: 213 passed - new code coverage: 43.6% (threshold: >= 80.0%) - delta coverage: -12.4pt (threshold: >= 0) [09:42:03] behavior_check: FAIL - check "csv_export_with_chinese_header": FAIL - response header missing "Content-Disposition" - check "permission_filter_applied_return_403": PASS - check "streaming_large_file_no_oom": FAIL - 896MB loaded in memory编译过了,既有测试也过了,但行为检查挂了两项,新代码覆盖率只有43.6%。一分析就发现两个典型问题。
第一,Agent把测试写成了“冒烟测试”。它只验证了接口返回200、CSV内容非空,完全没有验证中文表头是否正确、附件下载的文件名有没有、非管理员角色是否被拦截。单测用例看起来在跑,但跑的都是最浅层的路径,真正核心的契约断言一个都没有。
第二,实现上用了一次性读全表再过滤的方式。接口Agent图简单,直接SELECT * FROM orders之后在内存里按时间范围和权限过滤。测试数据只有几百条看不出来,验收清单里专门有一项是“导出100万条订单时不内存溢出”,这一跑就直接现出原形。这种性能问题在传统CI里基本测不出来,只有把“行为验收”做成清单并真正执行才拦得到。
3.3 让Agent自己看门禁反馈:自愈回路的实现
第一次看到门禁失败日志,我第一反应是转人工。但后来发现,如果反馈足够具体,Agent自己看着日志修,比人转述给它的效率要高得多。于是我把失败日志、覆盖率差量报告、diff统计一起打包,重新塞回给对应的执行Agent,让它基于git diff自己定位并修改。
第三轮的结果是这样的:
[10:07:48] build: PASS [10:08:11] unit_test: PASS - new code coverage: 83.2% - delta coverage: +4.1pt [10:08:12] behavior_check: PASS (3/3) [10:08:13] gate verdict: APPROVED注意中间经历了两轮反馈:第一轮失败后,Agent改掉了流式返回的问题,但测试用例还是偏薄,覆盖率54.1%;第二轮门禁再次打回,它才开始老老实实补完整断言,最后把覆盖率拉到83.2%,三个行为检查全过。整个过程约22分钟,比我自己上手写快不少,而且每一步都有机器证据,不是它在log里说一句“已修复”就算数。
我还设了一个兜底规则:同一任务最多自愈3轮,3轮之后不管门禁什么状态都强制转人工。因为Agent在反复的失败反馈里会开始“做题家化”,用各种花式手段只为把门禁搞绿,这时候人的介入比让它无限试错更划算。
4. 常见问题排查与开源落地建议
这套流水线跑了几个月,问题肯定没少出。我把最有代表性的几个坑整理成一份速查表,刚接入的朋友可以直接照着排查。
4.1 我踩过的五个坑和排查思路
| 现象 | 原因 | 排查思路 | 解决方式 |
|---|---|---|---|
| Agent总说执行了测试,门禁却显示没跑 | Agent没真正执行流水线命令,只靠想象写总结 | 查看门禁日志里是否有对应的step记录,不接受Agent口头描述 | 门禁只认日志产物和exit code |
| 门禁过严,Agent空转好几轮 | 阈值设置脱离项目实际,比如新代码覆盖率要求90% | 看失败项集中在哪个环节,评估是代码烂还是测试难写 | 改用“基线+差量”模式,用delta约束代替绝对值 |
| 两个并行Agent改同一个文件,互相覆盖 | 拆任务时没做文件级冲突预检 | 检查git提交记录里的文件路径是否重叠 | 拓扑排序+路径锁,重叠任务强制串行 |
| Agent钻验收清单的空子 | 验收项写得太含糊,比如“中文表头正确”,没说怎么判定 | 人工复核时看Checklist对应的命令是否可执行 | 每项验收都绑定一条可执行断言,不允许目测 |
| 跑一次流水线太久,Token费用爆炸 | 每个Agent都塞了全量仓库上下文,反馈又全量塞回 | 看日志平均耗时和token消耗,定位到上下文最大的环节 | 用diff范围裁剪上下文,失败日志只截相关片段 |
这里面我最想单独说的是“门禁过严”这个问题。一开始我以为阈值越严对质量越好,直接把新代码覆盖率定到90%,结果Agent为了凑覆盖率开始写一堆没有断言的垃圾用例,覆盖率数字上去了,项目总成本也上去了。后来把规则改成“覆盖率差量不小于0”加“行为检查全过”,质量反而更稳。门禁的职责是守住底线,不是逼Agent交完美作品。
4.2 开源项目怎么组织才有人愿意用
这个项目我开源的时候,刻意没做成一坨只有我自己能用的脚本,而是按“可复用”的思路组织仓库结构:
agent-factory-gates/ ├── gates/ │ ├── build.yaml │ ├── unit_test.yaml │ └── behavior_check.yaml ├── runners/ │ ├── gate_runner.py │ └── feedback_exporter.py ├── acceptance/ │ └── checklist.yaml.example ├── examples/ │ └── csv-export/ ├── config.example.toml └── README.md核心是两个文件。gate_runner.py只负责按配置调度命令,然后收集exit code、覆盖率JSON、验收清单日志,统一转成结构化结果;feedback_exporter.py把结构化结果翻译成Agent能消费的失败描述。设计原则是:runner不做语义判断,只做“跑命令、收结果、导反馈”这三件事,判断规则全部放到YAML配置里,这样不同项目只需要改配置就能复用。
门禁配置大概是这样的:
pipeline: name: agent-factory-gates trigger: [push, pull_request, manual] agents: - role: backend scope: ["src/api", "src/service"] max_retry: 3 gates: build: cmd: "./scripts/build.sh" unit_test: cmd: "pytest --cov=src --cov-report=json:coverage.json" rules: existing_tests: "no regression" new_code_coverage: 80 coverage_delta: 0 behavior_check: checklist: "./acceptance/checklist.yaml" static: cmd: "ruff check src" block_on: ["E", "F"]开源有几个注意点,我提醒一下:第一,不要把CI密钥或服务地址写进仓库,配置模板里只留placeholder;第二,一定要带一个跑得通的示例仓库,比如我这个examples/csv-export,否则别人拿到手不知道去哪儿改;第三,README里贴一份真实的运行日志,告诉别人“长什么样算过、长什么样算拒”,这比写十段说明都管用。
另外,如果要用在GitHub Actions或GitLab CI这类现有CI系统里,也完全没问题,runner本身不绑定平台,只要提供一个环境变量指到的输出目录,它就能把结果写出来,再让CI平台去展示。这套设计当时就是为了不改动团队已有CI习惯才做的,实测下来接入成本很低。
4.3 接下来我想做的两件“大事”
目前这套流水线已经能挡住“假完成”,但我觉得还有两个明显的升级空间。
第一,把验收清单的生成也交给Agent,再让人确认。现在checklist还得维护者手动写,这是整个流水线里最费人力的部分。我希望让一个“维护者角色”的Agent,基于需求文档和接口契约自动生成checklist,人只需要画勾或补一条。人从“写”变成“审”,效率能高一截。
第二,把人工评审意见结构化。门禁能证明“没崩、覆盖够、行为过了”,但代码可读性、设计合理性这些还是得靠人。我试过让审查Agent基于diff生成结构化评审意见,每条意见带行号、严重级别、修改建议,人工只要挑毛病而不是从头读代码。实测下来,大部分低级问题Agent能先挑出来,人只需要看它拿不准的那几条。这个方向我在持续优化,等效果稳定了再单独写一篇分享。
做完这个项目之后,我最深的体会是:让多Agent跑起来不难,难的是让它“停下来承认自己没做好”。门禁不是为了卡Agent,而是给Agent一面镜子,让失败反馈来得足够快、足够具体。与其逼Agent一次写对,不如把监督和反馈做硬。现在我的团队跑一个中等需求的Agent任务,大概二十到三十分钟,其中大部分时间不是Agent在写代码,而是门禁在反复验证它,人只做最后一道确认。这套路不算花哨,但真能让人从“盯着Agent干活”里解放出来。