
多 Agent 编排这个词我在一年多前还不怎么当回事。当时的想法很朴素单体 Agent 用得好好的为什么要拆成一群 Agent 互相沟通直到我把原本跑在 AutoDev 里的开发助手拆成并行协作的几个小 Agent才发现问题的根源从来不是“模型不够聪明”而是单体架构把太多的判断、状态和工具调用塞进同一条上下文管道里试错成本越滚越高。这篇文章就聊聊我从 AutoDev 迁到 Routa 的整个过程以及多 Agent 编排在开放生态下的设计思路和踩坑记录。1. 为什么需要从单体 Agent 升级到多 Agent 编排一次迁移的起因1.1 单体 Agent 的两大天花板上下文污染与局部故障放大先说 AutoDev 阶段遇到的问题。AutoDev 本质上是一个单体开发助手用户提一个需求它自己理解、拆解、检索代码、调用工具、生成补丁、跑测试全部在一个 Agent 上下文里完成。听起来很省事但用一段时间后就会发现上下文被搅成一锅粥。一个典型的场景需求是“重构订单模块的接口并把单元测试补上”。单体 Agent 先读了一批业务代码接着查了测试框架用法然后调用静态分析工具中间还夹杂用户临时补充的一句话。等到最后生成补丁时真正有用的业务上下文可能只占整个上下文的 20%剩下的全是检索回来的噪声和被覆盖的旧方案。上下文一长模型注意力就开始发散生成结果要么答非所问要么把无关模块的代码一起改了。另一个问题是故障放大。只要 Agent 在某一个环节出错——比如调测试命令时目录错了——整条任务就中断了。而因为所有状态都在同一个上下文里想“只回退这一步、保留前面的成果”非常困难只能整条重跑。重跑又意味着同样的上下文污染再来一遍费 token 还费时间。1.2 开放生态带来的新变量能力不再是单一模型独有的真正促使我决定换架构的是工具生态的变化。过去一个 Agent 能调用的能力基本取决于开发者给它接了多少工具函数。而现在越来越多的外部服务把自身能力暴露成标准化的工具接口模型通过声明式的工具描述就能调用。这意味着一个 Agent 的能力边界正在从“内置函数列表”扩展到“整个生态里可发现、可订阅的服务”。这种开放环境下单体 Agent 的短板暴露得更明显它根本没有精力去维护那么多工具协议也没有机制让不同技能并行工作。一个 Agent 既当规划器、又当执行器、还要当工具适配器迟早会顾此失彼。这时我意识到真正该做的不是换一个更强的模型而是把 Agent 拆开让每个 Agent 专注于一个领域再通过编排层把结果拼起来。1.3 迁移的触发点一次失败的“大而全”需求演示直接把想法推到临界点的是一次内部演示。我让 AutoDev 完成“从 GitHub Issues 里读取 bug 描述定位代码文件生成修复方案再写一封给用户的邮件”这么一条链路。结果它花了大量上下文去切换三类工具邮件内容甚至把代码里的变量名带进去了整段演示效果非常尴尬。这时我开始认真调研多 Agent 编排方案最后选定了我们自己改造过的 Routa。Routa 本身不是一个开箱即用的助手而是一个编排内核它只管 Agent 的注册、路由、任务分发、状态汇总和可观测性具体的业务能力全部由注册进来的独立 Agent 提供。换到 Routa 之后上述那条链路被拆成四个步骤每个步骤由不同 Agent 负责整个流程肉眼可见地稳定下来。2. AutoDev 与 Routa单体助手与编排平台的定位差异2.1 AutoDev 的优点和它真正擅长的地方我不想把 AutoDev 说成一无是处。对于单任务、单领域、上下文可控的场景它依然效率很高。比如“给某个函数补充 JSDoc 注释”或者“把一段 Python 列表推导式改写成生成器”这种目标明确、工具调用少的任务单体 Agent 反而更快因为它不需要跨 Agent 通信没有路由开销。AutoDev 的内部结构其实类似于一个“强化版的 ReAct 循环”模型根据当前上下文决定是调用工具还是直接回答每次调用工具的结果都追加回上下文。对于短链路的任务这个模式足够稳定。它的缺点是架构上没有强制分工所有子目标都堆积在一个上下文里因此一旦任务复杂模型的自我管理负担就过重了。2.2 Routa 的定位编排层不做业务只做连接和调度Routa 的理念跟 AutoDev 完全不同。它最核心的原则是编排层负责“谁来做、何时做、怎么做失败兜底”但不负责“具体做什么”。所有具体能力都沉淀在注册进来的独立 Agent 中每个 Agent 对外提供标准化的能力声明包括输入输出格式、依赖的上下文、可调用的工具集合。我们内部的 Routa 实现里每个 Agent 都是一个独立服务甚至可以用不同的模型、不同的提示词模板。编排层只维护三张表Agent 注册表能力路由、任务队列状态机流转、观测日志链路追踪。这种设计带来的一个好处是我不需要因为换某个子任务的实现方案而重写整个流程。比如最开始代码检索用的是基于关键词的工具后面换成向量检索只替换了“代码检索 Agent”上层路由根本不用动。对比维度AutoDev 阶段Routa 阶段任务粒度整条链路交给一个 Agent拆成子任务交给多个 Agent上下文管理所有中间结果堆在一起每个 Agent 只拿自己需要的最小上下文故障影响任意步骤出错整条链路重跑单 Agent 失败可重试或替换链路不中断能力扩展改代码加工具函数新写一个 Agent 注册到路由表即可故障定位靠翻完整对话日志靠任务链路追踪按节点定位2.3 演进路径不是非此即彼而是结构上的换脑回头看从 AutoDev 到 Routa 不是“把单体助手丢进垃圾桶”而是把单体中的能力剥离开。那些稳定、单一、高频的功能依然适合做成独立的微 Agent而那些需要全局统筹、跨任务决策的工作则上移到编排层。演进本身是结构性的从“一个 Agent 做所有事”变成“一个编排器协调多个 Agent 做各自擅长的事”。这个转变对工程团队来说尤其明显。原来让 AutoDev 跑一个复杂需求本质上是让算法在一个大上下文里碰运气而 Routa 的模型则类似组织架构设计——先把职责划分清楚再把不确定的路径路由到具体的执行者。对于追求稳定、可预期、可运维的团队后者的吸引力要大得多。3. 多 Agent 编排的核心模型任务分解、路由与协作方式3.1 任务分解的三个层次目标、子任务与动作多 Agent 编排里最容易被低估的是任务分解。我见过不少失败案例就是把一个复杂需求原样扔给编排器期望它自动分给合适的 Agent结果路由质量奇差。原因是缺了中间的“子任务”层次。我们内部约定三层结构。第一层是目标也就是用户最终要的结果比如“修复订单接口超时问题”。第二层是子任务这是编排器真正路由的基本单位例如“定位耗时逻辑”“生成修复补丁”“补充回归测试”“更新接口文档”。第三层是动作也就是每个子任务在 Agent 内部具体执行的操作比如读取文件、调用分析工具、执行测试。任务分解的关键在于子任务必须足够小小到单个 Agent 只靠自己的领域上下文就能完成同时子任务之间的依赖关系要显式建模而不是靠 Agent 之间的自由聊天去隐式协调。3.2 路由策略基于能力声明还是基于历史经验Routa 的路由层有两类路由策略我们在实践里都会用到。第一类是基于能力声明的路由每个 Agent 注册时声明自己“能做什么、接受什么输入、产出什么格式”路由层根据子任务的描述和输入 schema 做匹配。这种方式简单、可解释、便于排查适合首次接入的新 Agent。第二类是基于历史经验的统计路由当所有子任务跑完以后路由层会记录每个 Agent 在不同类型任务上的成功率、耗时和 token 消耗。再遇到相似任务时路由权重会向历史表现好的 Agent 倾斜。我倾向于让这两类策略叠加先靠能力声明保证功能正确再用统计信息做优先级排序。实践中效果最好的是把历史统计数据回传成一种“信任分”同时配合超时和熔断防止某个 Agent 偶尔抽风拖垮整体。3.3 三种协作模式流水线、共享黑板与事件驱动多 Agent 之间的协作模式我总结下来有三种用得最多。流水线模式适用于步骤之间依赖强、顺序明确的场景。比如“代码分析 Agent 产出评估报告评审 Agent 基于报告做判断最后执行 Agent 按判断行动”。信息传递是单向的像生产线一样每一站只消耗上一站的产物非常容易理解。共享黑板模式适用于多个 Agent 要共同围绕一个不完整问题挖信息的场景。比如做故障排查时检索 Agent、日志分析 Agent、代码静态检查 Agent 并行跑把自己发现的现象写到一块共享内存里最后由汇总 Agent 从黑板里挑出关键线索。这种模式的最大优点是可以利用并行性大幅压缩耗时但要注意黑板内容的格式冲突和值覆盖问题。我们后来给黑板里的每条信息都加了来源标签和时间戳才算把冲突控制住。事件驱动模式则更像真实世界的协同Agent 不直接调用对方而是发布事件对事件感兴趣的 Agent 订阅并响应。这种模式解耦最彻底但难点在于你很难保证“某个事件一定被正确处理”容易缺一环。目前我在生产环境用得最谨慎的就是这种模式除非链路里有明确的超时检查和补偿机制。3.4 失败处理给每个 Agent 留一条退路多 Agent 编排的可靠性从来不靠“Agent 永远正确”而靠失败时有什么机制兜底。我在 Routa 里建了三个层级的兜底。第一级是单 Agent 内部重试针对的是瞬时错误比如工具调用超时可以在同一个上下文里重新调用一次最多三次。第二级是替换执行如果某个 Agent 连续失败编排器把该子任务根据能力声明重新路由给另一个具备同类能力的 Agent。第三级是降级返回如果所有候选 Agent 都失败就把这个子任务标记成“未完成”并带着部分结果返回而不是让整条链路失败。这个机制帮我解决了一个特别实际的问题混合 Agent 模型时各模型能力参差不齐某些 Agent 可能对特定类型的输入特别敏感。有了替换执行和降级返回至少能保证整个流程不会因为单个 Agent 的薄弱而完全中断。4. 实操落地在开放生态中搭建一套多 Agent 编排系统4.1 基础设施选型与三个硬性约束真正动手搭 Routa 这套系统时我先定了三个硬性约束后续所有设计决策都围绕它们展开第一Agent 之间只通过结构化消息通信不允许自由文本闲聊第二编排层必须能记录每个子任务的完整输入输出和耗时便于审计和复盘第三新增一个 Agent 不能让编排层改代码必须是注册制。在这三个约束下基础设施选型就比较清晰了。任务队列选的是支持延迟消息和重试的通用消息队列Agent 服务则用轻量级 HTTP 框架独立部署。选型我的建议是不要贪图大而全的编排框架真正核心的是把“任务状态机”和“路由规则”做好这两块才是编排器的灵魂。状态机用来管理子任务从 pending、running、retrying 到 succeeded、failed 的流转规则引擎用来映射“子任务类型 输入特征 → Agent 候选列表”。4.2 Agent 注册与能力声明的最小 schema每个 Agent 注册时都要提交一份最小能力声明。这份声明的格式我做得很克制只保留五个字段服务标识、可处理的任务类型、输入输出 schema、依赖的工具集、以及一个默认的超时阈值。为什么不能搞得太复杂因为能力声明一旦复杂Agent 开发者就懒得写最后注册表会变成一堆过期的废数据。下面是一个简化版的注册声明示例实际中我会把 schema 字段用更严格的类型定义约束# agent_manifest.json { agent_id: code_locator, task_types: [locate_code, find_callers], input_schema: {target_symbol: string}, output_schema: {file_path: string, line_number: int}, tool_dependencies: [code_index, git_history], default_timeout_ms: 30000 }注册声明收了以后编排层会启动一个健康检查流程用一组固定样例任务去试探 Agent 的输出是否符合 schema。只有通过健康检查的 Agent 才会被加入有效路由表这样能避免“注册了但实际不能用”的死 Agent 占用路由名额。4.3 路由与调度器的实现要点路由器的核心逻辑不复杂但要写好却很考验细节。我贴一段核心调度逻辑的伪代码这里的重点是“候选排序”和“重试预算”的处理def dispatch_route(subtask, registry, task_ctx): candidates registry.match( task_typesubtask.type, input_schemasubtask.input_schema ) if not candidates: return build_failure(no_candidate_agent) # 按历史信任分排序得分高的优先 candidates.sort(keylambda a: trust_score(a.agent_id), reverseTrue) last_error None for attempt in range(min(3, len(candidates))): agent candidates[attempt] try: return agent.invoke(subtask.payload, timeoutsubtask.timeout_ms) except AgentCallError as e: last_error e # 通知路由层把该 Agent 的 trust_score 短暂降权 registry.downgrade(agent.agent_id, reasone.kind) return build_failure(last_error)这段代码每次调度时都重新计算候选顺序而不是缓存静态顺序。原因是 Agent 的表现会波动同一个 Agent 在某个时间段可能因为依赖的外部工具抖动而频繁超时如果一直缓存一份静态优先级就无法动态规避。实践下来动态降权对这个系统的整体成功率贡献最大。4.4 可观测性别等出问题才去翻日志多 Agent 编排系统里最怕的就是“黑盒”。一个任务经过七八个 Agent 转手只要其中一个环节吞了消息或者返回了残缺结果排查起来非常费劲。因此从第一天开始我就要求每条链路都打上 trace_id跟着子任务流转到每个 Agent最终落到统一的历程面板。观察面板上我只关注四个核心指标子任务成功率、平均链路耗时、token 总消耗和路由命中率也就是有多少任务成功匹配到了候选 Agent。其中路由命中率这个指标特别容易被人忽略但它其实最早预示问题——如果一段时间内命中率下降大概率是某个 Agent 下线了或者能力声明与被调用的任务类型出现了偏差得赶紧去看注册表。我后来还加了一条告警规则路由命中率低于 90% 直接通知值班程序比看错误日志高效很多。5. 常见问题与排查技巧实录被问得最多的四个坑5.1 Agent 之间死循环谁来踩刹车第一次上线 Routa 时我遇到的最严重问题是两个 Agent 互相调用停不下来。A 发现自己缺少某项信息发消息给 BB 处理不了这个请求又转回给 AA 认为问题更复杂了再次派给 B……要不是任务队列里有条数上限这个循环能把整个集群拖垮。后来我做了两件事一是给每条子任务都加深度计数器超过五层直接截断并返回告警二是要求消息必须携带原始 trace_id回环发生时能清楚看到是哪两个 Agent 在互踢皮球。排查死循环的时候建议先别一头扎进代码里先在观测面板上按 trace_id 拉出完整转发链确认循环的“入口 Agent”是谁然后在它的能力声明里限制它能转发的任务类型通常就能解开。5.2 上下文漂移子任务执行到后面忘了最初目标拆成多 Agent 之后我天真地以为上下文问题彻底消失了。结果发现它换了个姿势出现每个子任务内部依然有上下文如果 Agent 在内部执行了过多动作同样会出现“越干越跑偏”。最典型的是生成修复补丁的 Agent分析师喂给它的报告中提到了三个候选根因它会不自觉地把三个都修了产生一大批无关 diff。我的解法是强制在子任务的输入 schema 中带一个“目标约束”字段并在 Agent 的提示词开头把该字段置顶要求任何决策都必须对照目标约束。这不是什么高深的技巧但非常管用。信息论里有个简单的道理约束越明确生成结果的方差越低。多 Agent 编排里的“编排”本质就是在不停地给下游强加约束防止信息在流转中变形。5.3 token 预算失控多 Agent 不一定省 token很多人拆多 Agent 是为了省 token实际上你不做控制的话多 Agent 反而更烧钱。因为任务拆开后每个子任务都要重新描述背景和目标公共信息的重复传输会吃掉大量上下文窗口。我第一次全量切换到 Routa 时同一条开发任务的 token 消耗比 AutoDev 还高了 30% 左右。控制 token 的核心思路是共享上下文缓存。每个子任务只允许引用公共背景池里的指定片段不允许复制大段正文。比如代码检索 Agent 返回的定位结论只保留“文件路径 行号 一句原因描述”完整代码片段只有在执行 Agent 真正需要时才拉取。另外在路由层给不同类型的 Agent 设定独立的 token 预算上限超出就从降级返回而不是继续生成。5.4 编排器成了性能瓶颈高并发下的串行噩梦第一版 Routa 的路由器是单进程串行处理的结果在并发任务一多的时候路由响应从几毫秒涨到几百毫秒整个人工链路被拉慢用户体感非常明显。实际上瓶颈不在路由算法而在调度器每次匹配时都会做一次实时健康检查这种同步调用在高并发下把资源占满了。修这个问题的思路是异步化和缓存化。健康检查结果每 30 秒刷新一次缓存路由匹配只用缓存数据任务的调度从同步调用改成提交到队列后异步返回状态机结果。这样一次路由决策不依赖任何远程调用吞吐量的上限一下就上来了。这里也顺便提醒一句很多系统性能问题并不是出在“核心算法”上而是出在“核心算法里夹带的同步 IO 操作”。6. 几次迁移实践后我对编排架构的几点真实体会我个人在实际操作中的体会是多 Agent 编排这件事最难得不是技术实现而是“克制”。你每加一个 Agent就等于给整个系统引入一个新的不确定源每加一种协作模式就等于给排查问题新增一种复杂维度。所以我在 Routa 里的默认原则是能用三个 Agent 解决的绝不用五个能用流水线解决的绝不上黑板。再分享一个我后来坚持的小技巧每次新接一个 Agent 时强制要求它附带两个高质量的历史样本作为路由参考。这两个样本会在信任分尚未积累的冷启动阶段发挥作用让路由层不至于完全瞎猜。就是这个细节让新 Agent 的接入从三天磨合缩短到了半天。这个内容后续还可以继续扩展的方向是“自适应编排”——让编排层根据历史执行结果自动调整任务划分粒度而不是每次由人手工配置。我目前只在一小类固定流程上做了粗糙的尝试效果有好有坏等数据沉淀够了再写一篇详细复盘。