ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

动态路由与自适应编排:多Agent系统的关键设计

动态路由与自适应编排:多Agent系统的关键设计 Agent系列写到第9.3篇聊一个几乎所有多Agent项目都绕不开的硬骨头动态路由与自适应编排。很多刚接触Agent开发的朋友会把这两件事混为一谈其实定位完全不同——路由解决的是“这个任务该交给谁”编排解决的是“多个任务之间按什么顺序推进、状态如何延续”。在多Agent系统里这两者是连体婴只有路由没有编排任务会散成一地鸡毛只有编排没有路由调度逻辑会烂成一团意大利面。这篇先给结论动态路由解决的是“选人”问题自适应编排解决的是“干活顺序和容错”问题。如果你正在从单Agent往多Agent升级或者手里的Agent系统一加新场景就要改主流程代码那这篇文章就是为你准备的。我会先拆路由和编排各自的原理再给一套可以直接抄作业的落地方案最后把我们实际项目中踩过的坑一次性说清楚。1. 内容整体设计与思路拆解为什么路由和编排必须绑在一起设计先别急着写代码想清楚一个问题你的Agent系统到底需不需要动态路由我见过太多团队一开始只有两三个Agent主流程里写死if-else也能跑但随着Agent数量增加到十个以上问题就全冒出来了新Agent上线要改老代码某个Agent挂了整个链路崩掉同一个需求在不同数据源上的表现不稳定。这时候你才意识到“把任务分给谁”这件事必须从业务代码里抽出来变成一个独立的、可维护、可评估、可扩展的中间层。动态路由的定位就是任务分发层的“实时决策”它决定当用户请求进来时请求应该匹配到哪个或哪些Agent。而自适应编排的定位是任务执行层的“动态管线”它决定这些Agent之间的依赖关系、执行顺序、重试策略、失败降级路径以及需要时才临时调整的策略。我平时给团队讲这两个概念喜欢用一个“快递分拣中心”的类比。路由就像是分拣中心的扫描台每个包裹进来先扫码按目的地动态分拣到不同通道。注意是“动态”不是每个包裹都固定走同一条传送带——否则某个通道堵塞整条线就瘫了。编排则是传送带之间的调度规则哪个包裹必须先过安检、哪个包裹要合并装车、哪个包裹运输失败后自动进入退货通道。分拣台知道包裹去哪个通道调度器知道通道之间怎么配合——这两者没有先后关系是同一个系统的两个切面。在设计思路上我强烈建议遵循以下几点路由和编排的配置要外置不要写死在代码里。路由表、编排规则都应当是数据而不是代码逻辑。这样才能在不重新部署的情况下调整系统行为。路由决策要可评估也就是每次路由都应该能记录下“为什么选了Agent A而不是Agent B”无论是基于规则的匹配还是基于LLM的评分都要有迹可循。编排要面向失败设计而不是面向成功设计。真正让编排系统有价值的不是一条happy path跑通而是失败时能自动降级、绕过、重试甚至逆向修复。动态化范围有边界不是所有东西都要动态。能静态确认的依赖关系就静态化动态化只作用于“会变的部分”Agent数量会变、Agent能力会变、数据源分布会变、用户意图会变。这就是为什么这一篇要把路由和编排放在一起讲路由表的动态更新通常就是编排触发的编排策略的调整往往又依赖路由结果的反馈。两者分开设计你会在联调时发现大量逻辑断点。2. 动态路由把“找谁干活”这件事做成实时决策2.1 路由元数据先定义Agent的“简历”动态路由的第一件事不是写路由算法而是定义清楚每个Agent的元数据。没有一份结构化的“Agent简历”路由就是瞎猜。我建议的Agent元数据Schema至少包含这些字段{ agent_id: sql_exec_worker_01, name: SQL执行Agent, description: 负责连接MySQL/PG执行查询SQL返回结构化结果, version: 1.2.0, status: healthy, capabilities: [sql_execution, mysql, postgresql], limitations: [不支持DDL, 不支持ClickHouse], endpoint: http://agent-sql-exec:8080, weight: 80, max_concurrency: 5, strategy_tags: [data_plane, read_only], timeout_seconds: 30 }这还没完。生产环境里我还会额外维护一张扩展Profile表里面放着Agent的“性格参数”适合处理什么复杂度级别的任务、是否需要用户确认、对上下文长度的敏感度、运行时依赖哪些外部工具。这些字段在路由打分阶段非常有用。这里必须强调一个关键原则路由元数据本质上是路由决策的事实依据它必须是程序可读的而不是给人看个大概。capability标签的设计尤其重要要遵循“最小可判定粒度”——比如不要写一个很宽泛的“数据分析”而是拆成“sql_execution”“data_cleaning”“statistical_testing”“report_generation”。粒度太粗路由匹配时区分度不够粒度太细注册维护成本爆炸。2.2 路由算法规则兜底评分分层LLM补位路由算法是整个动态路由的心脏。我在不同项目里试过四套方案按复杂度从小到大排列完全规则匹配基于标签精确匹配。适合Agent数量少、职责边界清晰的场景。优点是零成本、可解释性强缺点是僵硬遇到没见过的输入直接失配。加权评分路由为每个Agent计算能力重合度、负载率、历史成功率、响应时延加权求和取最高分。这是性价比最高的方案生产环境里多数团队最后都会落到这层。规则LLM混合路由先用规则和评分筛出Top3候选再让LLM对候选做语义匹配判断。适合开放域任务比如“帮我分析一下这批用户的行为数据”这种模糊指令。完全LLM路由所有路由决策交给LLM。我不推荐成本和不确定性都太高而且出了问题很难排查。LLM应当只在“规则无法区分”的边缘场景介入。我用得最多的方案是“规则兜底 评分分层 LLM补位”。核心代码如下async def route_to_agent(task: Task, agent_registry: AgentRegistry): candidates [] for agent in agent_registry.list_alive_agents(): # 第一层能力匹配兜底没有重合的直接剔除 capability_hit calc_capability_overlap(task.required_capabilities, agent.capabilities) if capability_hit 0: continue # 第二层可解释性评分 能力重合度(0.5) 负载因子(0.2) 历史成功率(0.3) base_score capability_hit * 0.5 base_score (1 - agent.current_load / agent.max_concurrency) * 0.2 base_score agent.history_success_rate * 0.3 # 第三层LLM补位打分只对Top3候选执行 if agent.profile.get(llm_routing_enabled): candidates.append((agent.agent_id, base_score)) else: candidates.append((agent.agent_id, base_score)) if not candidates: return None candidates.sort(keylambda x: x[1], reverseTrue) top3 candidates[:3] llm_scores await llm_evaluate(top3, task) return pick_best(top3, llm_scores)注意这套算法的关键设计普通场景下评分就是最终决策LLM只在Top3内做“模糊仲裁”。这样做的好处是常用路径毫秒级完成路由冷门模糊指令才付出LLM调用的时间成本而且LLM的仲裁范围被限制在3个候选内不会出现“在50个Agent里大海捞针”的低效场景。关于权重参数我建议不要拍脑袋定用线上日志回放来调。每个Agent的调用结果、耗时、用户满意度都是label拿历史数据在离线环境跑一遍参数搜索比你手动调参快得多。我在一个项目里用这种方法把路由准确率从74%提到了91%。2.3 动态注册与健康检查路由表必须自己会“呼吸”静态路由表最大的问题不是不准而是没人维护的时候一定过期。某天新部署了一个Agent忘了往路由表里注册结果所有任务都打给老Agent老Agent负载飙升新Agent闲置吃灰。动态路由的高明之处是让Agent能“自注册”和“自注销”。我推荐用注册中心心跳机制的方案轻量点用Redis就能实现重量级场景用etcd或Nacos。Agent 启动 - 调用注册接口写入自身元数据到 route_registry:{agent_id} - 每10秒发送一次心跳刷新 TTL30 秒 - 收到注销信号后主动删除元数据 路由查询端 - 每次路由前扫描 route_registry:*剔除TTL过期的记录 - 对异常Agent执行熔断连续3次健康检查失败则降权这里有一个很多教程不会讲但极其重要的细节健康检查不能只检查Agent进程“活没活”还要检查Agent依赖的下游资源。比如一个SQL执行Agent自身进程活着但它依赖的数据库连接池满了这时候路由到它一样是失败。所以我给健康检查设计了“依赖就绪探针”每个Agent的健康状态由自身存活状态和关键依赖状态共同决定。新Agent上线后如何平滑加入路由我踩过的坑是新Agent刚注册历史成功率为0路由分数天然吃亏可能几天都接不到任务无法积累经验形成“冷启动死循环”。解决办法是给新Agent一个冷启动保护期在保护期内额外加一个“新鲜度加成”权重强制分配一定比例的小流量试跑等历史成功率稳定后再撤掉加成。还有灰度引流先让新Agent承接5%流量观察一周再逐步放大比例。3. 自适应编排所谓编排就是执行的前置思考3.1 状态机与工作流核心动态路由决定了“任务交给谁”自适应编排要回答的问题更复杂多个Agent之间如何配合失败后怎么走状态怎么延续我强调一个思维模式的转变不要把编排当成画工作流图要把它当成设计一个状态机。工作流图是静态的状态机是动态的——同一个任务在不同前置条件下路径可以完全不一样。我在工程落地中定义的核心状态比一般教科书上的要实用pending任务在队列里还没开始running正在执行某个Agentsucceeded当前步骤成功failed当前步骤失败retrying失败后进入重试等待skipped条件不满足跳过该步骤timeout超时未返回compensating进入逆操作/回滚自适应编排引擎的核心循环其实不复杂关键在于处理“条件跳转”和“策略调整”class AdaptiveWorkflowEngine: def __init__(self, context: dict): self.context context self.max_retries 3 self.total_timeout 120 async def run(self, steps: list[StepSpec]): start time.time() for step in steps: if step.skip_when and step.skip_when(self.context): logger.info(f跳过步骤 {step.name}原因: 条件不满足) continue result await self.execute_step(step, self.max_retries) self.context[last_result] result # 自适应调整每个步骤可以携带一个调整策略回调 if step.adjust_policy: step.adjust_policy(self.context) if time.time() - start self.total_timeout: raise WorkflowTimeoutError(任务总时长超限)这里最关键的抽象是StepSpec。它把编排逻辑拆成一个个可配置的步骤定义每个步骤具备名称、执行器、跳过条件、重试策略、超时时间、调整回调。自适应不是说代码里写满if-else而是把“条件”和“调整”变成可插拔的策略。3.2 上下文的动态维护编排的“工作记忆”自适应编排真正难的地方其实在数据层——每一步Agent的执行结果如何传递给下一步中间状态出了错怎么回滚我的做法是设置一个共享任务上下文这个上下文就是热搜词里说的“working memory”。它本质上是一个带冲突处理的分层KV存储任务级上下文整个任务的生命周期内共享包括用户原始输入、路由决策记录、中间结果、置信度评分。步骤级上下文仅当前步骤可见避免上一步的杂乱数据污染下一步的输入。外部知识Agent通过工具或MCP协议拉取的实时数据只读缓存不准直接改。共享上下文的写入规则要严格只有编排引擎能写入系统级变量Agent只能写入自己的命名空间。否则你会看到A Agent改了B Agent的中间变量整个任务状态乱成一锅粥。编排的任务状态持久化也很重要。任务执行到一半服务重启如果所有内存状态全部丢失那只能让用户重新发起。我用Redis做任务状态的持久化每个步骤完成就更新一次重启后从断点恢复而不是从头再来。这块的代价就是每个步骤多一次状态快照但换来的可用性提升完全值得。3.3 策略热更新让编排系统拥有“熵减”能力自适应编排区别于静态编排的一个核心能力是策略热更新——系统运行状态下编排策略能自动或半自动地调整。我给这个能力拆了两个层级第一层是运行时策略回调上文提到的adjust_policy就是干这个的。比如检测到SQL生成Agent连续两次产出低质量SQL校验不通过可以自动将第三步“直接执行”改为“先走SQL审查Agent再做二次改写”。这一步相当于系统在运行中自我调整工作流路径。第二层是配置中心联动把编排策略做成配置下发。一个典型的场景数据源Agent故障率上升运维人员在配置中心调整失败阈值参数不用重启编排引擎30秒内新策略生效。这里有个必须提的坑配置下发的“生效一致性”问题。某个步骤已处于running状态配置却已经更新是按旧规则跑完还是按新规则中断重来我的经验是默认不中断正在执行的步骤只影响后续步骤的选择。原因很简单无故中断会放大故障你本来想通过动态调整保稳定性结果却因为频繁切换策略制造了更多不确定性。另外自适应不等于无限自愈。编排系统至少要有一层熔断总开关——当系统检测到连续N次步骤失败或者异常率超过阈值时自动中止整个任务并进入人工兜底而不是让Agent自己来回重试。这个开关的代码我建议放在编排引擎最靠外的一层确保任何策略回调都绕不过它if self.context.failure_count self.failure_threshold: self.context.status circuit_open raise CircuitBreakerOpenError(连续失败超过阈值任务自动熔断)4. 实战案例企业级数据查询Agent的动态路由与自适应编排落地为了把前面讲的原理串起来我分享一个真实落过地的场景企业级Data Agent。这类系统可以说是动态路由的最佳试验场因为数据源多、权限复杂、查询链路长静态编排根本扛不住。典型用户请求“帮我看一下华东区Q3的销售额环比变化出个简图。”当这个请求进来系统要经过的步骤用静态代码写会疯掉的权限校验Agent判断用户有没有华东区销售数据的读取权限权限粒度可能细化到“只看汇总不看明细”SQL生成Agent根据用户自然语言生成SQLSQL验证Agent检查SQL安全性防止拖库、语法正确性、是否命中索引数据源路由Agent判断该查MySQL还是ClickHouse还是API网关同一指标在不同库里可能都有副本但实时性不同SQL执行Agent实际执行查询结果分析Agent做环比计算可视化Agent生成图表静态编排的问题很明显用户如果没有权限第1步就该结束没必要走到第5步SQL一次生成就正确第3步就是纯浪费数据量小时直接查MySQL快数据量大时才需要走ClickHouse。流程中的每一个分叉点都必须根据运行时上下文动态决策。我们最终落地的路由配置长这样route_policy: query_classification: strategy: rule_first rules: - if: role auditor and query_scope contains 明细 route: [permission_denied] - if: query_time_range 90 days priority_agents: [ck_query_agent] - if: query_time_range 90 days priority_agents: [mysql_query_agent] fallback_agents: [api_gateway_agent] sql_validate: strategy: score_based weight: grammar_score: 0.5 security_score: 0.3 index_hit_score: 0.2 threshold: 0.85 if_below_threshold: [sql_fix_agent, sql_rewrite_agent]这条配置看起来很浅背后的逻辑值得展开说三层第一层这是一条业务语义与架构语义混合的策略。query_time_range 90 days这种规则来源于我们对数据量的认知——超过90天的销售明细在MySQL里全表扫描要10秒以上ClickHouse秒级返回。所以路由不是不变的“固定优先级”而是随请求特征动态变化的判断。第二层阈值与降级路径是关键技术参数。SQL验证分数低于0.85就自动进入修复Agent这不是拍脑袋定的。我们离线回放过2000条真实查询画了“验证分数 vs 人工审核通过率”的分布曲线发现0.85是临界点低于这个分数的SQL人工审核通过率骤降。这个参数在生产中还要定期复评。第三层路由输出不是单值而是一个候选序列。很多初学Agent开发的朋友会误以为路由的结果就是“唯一选中的Agent”。真正生产级的路由器输出的是一个降级优先级序列。首选MySQLMySQL挂掉自动降级到API网关再不行走批处理。这个序列才是有弹性的。自适应编排在这套场景里最出彩的部分是“SQL修复回退链路”。原始编排是线性的生成SQL → 验证 → 执行。我们加了回退后是这样的验证Agent打回SQL自动触发修复Agent尝试改写修复两次仍失败编排引擎将查询代际升级从直查OLTP库切换为查询预聚合表离线指标表如果连预聚合表也没有自动生成一个“数据缺失工单”连同上下文一起转人工用户侧收到的是“查询已受理预计30分钟返回结果”的异步任务而不是当场失败这套机制上线后原本的SQL失败率从12%降到了2.6%用户情绪稳定性提升非常大。编排的价值不是让系统不出错而是让错误变得可控制、可降级、有时限。5. 常见问题与排查技巧实录5.1 “Agent execution terminated due to error”的三类高发原因这个报错可以说是Agent开发圈的热门常客了。我在知乎和GitHub Issue上见过无数次也在自己项目里折腾过多次。总结下来这个错误在动态路由与自适应编排场景里诱因基本逃不出三类上下文爆炸任务执行链路太长中间结果全部塞进上下文超过模型的上下文窗口。在编排系统里尤其容易踩——每个步骤都往共享上下文里写结果四五步之后token数就爆了。我们每次步骤切换后都会做一次“上下文压缩”只保留最近两步的完整结果、更早的步骤降维成摘要。工具schema匹配失败Agent决策说要调用某个工具但动态路由分配给它的Agent根本没有注册这个工具。这种情况多半发生在“路由决策和工具调用决策脱节”时。排查思路是检查路由决策缓存看当前Agent的capabilities里是否包含目标工具。超时后强制终止编排引擎设置了步骤最大时长Agent没跑完就被引擎kill掉。这种通常伴随“总任务时间超限”的次级报错。排查时先看哪一步超时再判断是Agent本身慢还是路由选到了过载的Agent。排查这种报错我的路径是先看完整链路日志再定位是节点故障还是编排引擎故障。核心建议是从头到尾给每个步骤打上traceId和stepId否则你会在一堆Agent日志里迷失方向。5.2 路由震荡和重试风暴两个最隐蔽的坑路由震荡是指同一个请求在极短时间内被路由到不同Agent导致系统行为“抖动”。它的根源通常是评分卡的参数过于敏感——比如LLM评分的置信度在阈值边缘反复横跳或负载因子随并发数快速波动。解决办法是给路由决策加一个决策缓存窗口同一维度下连续两次路由决策必须落在同一个Agent上才生效否则沿用上次决策。重试风暴则是自适应编排引入的大坑。一个下游Agent短暂超时编排引擎立刻重试但重试加重了下游负载导致更多请求超时然后更疯狂重试——类似雪崩。我们的应对措施重试次数上限统一收敛为最多2次无论哪个Agent重试间隔采用指数退避而不是固定间隔Agent执行失败时先进入“降级路由”切到备用候选而不是原地重试同一Agent这个设计看似牺牲了一点单次成功率但换来的是全链路稳定性的巨大提升。我在PPT里跟团队讲重试不是技术利他主义而是对失败代价的重新定价。5.3 常用排查命令速查表这里给出一份在实际项目中反复用到的排查速查表方便大家直接保存症状可能原因排查命令 / 操作路由结果不稳定、忽A忽B路由评分参数过敏感检查路由日志中的decision_reason字段看触发波动的是哪个因子新Agent上线后一直无流量冷启动保护期未配置检查注册中心的freshness_score确认保护期加成是否为0任务中途卡住无响应上下文持续膨胀或Agent等待外部工具返回用traceId查Agent日志检查工具调用的started_at和returned_at编排策略更新不生效配置中心watch未生效或步骤进行中检查配置版本号hash是否更新确认当前步骤状态是否running某Agent负载不均匀权重配置不合理或健康检查误判查看每个Agent的success_rate/avg_latency/current_load三张曲线最后再分享一个压箱底的经验动态路由与自适应编排不是越动态越好。真正稳定的系统是分层自治的最稳定的部分用规则静态兜底中等动态的部分用评分动态切换只有极少数的边缘场景才交LLM仲裁。无条件信任“动态”两个字生产环境会教你做人的。另外如果你想从这套设计里快速起步我建议第一步只做一件事把写死在主流程里的if-else路由迁移到配置中心生成一份可热更新的路由表并加一个可视化的路由决策日志面板。把这步跑通再做编排系统从静态到动态的跨越就会非常平滑。不要一上来就上LLM决策先把结构化规则的底座做实了。
返回列表