ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

多Agent协作编程如何防止“假完成”?验收门禁流水线设计

多Agent协作编程如何防止“假完成”?验收门禁流水线设计

多 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, cwd=workspace, timeout=contract.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(passed=False, details=results) return GateResult(passed=True, details=results)

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(任务拆解与契约生成)、executor(Coder/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 里提前打预防针,从源头减少门禁打回。这比事后反复修效率高得多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进