多 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_type=subtask.type, input_schema=subtask.input_schema ) if not candidates: return build_failure("no_candidate_agent") # 按历史信任分排序,得分高的优先 candidates.sort(key=lambda a: trust_score(a.agent_id), reverse=True) last_error = None for attempt in range(min(3, len(candidates))): agent = candidates[attempt] try: return agent.invoke(subtask.payload, timeout=subtask.timeout_ms) except AgentCallError as e: last_error = e # 通知路由层把该 Agent 的 trust_score 短暂降权 registry.downgrade(agent.agent_id, reason=e.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 发现自己缺少某项信息,发消息给 B;B 处理不了这个请求,又转回给 A;A 认为问题更复杂了,再次派给 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 的接入从三天磨合缩短到了半天。
这个内容后续还可以继续扩展的方向是“自适应编排”——让编排层根据历史执行结果自动调整任务划分粒度,而不是每次由人手工配置。我目前只在一小类固定流程上做了粗糙的尝试,效果有好有坏,等数据沉淀够了再写一篇详细复盘。