资讯动态

从单体Agent到多Agent编排:Routa架构设计与实践记录

发布时间:2026/10/22 14:19:59 来源:尧图企业网站定制
做了一年多的单体 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_typetask.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 解耦。任何一个业务团队只要按这个格式提供一份 JSONRouta 不出一行新代码就能让 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与其说是一次工具重写不如说是我对“怎么让多个模型可靠地协作”这个问题的一次系统思考。单体像一张大桌子所有东西都堆在一起找起来迟早会乱编排像一个运转有度的车间每个工位做自己擅长的事用标准化的工单流转。后者前期搭建设施要费些功夫但跑顺之后扩建产能、接入新工具、换用新模型都变得比想象中轻松。如果你也在摸着石头过河希望这篇实践记录能帮你少踩几个我已经踩过的坑。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