多代理并行编排实战:从TeamFanout到TeamCollect的工程落地
2026/9/10 5:33:50 网站建设 项目流程

1. 先交代背景:单 Agent 卡在哪,AgentTeams 为什么选择并行扇出

做 Code Agent 项目久了,我越来越觉得单代理的瓶颈不在模型能力,而在任务链路太“线性”。早期我搭的 agent 就是典型的循环:拿到任务,读代码,改代码,跑测试,有问题再回头改。一条流水线走到黑,看起来逻辑清晰,实际上每一步都在等上一步。遇到一个跨模块的改动,整个链路被拉得特别长,中间只要有一次工具调用超时或者上下文漂移,后面节奏全乱。

这个感受在代码库变大以后尤其明显。单个 agent 要同时兼顾“看接口定义”“查调用方”“设计改动方案”“写实现”“补测试”这五件事时,Prompt 里的上下文会迅速膨胀。更麻烦的是,很多信息彼此之间没有强依赖,完全不需要串行处理。比如一个前端接口改动,接口签名设计、后端适配、前端调用方升级、测试用例补充,这四块理论上可以同时进行,互相只依赖一份“接口契约”,可是在单代理模型里,它们还是被硬生生排成了先后顺序。

后来我接触到 AgentTeams 的设计思路,核心就是两个组件:TeamFanoutTeamCollect。名字很直白,一个负责把任务“扇出去”,一个负责把结果“收回来”。但它的价值不在名字,而在背后那套关于“什么任务能并行、拆多细、怎么收拢、冲突怎么处理”的完整机制。这篇文章我打算把它拆开来讲,重点放在并行机制的工程实现上,适合已经在做多代理编排、或者正打算把单代理升级成多代理团队的读者。

1.1 不是模型变笨了,是串行链路把错误放大了

我见过不少团队在做多代理的时候,第一步就是堆 agent,一个负责写代码,一个负责 review,一个负责测试,人多了,问题更多了。为什么?因为 review 的 agent 必须等写代码的 agent 彻底结束才能开始,测试 agent 又要等代码和 review 都过。整条链路的时延不是加法,是乘法。

更隐蔽的问题是错误放大。单代理跑一个长任务,模型在中途出现理解偏差,后面还能凭全局上下文自己纠回来一点。可一旦拆成多个 agent,每个子任务拿到的是“局部信息”,一旦上游传下来的信息有偏差,下游所有 agent 都会沿着错误方向走。到最后 Collect 阶段会发现,五个子任务产出了三套互相矛盾的结果,而且你还说不清是哪个环节先歪的。

所以我一直觉得,多代理架构的第一目标不是“把活分掉”,而是“把串行依赖切开,让彼此独立的部分并行跑起来”。这正是 TeamFanout 的存在理由:它不是简单地把一个 Prompt 复制给多个模型,而是先把任务切成可以并行的子问题,再为每个子问题分配独立的执行上下文。

1.2 扇出加汇合,Code Agent 协作的基本模型

TeamFanout 和 TeamCollect 本质上是一对“扇出—汇合”原语,思路跟 MapReduce 里的 Map 和 Reduce 很像,但放到 Code Agent 场景里,复杂程度高了一个量级。MapReduce 处理的是无状态的数据分片,而 Code Agent 的子任务会改代码、跑命令、读上下文,它们之间可能存在隐性的文件依赖和资源竞争。

AgentTeams 的做法是把并行过程收敛成三个阶段:

  1. 规划阶段:TeamFanout 根据总目标做任务分解,生成一组带依赖约束的子任务。
  2. 执行阶段:多个子代理并发执行,每个子代理只看到自己相关的代码片段和工具权限。
  3. 汇合阶段:TeamCollect 把各子代理的产物按统一协议收拢,处理冲突,生成最终结果。

这个模型看起来简单,但每个阶段都有值得深挖的工程细节。下面我按组件拆开讲,先讲 TeamFanout 的设计,再讲 TeamCollect 的归并策略。

2. TeamFanout:子任务划分、上下文预算与扇出边界

TeamFanout 要做的事,绝不只是在代码里写个for循环然后丢给几个 worker。拆得不好,并行比串行还慢;拆得太细,Collect 阶段会被碎片化结果淹没。我自己的经验是,扇出过程必须先做“分诊”,再做拆分,最后用上下文预算约束每个子任务的范围。

2.1 先做“分诊”,再决定哪些任务不该扇出

不是所有代码任务都适合并行。我在实际项目里总结出三条筛选规则,凡是触犯其中一条的,宁可留在主链路里串行执行。

