做了一年多的单体 Agent 工具链,也就是我们内部代号叫 AutoDev 的那套东西,我一度觉得它已经够用了:一个大的 Agent 实例,背上几十个工具,读完仓库上下文,按指令干活。直到有一天它在一段跨模块任务上连续翻车——先是工具调用列表互相干扰,模型把“查询用户权限”的工具误当成了“写入配置”的工具,接着上下文窗口被十几个中间结果塞爆,生成的代码张冠李戴。那次故障让我彻底想通了一个事:一个脑子管太多事,早晚要出问题。也是从那时候起,我开始动手做 Routa——一个把任务切给多个专职 Agent、靠事件协作、对模型和工具都保持开放的编排引擎。这篇文章就是我从 AutoDev 一路走到 Routa 的完整记录,包括核心设计、迁移步骤、以及我在实测中被现实教育过的几个坑,希望对正在做多 Agent 编排、或者已经被单体 Agent 气到过的团队有点参考价值。
1. 单体 AutoDev 的瓶颈,出在“一个脑子管太多事”
1.1 工具清单膨胀,上下文稀释到失真
AutoDev 单体方案最直观的问题,是工具描述本身就在吃上下文。我做过一个统计:单个 Agent 挂了 38 个工具,每个工具按规范写 description 和参数 schema,平均要占 200 个 token,光工具描述就接近 8000 token。再算上 system prompt、历史消息、检索回来的代码片段,一轮任务还没开始干正事,窗口已经见底。
后果很直接:模型在工具选择上的准确率肉眼可见地下降。不是模型变笨了,而是它在一个超长的列表里找正确工具,本身就接近“大海捞针”。有一次它成功把git_commit调用成了git_push,还有一次它把枚举参数里的pending错误传成了pendding,这类低级错误在工具少的时候几乎没有出现过。
比工具误选更隐蔽的,是上下文稀释。单体模式下,所有任务的历史都挤在同一个上下文里。如果你同时让它重构一个后端服务、写一个前端组件、再跑一遍测试,这三个任务的状态就会互相污染。模型经常会在这个任务的末尾,突然去处理另一个任务的残留指令。我在日志里看到过一段非常经典的混乱输出:代码生成到一半,模型开始解释数据库表结构,还附了一句“上一步已经执行了迁移”。实际上迁移那步它根本没做。
这种“一个脑子管所有事”的架构,在任务单一、工具不超过二十个时是高效的。但一旦工具数量上来、任务变复杂,效率就会断崖式下跌,而且问题不是换个更强的大模型就能解决的,它更像结构性问题:单体 Agent 的注意力是有限的,而你强迫它同时关注太多东西。
1.2 多步调用链的脆弱性,一步错步步错
AutoDev 模式下比较典型的失败场景是一个跨步骤的调用链。比如“扫描代码、定位 bug、修复、跑测试、提交”,这五个步骤在一个 Agent 内部串行执行,每一步的结果都直接拼接到历史里,作为下一步的依据。
这个设计的链子很长,任何一个环节出幻觉,后续所有环节都会跟着错。我有一次让它修一个并发问题,它第一步扫描时把项目里的“锁的粒度”理解错了,第二步定位直接找错了对象,第三步的修复方案自然就歪了,第四步测试因为方案错而失败,它没有回头反思,反而认为是测试写错了,开始“修”测试用例。整个过程看起来每一步都在干活,实际上是沿着错误方向一路狂奔,而且因为上下文里塞满了错误中间结果,纠正的成本极高,几乎等于推倒重来。
我还注意到一个现象:一旦 Agent 意识到自己前面的结论有问题,它倾向于“修补叙述”,而不是“纠错”。它会在后续总结里淡化前面步骤的问题,甚至编造一个能自圆其说的故事。这种情况下,单体 Agent 的日志看起来是完整的,但内容是失真的,排查起来非常痛苦。
这让我意识到,多步骤任务需要一个更结构化的机制:每步独立、可校验、可回滚,而不是靠上下文自然地“接力”。这就是 Routa 编排模型出现的直接原因。
2. Routa 的编排内核:先切任务,再连消息
2.1 角色划分:编排器、规划器、执行器与审查器
Routa 的核心设计理念很简单:把原来一个 Agent 的“全知视角”,拆成一组有明确分工、有独立上下文的角色。我最早只设计了四个角色,后来根据实践加了两个,目前稳定在六个:
| 角色 | 职责 | 说明 |
|---|---|---|
| Orchestrator | 接收用户请求,拆任务,下发、回收、裁决 | 不直接写代码,只负责调度 |
| Planner | 把任务展开成任务图(DAG),标注依赖关系 | 在复杂任务上格外有价值 |
| Executor Worker | 执行具体单元任务,调用工具 | 一个 Worker 只配一组相关工具 |
| Reviewer | 对 Worker 产物做静态检查、测试校验、接口对齐 | 在合并之前拦截问题 |
| Tool Agent | 连接外部系统,做数据库、CI、消息等操作 | 从 Worker 中剥离出来,统一收口 |
| Human Gateway | 人工审批通道,权限敏感操作必须走这里 | 关键安全阀门 |
一开始我犯过一个小错误:把 Planner 和 Orchestrator 合并成一个角色。结果它一边拆任务一边调度,逻辑混乱,任务拆得特别粗糙——经常把一个完整的“改日志级别”的任务拆成四个子任务。后来我强行分开了它们:Orchestrator 只做“拆”和“收”,Planner 只做“策略设计”,各管一段,问题明显好转。
这里有个设计原则值得分享:角色不是越多越好,每个角色都要有独立存在的理由,要么是上下文隔离需要,要么是安全边界需要,要么是校验职责需要。如果一个角色只是“把代码从 A 文件挪到 B 文件”,它就不该存在,直接让 Worker 做就行。
2.2 消息协议与任务生命周期
角色之间不共享内存,不共享上下文,它们之间只有一种交互方式:事件消息。这是 Routa 和 AutoDev 最大的架构差异。AutoDev 靠“同一个人记住整件事”,Routa 靠“一群人交换标准格式的纸条”。
我们定义了一套最小化的事件协议,核心字段包括:
event_id:全局唯一事件 IDtask_id/correlation_id:关联同一次请求的所有任务event_type:task.assigned、task.started、artifact.produced、task.completed、task.failed、approval.requested、context.queryproducer/consumer:来源角色和目标角色payload:不超过一定大小的结构化数据,超出部分落库,只传引用
任务生命周期也做了严格定义,状态机是:
created → scheduled → running → completed,以及running → awaiting_approval → running,失败则进failed,可重试则回scheduled,人工取消则进canceled。
为什么要搞得这么正式?因为我吃过多 Agent 协作不定义协议的亏。早期版本里 Worker 之间直接互相调 API,结果 A 等 B 的回调,B 又等 A 的结果,形成死锁,排查到后面只能靠重启。改成事件驱动加状态机之后,任何一个任务卡住,定位到状态就完了:要么是running太久,要么是awaiting_approval没人批,秩序感完全不同。
2.3 上下文策略:按需注入,而不是全量共享
上下文管理是这轮改造里最值得说的一块。Routa 的指导方针是“四不传”:
- 不传全量历史:Worker 之间只传必要输入,不传对方的聊天记录
- 不传无关工具:我们做了一个工具路由表,每个任务只注入关联度高的几个工具描述
- 不传原始代码大块:超过阈值就存到工作记忆库,传引用
- 不传未收敛结论:Planner 的早期草案不进执行上下文,只有定稿任务图才下发
对应地,Routa 引入了两个辅助设施。一个是工作记忆(Working Memory),本质是一个带向量索引的知识库,每个执行的中间产物、总结、关键决策都会异步写入,Worker 需要时可以“查询”而不是“等待上一棒传递”。另一个是检查点(Checkpoint)机制,每隔一段任务进度,把当前状态落盘。一旦某个 Worker 崩溃或者产出被判无效,可以直接从最近的检查点重建上下文,而不是重跑整个链路。
我最初怀疑这套设计会引入额外的延迟,但实际跑下来发现:检索一次工作记忆的延迟,远小于把一个大上下文全量传给下一个 Worker 的 token 消耗和时间开销。尤其在工具多、代码量大的场景里,按需注入这套思路几乎是必须的。
3. 开放生态接入:工具注册表与模型无关的适配层
3.1 统一工具描述协议:任何工具,一层 Schema 接入
Routa 的“开放”首先体现在接入形式上。我们不想为每个新工具写定制代码,而是定义了一个统一工具注册协议,所有工具只要按这个 Schema 描述自己,就能被编排体系使用:
{ "tool_name": "query_order_status", "version": "1.2.0", "description": "根据订单号查询订单当前状态,只读操作,无副作用", "category": "order_system", "input_schema": { "type": "object", "properties": { "order_id": {"type": "string", "pattern": "^ORD_[0-9]{10}$"}, "env": {"type": "string", "enum": ["staging", "production"]} }, "required": ["order_id", "env"] }, "output_schema": { "type": "object", "properties": { "status": {"type": "string", "enum": ["pending", "paid", "shipped", "canceled"]}, "updated_at": {"type": "string"} } }, "execution_endpoint": "internal://order_system/query_status", "execution_mode": "sandbox", "auth_scope": "read_only", "timeout_seconds": 10, "rate_limit": 50 }这套协议看起来简单,真正落地时才感受到它的威力。模型侧不需要知道工具的实现语言,执行侧不需要关心模型怎么理解工具,两边通过这个 Schema 解耦。任何一个业务团队,只要按这个格式提供一份 JSON,Routa 不出一行新代码就能让 Agent 群里所有角色用上这个工具。
我还给注册表加了一个自动“工具相关性索引”——用工具名和 description 的向量,跟当前任务的语义做匹配。比如任务是“确认用户订单是否已发货”,工具路由模块就会把query_order_status、get_user_info这一类工具列为高相关,优先注入执行 Worker 的上下文。这样做以后,单个 Worker 上下文里的工具描述从几十个降到了五六个,模型选工具的准确率明显回升。
3.2 沙箱执行与权限收口:开放不等于裸奔
开放生态最大的担忧永远是安全。一个工具接入进来,模型什么能调、什么不能调、调用范围有多大,必须在注册表层面说清楚。
Routa 每个执行器都跑在独立沙箱进程里,遵循最小权限原则。对外部命令,我们做了三件事:
- 文件系统只读映射,执行器默认看不到宿主机真实目录,只有显式声明的目录会被映射
- 网络白名单,外部工具访问域名或内部接口需要注册时申报,未申报的一律拦截
- 超时与重定向,任何执行器最多运行 N 秒,输出大小限制在指定范围内,超过直接截断并记一条异常
权限这块我们还做了一个分级:read_only、read_write、requires_approval。像查订单这种只读操作,Worker 可以直接执行;像推送分支、删除表这种有副作用的操作,必须经过 Human Gateway 审批,审批界面会展示操作对象和影响范围,人工同意后任务才继续。
这套机制跑到现在,最大的收获不是“防住了多少攻击”,而是让业务方敢于把内部接口接进来。以前接一个工具要层层审批,现在只要 Schema 声明清楚、权限级别合规,几分钟就能接入,安全可控。
3.3 模型无关适配:不要让编排层绑死某个模型
多 Agent 编排不能绑死某一个模型,这是 Routa 能持续演进的另一个原因。不同模型在工具调用上的行为差异很大:有的模型严格遵循 JSON Schema,有的模型会略做修改;有的模型擅长长文本规划,有的模型短指令响应更好。
Routa 在模型层和编排层之间加了一个适配层,统一转换成内部“动作指令”。适配层负责几件事:
- 把模型的自然语言工具调用解析成标准工具调用,参数类型强校验,不匹配的尝试按 Schema 做最近匹配纠正
- 把多个模型提供商返回结果统一成标准
ActionResult,带上 token 消耗、耗时、置信度字段 - 处理不同模型对“拒绝任务”的表达差异,统一转换成
task.refused
有了这层适配后,我们可以针对不同角色选不同模型:Planner 用一个擅长推理思考的强模型,普通 Worker 用延迟更低的轻快模型,Reviewer 用一个专注代码审查的模型。整体成本比原来“一个大脑全包”下降了不少,效果反而更稳。
4. 从 AutoDev 迁移到 Routa 的三步实操清单
4.1 先横向切:一个调度器加多个专职执行者
如果你现在也在用单体 Agent,我的建议是不要一步到位搞成六角色大编排,那太激进,容易崩。我们当时的迁移分了三步,每步都是独立可验证的。
第一步是横向切分:把 AutoDev 按领域拆成多个 Worker Agent——代码生成、测试执行、数据库操作、Git 操作各一个。拆完以后保留一个简单的 Router Agent,根据用户请求的语义把任务分发给对应 Worker。这一步本质上还是“一个人派活给几个人干”,没有真正的规划环节,但效果已经立竿见影。
原因在于,每个 Worker 的工具清单从三十多个降到了十个以内,上下文干净了,错误率立刻下降。我统计过迁移第一步后的准确率,工具误选类错误几乎清零,这还不算 token 成本上的节省。
4.2 再纵向切:引入 Planner 和 Reviewer
第二步才是真正进入编排:加 Planner 做任务图拆解,加 Reviewer 做产物校验。从这一步开始,任务不再是一个 Action 到底,而是变成一个 DAG——有并行的地方并行,有依赖的按依赖串行。
Planner 的输入是用户请求加初始上下文,输出是一份结构化的任务图,包含每个子任务的角色、输入引用、预期产出、依赖关系。Reviewer 拿到 Worker 产物后,会做静态检查、编译验证、接口对齐检查,不合格直接打回并附上原因。这一步以后,质量问题不再靠“最后人工看一遍代码”,而是形成了执行与校验的闭环。
这里有一个坑提醒一下:Reviewer 不要设计成“全知全能的裁判”,它必须有明确的校验清单。早期我们把 Reviewer 设计得太开放,让它“全面检查产物质量”,结果它每次都能挑出一堆风格问题,把 Worker 逼疯。后来把校验清单收敛成接口对齐、类型正确、测试通过、无硬编码这四项核心指标,其他风格问题不阻塞合并,流程顺畅多了。
4.3 最后补可观测性:trace、回放与成本分析
第三步是配套工程。多 Agent 编排链路长了,如果没有可观测性,出了问题只能胡乱猜测。Routa 从上线第一天就要求每个事件必须带 trace 上下文,我后来又补了两样东西:回放机制和成本看板。
回放机制用于失败排查:某个任务失败了,系统把该任务从输入到输出的所有消息、决策、工具调用原始记录收集起来,按时间轴重新播放。排查问题的效率提升了非常多,以前在单体里是“靠日志和猜”,现在是可以精确地看到“Planner 在哪一步把约束丢了”或者“Worker 在哪个工具调用上返回了异常结果”。
成本看板则跟踪每个任务的 token 消耗,按任务类型、角色、模型来源聚合。我看了几周的数据后发现,Reviewer 的 token 消耗远超预期,因为它每次校验都重新读一遍代码。后来给它加了“变更 diff 优先”策略,只看改动部分,成本直接掉了一半。这种优化没有看板靠拍脑袋是拍不出来的。
4.4 迁移前后对比:到底改了什么
| 维度 | AutoDev 单体 | Routa 编排 |
|---|---|---|
| 工具承载量 | 30+ 工具全量入上下文 | 按任务注入,每个 Worker 5 个左右 |
| 上下文管理 | 全量历史共享 | 按需注入 + 工作记忆检索 |
| 任务执行模型 | 单链串行 | DAG 并行,依赖可控 |
| 失败恢复 | 靠重新生成 | 检查点重建 + 任务重试 |
| 质量保障 | 看模型心情 | Reviewer 闭环校验 |
| 可观测性 | 日志难还原决策过程 | 全链路 trace 回放 |
| 模型选型 | 一个模型全包 | 按角色选不同模型 |
这张表我给很多团队讲过,每次都要强调:迁移的价值不是“用了某种框架”,而是把不可控的“全知上下文”变成了可控的“任务流”。认知原理下的投资回报,比工具的换新重要得多。
5. 实测翻车现场:上下文漂移、过度编排与工具幻觉
5.1 上下文漂移:摘要没传,约束就丢了
多 Agent 编排最常见的一个坑,是我称之为“上下文漂移”的现象。它发生在任务链路较长时:Planner 把任务切了八个子任务,Worker A 做完,把关键信息压缩成摘要传给 Worker B。摘要当然是简洁的,但简洁意味着要丢弃细节,而模型偏偏可能把不该丢的约束丢掉了。
我们真踩过一次:Planner 拆了两个任务,一个是“按接口文档生成订单模块”,一个是“为订单模块写单元测试”,生产者和消费者之间通过摘要传递,摘要里有接口字段包含金额、状态、时间,但丢了金额必须是分单位整数这个关键约束。下游 Worker B 写测试时按“元单位小数”去断言,结果整体校验通过,部署上去才发现数据错位。
这让我总结出一个硬规矩:关键约束不能靠摘要传递,要落成结构化断言。我们在工作记忆里增加了一个“约束卡片”类型,Planner 在拆任务时如果识别到关键约束,会单独写成卡片存进工作记忆,下游 Worker 必须引用卡片 ID,执行完还要附带“已满足/未满足”的逐项检查。把约束从“自然语言描述”变成“结构化检查项”之后,这类问题基本绝迹。
5.2 过度编排:一个改注释的任务被拆成了五步
如果你把编排做得太细,会走到另一个极端:过度编排。有一次为了验证新架构,我强行让一个“修改 README 描述文字”的任务走了完整编排链路。Planner 拆了五个子任务、调度了三个角色、开了四次事件传递、模型跑了三轮,最后为了加一行字,消耗的 token 比原来单体模式翻了三倍,耗时增加了十几秒。
这个教训很深刻:编排是有固定开销的,必须让任务复杂度配得上编排复杂度。Routa 后来加了一个“复杂度评估器”:在 Orchestrator 收到请求后先做一个快速判断,如果是低复杂度任务,直接走“快路径”——用一个 Worker 一次性完成,不经过 Planner,不生成 DAG。只有当任务被认定确实需要多角色协同时,才会进入完整的编排链路。这个设计看似是“退步”,其实是让编排系统的适用范围更合理了。
5.3 工具调用幻觉:模型调了一个不存在的工具
开放生态接入后,工具数量会快速增加,模型又会出现一种新问题:明明注册表里没有这个工具,它却倾向于“认为”自己可以用。有一次模型直接请求调用send_email_to_admin,实际上注册表里只有send_notification。还有一次模型调search_code时,给一个枚举字段传了不存在的值。
这个问题靠模型本身很难彻底避免,我选择在适配层加一道“工具调用校验+修正”机制:先按注册 Schema 强校验,不合格的调用不会真正执行,而是生成一条修正建议返回给模型。枚举值错时会做最近匹配;工具名不存在时会返回“你手上有这些相关工具”的参考列表,引导模型选择正确的工具。这套拦截机制上线后,工具调用类异常下降了绝大部分。
我还把工具执行改为幂等优先的设计:执行器会为每个调用生成幂等键,重试不会产生重复副作用。这对于多 Agent 场景尤其重要,因为在重试机制下,同一个工具调用可能被执行多次,如果没有幂等保护,就会出现重复下单、重复发通知这类事故。
5.4 多 Agent 协作死锁:事件驱动的另一个隐藏坑
事件驱动虽然解决了单体上下文污染问题,但它引入了一个新问题:无界等待。早期版本里,如果 Worker A 发布了事件,没人消费,任务就挂在running状态一动不动。我们花了很大力气排查,发现是消费者注册顺序出了问题——新加的角色没有订阅正确的事件。
我不能确认这个问题是否普遍,但我的经验是:每个任务都必须有超时,每条事件都必须有死信路由。超时之后要么自动重试,要么标记失败并通知 Orchestrator;事件如果没有消费者,必须进入一个可检查的死信队列,而不是无声无息地丢在空气里。没有这两样东西,编排系统的“失控方式”比单体更隐蔽,也更难排查。
6. 写给也想动手的人:最后几条经验
多 Agent 编排听上去是个很“新”的实践,真正把它落到生产环境时,重复踩的坑其实非常集中。这几条是我的真实感受,分享给你作为参考。
第一,先想清楚你到底需不需要编排。如果团队任务大多是单刀直入型——改个配置、写个文档、生成一段固定模板代码,单体 Agent 或直接调 API 可能更快更省。编排的价值只有在复杂任务频繁出现时才体现出来,为了用技术而用技术,只会徒增维护成本。
第二,启动时从最小的切分开始。我第一次规划 Routa 时设计了八个角色,上线后有一半在“等活干”,实际跑起来根本没有那么多种任务类型。后来砍到六个角色才算稳定。角色的增减应该由真实任务分布决定,而不是架构师纸上拍板。
第三,可观测性是一等公民,不是事后补丁。如果一个 Agent 系统你无法回答“它为什么做了这个决定”,你就没法对它做任何可靠的上线。我会建议从第一天就把 trace、事件日志、成本记录都建好,多花半天配置,能省下未来好几天排查的功夫。
第四,保留人工审批的闸门。无论技术做得多成熟,涉及权限、付费、删数据的操作,都应该留一个 Human Gateway。它不只是安全机制,也是让业务方对这套体系建立信心的关键。看到风险操作需要确认,很多团队才敢把更多任务交出来。
从 AutoDev 到 Routa,与其说是一次工具重写,不如说是我对“怎么让多个模型可靠地协作”这个问题的一次系统思考。单体像一张大桌子,所有东西都堆在一起,找起来迟早会乱;编排像一个运转有度的车间,每个工位做自己擅长的事,用标准化的工单流转。后者前期搭建设施要费些功夫,但跑顺之后,扩建产能、接入新工具、换用新模型,都变得比想象中轻松。如果你也在摸着石头过河,希望这篇实践记录能帮你少踩几个我已经踩过的坑。