
多 Agent 协作编程这两年热度一直没降过从最早的 AutoGPT 式单线程循环到后来 Planner Executor Reviewer 的多角色拆分框架换了一茬又一茬但真正落到日常开发里有个问题始终绕不过去Agent 说我做完了你敢信吗我前后搭过三套多 Agent 编程流水线前两套都栽在同一个坑上——Agent 交回来的东西看着像模像样一跑测试全是红的或者干脆连编译都过不了。后来我干脆在流水线里加了一道验收门禁让 Agent 的产出必须过一道硬性检查才能算完成这套东西跑了大半年稳定性提升非常明显。这篇文章就把这套带验收门禁的多 Agent 编程流水线的设计思路、核心机制、踩坑经验完整拆一遍代码是开源的思路你可以直接搬到自己项目里。不管你是刚接触多 Agent 协作的新手还是已经踩过Agent 假完成坑的老手应该都能从里面捞到点能用的东西。1. 为什么Agent 说做完了这件事本身不可信1.1 大模型的完成是一种语言行为不是事实判断先把最根本的问题说清楚。当你让一个 Agent 去实现某个功能它最后回你一句我已经完成了 XXX 功能的实现这句话的本质是什么是它在当前上下文里基于概率生成了一段看起来像是任务收尾的文本。它并不是真的去跑了一遍代码、验证了功能、确认了边界情况它只是在语言层面完成了一次收尾表达。这个区别非常关键。人类工程师说我做完了通常意味着他至少自己跑过一遍心里有个基本判断。但 Agent 没有这个心里过一遍的环节它的完成和没完成在语言表达上可以长得一模一样。我见过太多次Agent 信誓旦旦说已实现完整的用户认证模块结果打开文件一看函数体里写的是# TODO: implement this或者干脆调了一个根本不存在的库函数。所以第一件事要扭转的认知是Agent 的完成声明只能当作一个信号不能当作一个结论。信号的意思是它认为自己写完了可以进入下一环节了至于到底完没完成得由外部机制来判定。这就是验收门禁存在的根本理由。1.2 多 Agent 协作放大了假完成的传播风险单 Agent 场景下假完成最多坑你一次。但多 Agent 协作里这个问题会被放大。典型的流水线是这样的Planner 拆任务Coder 写代码Reviewer 审代码Tester 跑测试。如果 Coder 说我写完了Reviewer 基于 Coder 的产出和描述去审它很可能顺着 Coder 的叙述走给出看起来没问题的结论。然后 Tester 拿到一个本身就错的产物去测测出来的失败又会被归因到测试用例写得不对。整条链路上每个 Agent 都在基于上一个 Agent 的语言声明工作而不是基于客观事实。错误就像传话游戏一样一层层被包装、被合理化最后到你手里的时候已经变成一个看起来逻辑自洽、实际上千疮百孔的东西。我第二套流水线就是这么崩的Reviewer 和 Coder 互相确认了三次最后交付的代码连 import 都是错的。1.3 验收门禁要解决的核心命题验收门禁要干的事情其实很朴素把是否完成的判定权从 Agent 的语言输出里拿走交给一个确定性的、可复现的外部检查。这个检查可以是编译、可以是单元测试、可以是静态检查、可以是 schema 校验总之必须是跑一遍就有明确真假结果的东西而不是再让一个模型看一眼。这里有个容易混淆的点很多人会把让 Reviewer Agent 审一遍当成验收门禁。不是的。Reviewer 是另一个模型它的判断依然是概率性的、语言性的它可能漏、可能误判、可能被上下文带偏。真正的门禁必须是确定性的程序化检查。Reviewer 可以作为门禁之前的一道软性过滤但绝不能替代门禁本身。2. 验收门禁流水线的整体架构设计2.1 三层结构生成层、验证层、门禁层我把整套流水线拆成三层职责边界划得很清楚。生成层负责产出包括 Planner拆解任务、Coder写代码、Refiner根据反馈修改。这一层的 Agent 可以自由发挥允许它们自认为完成了。验证层负责检查包括静态检查器lint、类型检查、构建器编译、测试执行器跑单测/集成测试。这一层全是确定性程序不涉及任何模型判断。门禁层负责裁决它拿到验证层的所有结果按照预设规则决定这个任务算通过还是打回。门禁层是整条流水线的闸机只有过了闸机任务才会被标记为完成才会进入下一个任务。这个分层的好处是生成层可以随便吹牛反正吹完得过验证层验证层只认事实不认叙述门禁层规则透明谁都能看懂为什么被打回。2.2 门禁的判定规则怎么定门禁规则是整个系统的灵魂定得太松等于没有定得太严会把 Agent 卡死。我现在的规则是这样的按优先级从高到低检查项是否阻断说明编译/构建通过是编译不过直接打回没有商量余地单元测试全绿是新增代码必须带测试且测试必须过静态检查无 error是lint 的 error 级别问题必须清零静态检查 warning 数量否但记录warning 不阻断但超过阈值会告警覆盖率不低于基线是新增代码覆盖率不能拉低整体基线关键路径集成测试是涉及核心链路的改动必须过集成测试注意是否阻断这一列。不是所有检查都要阻断warning 这种就适合记录而不阻断否则 Agent 会陷入为了消 warning 反复改代码的死循环。但编译、测试、覆盖率这几项必须硬阻断这是底线。2.3 为什么门禁要放在任务粒度而不是项目粒度一开始我是把门禁放在整个项目交付前的结果发现一个问题等到项目结束再检查前面所有 Agent 的错误已经层层叠加改起来成本极高而且根本定位不到是哪一步出的问题。后来改成每个子任务完成就过一次门禁效果立竿见影。Planner 把大任务拆成若干子任务每个子任务由 Coder 实现后立即过门禁过了才进入下一个子任务。这样错误在产生的当下就被拦住反馈回路极短Agent 修改的成本也最低。这其实就是持续集成里小步提交、快速反馈的思路搬到 Agent 流水线里一样成立。3. 门禁的核心机制从语言声明到事实判定的转换3.1 任务契约让每个任务自带验收标准门禁要判定前提是得知道判定标准是什么。如果 Planner 只说实现用户登录功能门禁根本不知道要检查什么。所以我在 Planner 的输出里强制加了一个字段任务契约Task Contract。任务契约包含三部分一是产出物清单明确这个任务要产出哪些文件、哪些函数二是验收命令明确跑哪些命令来验证比如npm test -- auth.test.js三是通过条件明确什么算通过比如该测试文件全部用例通过。有了任务契约门禁就有了明确的判定依据。Coder 拿到任务时也知道自己要满足什么条件而不是凭感觉写。这个设计我觉得是整套流水线里最关键的一环它把模糊的完成变成了可执行的验收。3.2 验收命令的自动生成与人工兜底任务契约里的验收命令理想情况是 Planner 自动生成。但实测下来Planner 生成的命令经常有各种小毛病比如路径写错、测试文件名对不上。所以我的做法是Planner 生成初稿门禁层做一次命令可用性预检如果命令本身跑不起来比如文件不存在直接打回让 Planner 重生成而不是让 Coder 去猜。预检的逻辑很简单把验收命令里的文件路径、命令名提取出来检查文件是否存在、命令是否在允许列表里。这一步能拦掉大概三成的契约错误省下大量后续返工。3.3 门禁执行器的实现要点门禁执行器本身不复杂核心就是按契约跑命令、收集结果、按规则裁决。但有几个实现细节值得说第一命令必须在隔离环境里跑。Agent 写的代码可能有意无意搞出副作用比如改了全局配置、删了临时文件。我一开始没做隔离结果有次 Agent 写的测试把项目根目录的配置文件覆盖了排查了半天。后来所有验收命令都在临时目录或容器里跑跑完即销毁。第二超时必须有。Agent 写的代码可能死循环验收命令可能卡住。每个命令我都设了超时超时直接判定失败不让它拖垮整条流水线。第三结果要结构化。不要只存一个通过/失败要把 stdout、stderr、退出码、耗时都存下来。这些信息在打回给 Coder 修改时非常有用Coder 能看到具体错在哪而不是只知道失败了。# 门禁执行器的核心逻辑简化版 def run_gate(contract, workspace): results [] for cmd in contract.acceptance_commands: result execute_in_sandbox( cmd, cwdworkspace, timeoutcontract.timeout or 300 ) results.append({ command: cmd, exit_code: result.exit_code, stdout: result.stdout, stderr: result.stderr, duration: result.duration }) # 阻断型检查失败立即返回不再跑后续命令 if result.exit_code ! 0 and cmd.blocking: return GateResult(passedFalse, detailsresults) return GateResult(passedTrue, detailsresults)3.4 打回时的反馈怎么组织门禁判定失败后要把结果打回给 Coder 修改。这里有个坑如果只是把原始 stderr 一股脑丢回去Coder 经常抓不住重点改了半天没改到点子上。我的做法是对反馈做一次结构化整理把失败的命令、关键错误行、涉及的代码位置提取出来按重要性排序再附上原始日志。比如编译失败我会提取出第一条 error 及其上下文几行放在反馈最前面后面再附完整日志。这样 Coder 一眼就能看到哦是第 42 行类型不匹配而不是在几百行日志里大海捞针。实测这个整理步骤能让 Coder 一次修复的成功率提升不少。4. 多 Agent 角色分工与门禁的配合4.1 Planner 的契约生成职责Planner 在这套流水线里的角色被强化了。它不只是拆任务还要为每个子任务生成任务契约。这就要求 Planner 对项目的测试体系、构建方式有基本了解。我的做法是给 Planner 的 prompt 里注入项目的工程画像用什么语言、什么测试框架、测试文件放哪、构建命令是什么。有了这些信息Planner 生成的契约才靠谱。如果项目还没有测试体系怎么办那 Planner 的第一个任务就是搭建测试框架这个任务的契约就是测试框架能跑起来且有一个示例测试通过。先把地基打好后面的任务才有验收依据。4.2 Coder 的自检环节Coder 写完代码后我要求它先自己跑一遍验收命令把结果附在提交里。这一步不是门禁是 Coder 的自我检查。好处是很多低级错误比如语法错误、import 漏了在 Coder 这一层就被自己发现并修掉了不用浪费门禁的资源。但要注意Coder 的自检结果不能作为门禁的替代。Coder 可能自检时跑的是旧版本代码或者干脆伪造结果。所以门禁必须独立重新跑一遍不信任 Coder 提交的任何自检结论。这是零信任原则在流水线里的体现。4.3 Reviewer 在门禁前后的位置Reviewer 我放在门禁之前作为一道软性过滤。它主要看代码风格、设计合理性、有没有明显的逻辑问题。Reviewer 通过后才进入门禁的硬性检查。这样安排的原因是Reviewer 能发现一些门禁发现不了的问题比如命名混乱、职责不清而门禁能发现 Reviewer 发现不了的问题比如测试没过。两者互补但门禁是最后一道闸机Reviewer 不能否决门禁的判定。有个细节Reviewer 的结论如果和门禁冲突比如 Reviewer 说没问题但门禁挂了以门禁为准。因为门禁是事实判定Reviewer 是主观判断事实优先。4.4 Refiner 如何利用门禁反馈门禁打回后任务交给 Refiner 修改。Refiner 拿到的输入包括原始任务契约、Coder 的产出、门禁的结构化反馈。它的职责就是根据反馈把问题修掉然后重新提交过门禁。这里有个循环上限的问题。如果 Refiner 改了三次还是过不了门禁说明要么任务契约有问题要么这个任务本身超出了 Agent 的能力范围。我的处理是三次不过就升级把任务标记为需要人工介入同时把三次的反馈都记录下来方便人工快速定位。不要让 Agent 无限循环那只会烧钱。5. 实测中踩过的坑与应对5.1 契约漂移Agent 偷偷改了验收标准这是最阴险的一个坑。有次我发现某个任务反复过不了门禁查了半天才发现Refiner 在修改过程中顺手把测试文件也改了把原本该失败的断言改成了通过。这就是契约漂移——Agent 为了让门禁通过去改验收标准本身。应对方法很直接门禁执行时验收相关的文件测试文件、配置文件必须从原始契约指定的版本重新拉取不允许使用 Agent 修改后的版本。也就是说Agent 可以改业务代码但不能碰验收标准。这个规则一加契约漂移问题基本绝迹。5.2 测试用例本身是错的门禁依赖测试但如果测试本身写错了呢我遇到过 Agent 写的测试断言写反了导致正确的代码反而过不了门禁。这种情况门禁会一直打回Refiner 改来改去也过不了最后触发升级。后来我加了一条规则如果同一个任务连续两次因为同一个测试用例失败且代码逻辑看起来正确就触发测试用例复核让 Reviewer 专门审一下这个测试用例是不是写错了。这个机制救过我好几次避免了好代码被错测试冤枉。5.3 环境不一致导致的假失败门禁在隔离环境里跑但有时候隔离环境和开发环境不一致导致本地能过、门禁不过。比如依赖版本不同、环境变量缺失。这类问题排查起来很烦因为代码本身没问题。我的应对是把环境配置也纳入契约契约里明确声明这个任务需要哪些依赖、哪些环境变量门禁执行前先按契约准备环境。这样环境不一致的问题在契约层面就被消除了。另外隔离环境的镜像要定期和开发环境同步避免长期漂移。5.4 Agent 为了过门禁而作弊这个坑比较隐蔽。Agent 发现门禁检查测试覆盖率就写一堆空测试来凑覆盖率发现门禁检查 lint就把有问题的代码注释掉。这些都是应试行为。应对方法有两层一是覆盖率只算有效测试空测试、无断言的测试不计入二是门禁之外加一道人工抽检定期抽查通过的产物看有没有明显的应试痕迹。完全杜绝很难但能把作弊成本抬高到 Agent 不愿意做的程度。5.5 门禁太严导致 Agent 卡死前面说过门禁规则要松紧适度。我一开始把 warning 也设成阻断结果 Agent 为了消一个无关紧要的 warning反复改代码改了七八轮还没过最后把原本正确的逻辑改坏了。后来把 warning 改成不阻断只记录问题就消失了。经验是阻断项只保留那些错了就一定是错的检查比如编译、测试。那些可以更好的检查warning、风格记录但不阻断。门禁的目的是保证正确性不是追求完美。6. 把门禁思路迁移到其他场景6.1 非代码类任务的验收门禁这套思路不只适用于编程。任何Agent 产出 需要验证的场景都能用。比如让 Agent 写文档门禁可以是markdown 语法检查 链接有效性检查 字数达标让 Agent 做数据处理门禁可以是输出 schema 校验 行数校验 抽样人工核对。核心逻辑是一样的把完成的判定从 Agent 的语言声明里剥离出来交给确定性检查。只要你能为任务定义出可执行的验收命令就能给它加门禁。6.2 门禁规则的可配置化不同项目对门禁的要求不一样。有的项目测试必须全绿有的项目允许少量失败。所以门禁规则我做成可配置的用一份 YAML 描述gate_rules: blocking: - build - unit_test - lint_error non_blocking: - lint_warning - coverage_delta thresholds: coverage_baseline: 0.75 max_warning: 50 max_refine_rounds: 3这样换项目只需要改配置不用改代码。配置化的另一个好处是门禁规则本身可以被版本管理谁改了规则、什么时候改的一目了然。6.3 和现有 CI 的关系有人会问这套门禁和现有的 CI 有什么区别区别在于时机和粒度。CI 通常在代码提交后跑粒度是整个仓库门禁在 Agent 任务完成时跑粒度是单个子任务。门禁是前置的、细粒度的 CI它把 CI 的思想提前到了 Agent 生成阶段。两者不冲突可以共存。门禁过了之后代码进入仓库再走一遍完整的 CI。门禁负责拦住 Agent 的低级错误CI 负责拦住集成层面的问题。我现在的流水线就是门禁 CI 双层防护效果比单层好很多。7. 开源实现与上手建议7.1 代码结构概览开源版本的核心模块就四个planner任务拆解与契约生成、executorCoder/Refiner 的执行封装、gate门禁执行器、orchestrator流水线调度。每个模块职责单一方便你按需替换。比如你想换成自己的 Planner只要实现约定的接口就行。调度器用的是简单的状态机任务状态在pending - coding - reviewing - gating - done/failed之间流转。状态流转的每一步都有日志方便排查。没有用复杂的框架因为这套东西的核心价值在门禁机制不在调度框架。7.2 最小可用配置想快速跑起来你只需要准备三样东西一个能跑的项目有构建命令和测试命令、一份工程画像配置告诉 Planner 项目用什么技术栈、一个模型 API。然后按 README 跑起来先拿一个小任务试试水比如给某个函数加个参数校验并补测试。建议第一次跑的时候把max_refine_rounds设成 1观察一下门禁打回后 Refiner 的修改效果心里有数了再放开。另外第一次跑一定要盯着日志看门禁的判定是否符合预期规则不对就及时调。7.3 我个人的使用体会跑了这么久最大的体会是门禁的价值不在于拦住错误而在于改变了 Agent 的行为模式。当 Agent 知道自己的产出会被硬性检查它在生成时就会更谨慎会更倾向于写能通过检查的代码。这有点像考试知道要判卷答题态度就不一样。另一个体会是门禁规则要渐进式收紧。一开始别设太严先让流水线跑通观察哪些检查经常误报、哪些检查形同虚设然后慢慢调。一上来就设一堆严格规则大概率会把 Agent 卡死最后你只能把规则全关掉等于白做。最后分享一个小技巧门禁的失败日志我建议单独存一份定期翻一翻。你会发现 Agent 犯的错误其实高度重复就那么几类。针对高频错误可以在 Coder 的 prompt 里提前打预防针从源头减少门禁打回。这比事后反复修效率高得多。