第一类是强依赖任务。比如“先改数据库迁移脚本,再改 ORM 模型,再改业务代码”,这三步天然存在前后顺序。虽然可以拆成三个 agent,但每个 agent 都必须等上一步的结果才能开工,拆了反而增加调度开销。第二类是小任务。一个只需要改两三行配置的活,单独起一个代理,光模型初始化、上下文加载、工具预热的时间就比手工改还长。第三类是全貌决策任务。比如“优化整条链路的性能”这种目标,必须有人先看全局,得出方向性结论之后,才有条件往下拆。

判断完这三类之后,剩下的任务才进入 TeamFanout 的候选池。实际操作中,我一般会用一个规划器代理来做分诊,它不是写代码的,只负责读代码库结构、列依赖、给任务打标签。规划器输出的是一份结构化拆分结果,大概是这个样子:

{ "goal": "为订单服务增加导出功能", "fanout_tasks": [ { "task_id": "task-001", "objective": "新增导出接口定义与参数校验", "depends_on": [], "context_files": ["api/order_routes.py"], "output_format": "code_diff" }, { "task_id": "task-002", "objective": "实现导出任务后台异步处理逻辑", "depends_on": ["task-001"], "context_files": ["services/order_export.py"], "output_format": "code_diff" }, { "task_id": "task-003", "objective": "补充订单导出的集成测试用例", "depends_on": ["task-001", "task-002"], "context_files": ["tests/test_order_export.py"], "output_format": "code_diff" } ] }

注意depends_on字段,这是并行机制的核心。真正能立刻扇出的只有task-001,另外两个必须等它完成。所以 TeamFanout 不是一个“一把梭”的分发器,它内部其实维护着一张有向无环图,每完成一个节点,就解锁一批新节点投入执行。

2.2 子代理的上下文预算与工具清单

Code Agent 的并行和普通服务端并发最大的区别,在于每个子代理都要占用上下文窗口。假设模型上下文是 128k,主协调器自己已经用了 20k,那剩下的 108k 不可能全部分给子代理,因为 TeamCollect 阶段还要把子代理的产物放回来汇总,这部分也要占上下文。

我给 TeamFanout 定了一条很死板的规则:每个子任务的输入上下文预算,不能超过模型窗口的 20%。128k 的窗口,单子任务最多给 26k 左右。这个预算要覆盖任务描述、相关代码片段、历史上下文摘要三部分。如果某个子任务需要的代码面明显超出预算,说明任务拆得太大,需要继续往下拆,而不是硬塞。

工具权限也得跟着任务走。一个只负责“补充测试用例”的子代理,不需要给它数据库写入权限,也不需要它能修改生产配置文件。经验之谈,工具暴露面越大,子代理跑偏的概率越高。我见过一个负责文档生成的 agent,因为工具包里带着代码搜索能力,结果自己绕进去查了半天根本无关的历史代码。TeamFanout 在扇出时给每个子代理配一份白名单工具列表,是控制这种漂移最直接的手段。

2.3 扇出上限不是越大越好

现在主流大模型的推理服务都有吞吐限制,扇出的子任务太多,会在 token 层面互相排队。我一般按两个维度掐上限:

  • 并发 worker 数:默认不超过 4,除非代码库结构非常清晰,我可以放宽到 8。
  • 同时写入的代码文件冲突密度:如果多个子任务会改到同一个模块,扇出数量就要压低。

一条经验公式可以参考:max_workers = min(模型服务并发上限 - 2, 待修改文件数 / 2)。比如这次任务涉及 8 个文件,模型服务并发上限是 10,那取min(8, 4),并发设 4 就够。设太高没有意义,文件锁一卡,后面全在等。

3. TeamCollect:结果汇合时的排序、冲突裁决与部分失败语义

Fanout 把任务拆出去了,真正考验工程能力的是 Collect。扇出阶段出了问题,最多是任务执行慢一点;Collect 阶段出了问题,那就是把错误结果合入代码库,后面要花几倍时间回滚。所以我把 TeamCollect 看得比 TeamFanout 更重。

3.1 让每个子代理都按统一协议交作业

没有统一协议的多代理协作,本质上是灾难。子代理 A 返回一个 diff,子代理 B 返回一段 Markdown 说明,子代理 C 直接回了一段修正后的完整文件,到了 Collect 阶段,你根本没办法律化地合并它们。

AgentTeams 的解法是给每个子任务绑定一个output_format,子代理必须按这个 schema 返回结果。我常用的协议大概长这样:

{ "task_id": "task-002", "status": "success", "summary": "实现了导出任务的异步处理,核心逻辑在 order_export.py", "changed_files": [ { "path": "services/order_export.py", "change_type": "modified", "language": "python" } ], "code_diff": "diff --git a/services/order_export.py ...", "test_results": [ { "name": "test_export_csv", "passed": true, "duration_ms": 1280 } ], "risks": ["导出大文件时内存占用偏高,建议后续接入流式写入"] }

