
好几个做多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?fromto - 输出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 --covsrc --cov-reportjson: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干活”里解放出来。