这里最关键的不是 status,而是code_diff必须基于最新的主干代码生成。Collect 阶段做合并时,如果发现某个子代理的 diff 基线已经过期,必须立刻标记冲突,而不是强行应用。

我一直强调结构化输出,是因为自由文本在 Collect 阶段不可信。模型生成的 Markdown 说明写得多漂亮都没用,代码库里最终生效的必须是可以机械应用、可以机械回滚的 diff 格式。收起你对自然语言的好感,这里是工程领域。

3.2 同文件冲突的两种判定:行号级与语义级

并行改代码,最头疼的就是多个子代理改到同一个文件。TeamCollect 要做冲突检测,我总结了两个层级。

第一层是行号级冲突。两个 diff 都修改order_export.py,一个改了第 30 行,一个改了第 88 行,行号不重叠,理论上可以直接合并。但如果一个 diff 在文件头部新增了导入语句,导致行号整体偏移,另一个 diff 基于旧行号生成的修改就会错位。收集器在合并前先重算行号,是最基本的健壮性要求。

第二层是语义级冲突。两个 diff 改了不同行,但一个定义了一个函数build_export_task(),另一个在另一个文件里也定义了同名函数,合并后出现重复定义。这种问题行号检测不出来,只能靠符号表交叉检查。我的做法是在 Collect 阶段维护一个“本次会话修改符号表”,每个 diff 应用前先检查它引入的符号是否与已有的符号表冲突。

这里没有银弹,语义冲突检测本身就是一条需要持续投入的工程线。我的最低建议是:至少做行号级检测,并在最终的合并评审阶段留给一个人或一个模型做整体检查,不要让 Collect 阶段完全无人值守。

3.3 失败不重来,Collect 的分区收编策略

并行任务里必然有失败的子任务。最简单的策略是“一个失败,全部重来”,这在多代理场景下成本高到你无法接受——其他子代理可能已经跑了十几分钟。

TeamCollect 的思路是分区收编:把已完成且通过冲突检测的产物先合入代码库,失败或超时的子任务单独标记出来,进入下一轮重试队列。重试时只需要重新派发失败的那几个任务,但要注意,新派发的任务需要基于“已经合入部分结果后的最新代码”来生成 diff,否则基线又不对了。

部分成功语义对用户感知也很重要。交付结果时,我会明确列出:

  • 哪些子任务成功合入
  • 哪些子任务失败或超时
  • 失败原因是什么(模型推理错误、工具超时、冲突无法自动解决)

这样即使整个任务没有完全跑完,前期的产出也不会白费。比起那种“全盘成功或全盘失败”的二进制结果,这种渐进式交付在真实开发流程里友好得多。

4. 从 Fanout 到 Collect 的链路追踪:出问题该查谁

并行机制上线之后,你最需要的是一个能回答“到底是谁搞坏了这次合并”的追踪系统。多代理一跑起来,日志是爆炸的,没有 trace 维度去关联,排障就像在垃圾场里找一根针。

4.1 用 trace_id 贯穿扇出与汇合

我习惯在 TeamFanout 生成任务时,就为整个请求生成一个trace_id,它贯穿规划、扇出、子任务执行、收集合并的完整链路。每个子任务在这个 trace 下再展开自己的task_idagent_id,所有日志、token 消耗、工具调用记录、耗时,都挂在这条链路上。

这样定位问题非常高效。如果 Collect 阶段报告冲突,我第一时间能查到是哪个 task 引入了符号冲突,它当时依赖的代码基线是哪一版,它执行过程中调用了哪些工具。有一次线上并发任务把同一个配置文件改乱了,我靠 trace 定位到两个子任务几乎同一秒加载了旧版配置,问题根源立刻清楚——不是模型写错了,是扇出时没有给它们都传递最新的配置基线。

4.2 三个关键指标:耗时、token、重试

链路追踪的数据最终要沉淀成指标,不沉淀指标的追踪系统,排一次障可以,长期优化没用。我重点看三个指标:

一是每个子任务的端到端耗时分布。如果多数子任务都在 1 分钟完成,有一个卡了 10 分钟,先看它是不是等工具返回等了太久。

二是token 消耗拆分。扇出阶段拆得越细,子任务描述和上下文准备阶段消耗的 token 越多;Collect 阶段收到的产物越大,归并消耗也越高。这两个数字记录后可以反推拆分粒度的合理性。

三是重试率。单个子任务的重试率超过 20%,基本可以判断要么任务描述含混不清,要么工具清单给得太宽导致它反复试错。

这些指标我一般按天聚合,观察趋势。重点不是某次任务跑得好不好,而是随着代码库演化,并行机制是否还在健康运转。

5. 一组可以直接参考的落地参数与调优经验

理论说完了,列一份可以“抄作业”的配置参考。这些参数是我在真实项目里验证过、并持续在用的默认值,不保证对所有场景最优,但作为起点很稳。

5.1 我给 AgentTeams 的默认配置

agent_teams: fanout: max_workers: 4 max_dependency_depth: 3 subtask_context_budget_ratio: 0.2 tool_whitelist_per_task: true collect: timeout_seconds: 120 conflict_detection: line_and_symbol_level partial_success_enabled: true max_retry_per_task: 2 require_code_diff: true tracing: enabled: true trace_header: x-agent-teams-trace-id collect_daily_snapshot: true

几个值得解释的地方:

max_dependency_depth: 3指的是子任务的依赖链不超过三层。超过三层的拆法,本质上说明规划器把串行任务硬拆成并行,徒增调度成本。

subtask_context_budget_ratio: 0.2前面提过,128k 窗口下给单个子任务 26k 左右输入预算。如果你的任务普遍需要更多上下文,先压任务粒度,而不是调这个比例。

require_code_diff: true是硬开关。任何任务,只要涉及代码修改,必须返回标准 diff。不接受“我修改了 xx 文件”这样的自然语言描述。

5.2 实测中的表现与调优笔记

我手上一个中型项目,代码库约 20 万行,涉及 30 多个模块。以前用单代理串行做一个跨模块功能,平均耗时 40 到 50 分钟,中途成功率不到六成。切到 AgentTeams 并行机制后,相同类型任务平均耗时可压到 15 分钟左右,一次交付成功率到了八成以上。不过第一版上线时也踩过坑。

第一次调优是扇出数量。我一开始把max_workers设到 8,想着并行度拉满,结果因为多个任务都依赖读同一个公共模块,模型服务端排队严重,整体耗时反而比 4 并发慢。降回 4 之后,单任务延迟高了一点点,但整体吞吐反而上去了。这不是模型变快了,是排队时间变短了。

第二次调优是Collect 的冲突检测。早期我只做行号级检测,结果合并出来的代码语法没问题、符号却重复定义,CI 阶段才爆出来。加了一层符号级检测之后,这类问题少了七八成。剩下的漏网之鱼,主要发生在跨文件引用场景,靠人工评审兜底。

5.3 真实接入后的三个坑

如果你准备照着这套机制落地,提前跟你说三个我踩过的坑。

第一个坑是子代理读到的代码版本不一致。扇出之前,规划器会把相关文件路径打包给子代理,但子代理实际执行时是从 git 分支读取代码。如果扇出过程中有别人推了代码,不同子代理可能看到不同版本。我现在统一用快照分支来隔离,扇出时从主干创建一个临时分支,Collect 成功后才合并回主干。

第二个坑是Collect 阶段的上下文超载。子代理返回的产物如果都塞给 Collect 的模型,上下文瞬间爆掉。我的处理方法是让 Collect 阶段不直接吃原始 diff,而是先让一个轻量模块把每个 diff 转成摘要和风险标签,真正给到模型做决策的是压缩后的信息,完整 diff 只在落盘时保留。

第三个坑是超时时间不能一刀切。不同任务的耗时差异极大,读代码的任务几秒就完,跑测试的任务可能要三五分钟。现在我会根据depends_on的层级和工具类型,给子任务单独计算超时时间。比如涉及执行测试用例的子任务,超时设置为基础值的 3 倍,避免收集器把还在正常运行的子任务误判为失败。

6. 收个尾:并行机制不是架构终点,是编排起点

把这套 Fanout 和 Collect 的机制跑通之后,我最大的一点体会是:并行机制的真正价值不是把任务变快,而是把“编排”这件事从隐式变成显式。单代理时代,任务拆解、上下文分配、结果合并都藏在模型的一次次推理里,出了问题说不清道不明。有了 TeamFanout 和 TeamCollect,这些环节全部变成了可以设计、可以观测、可以干预的工程组件。

现在再接到“能不能让多个 Agent 一起干活”的需求,我心里就有谱了:先做任务分诊,把强依赖和弱依赖分开;再定上下文预算和扇出上限;然后约定结构化输出协议,让 Collect 阶段可以机械合并;最后把链路追踪和指标踩好,不放过任何一次慢请求和冲突。

如果你正在做自己的多代理系统,我建议不要一上来就去追求复杂的协议和框架。先手工把一次任务拆成三个子任务,用最朴素的并发方式跑通,再把冲突检测和失败重试补上,就够了。等这个最小闭环稳定了,再去思考 AgentTeams 这类高阶机制也不迟。

最后分享一个小经验:并行任务的子代理,各自跑得再快也没用,瓶颈永远在汇合点。所以你的精力分配上,Collect 的归并策略、冲突解决和失败处理,值得投入比 Fanout 多一倍的注意力。我把太多精力浪费在让 Fanout 更快上,最后发现真正决定系统上限的,是那个默默收拢结果、解决冲突的 Collect。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询