从硬编码到模型自主决策:Agent 路由重构的完整实践指南
发布时间:2026/9/9 6:36:15来源:尧图网络
做 Agent 开发的朋友应该都经历过这个阶段一开始路由逻辑写得很死用户说“查天气”就命中天气工具说“设闹钟”就跳闹钟流程代码里全是 if-else 关键词匹配。工具少的时候没问题但随着接入的工具越来越多分支不断膨胀新场景层出不穷规则之间开始互相打架。后来我下定决心把写死在代码里的 Agent 路由改成了模型自主决策简单说就是让大模型自己选下一步调用谁、怎么调。这篇文章从设计思路、核心实现、踩坑记录到性能对比把完整过程拆给你看适合那些正在做多工具 Agent、子 Agent 编排或者准备把路由模块重构成“模型驱动”的开发者。1. 内容整体设计与思路拆解1.1 写死路由的痛点代码维护与扩展性瓶颈先说最直观的问题硬编码路由在早期确实好用但一膨胀就开始失控。我最早写的路由是这个风格的def route(user_input): if 天气 in user_input: return weather_tool elif 闹钟 in user_input or 提醒 in user_input: return alarm_tool elif 日历 in user_input ...: return calendar_tool else: return default_llm刚开始只有三四个工具代码还算清晰。后来接入了邮件、待办、股票、导航、音乐播放、智能家居等等这段代码变成了上百行的 if-else甚至还有嵌套判断。真正的问题不是代码丑而是三个结构性矛盾第一意图边界越来越模糊。用户说“帮我看看明天早上有没有会顺便定个提醒”这句话同时涉及日历和提醒两个工具。硬编码路由只能匹配一个分支导致另一个需求被丢掉。我试过用数组返回多个候选工具但参数如何分发又成了新的复杂度来源。第二规则之间存在优先级冲突。“帮我订个餐厅”可能是“搜索工具”也可能是“本地生活工具”还可能是“日程工具”。一旦用规则处理这种歧义代码里全是优先级魔法今天改了 A 明天坏了 B。团队里其他同事改起来也胆战心惊。第三规则永远赶不上需求变化。每接一个新工具都要重新梳理所有旧规则确认关键词不冲突、顺序不覆盖。这种维护成本是指数级上升的。我当时统计过每增加一个新工具平均要花半天到一天的时间调路由逻辑。说白了路由本身就应该是一个决策问题而不是匹配问题。如果把“用户当前最需要哪个工具/哪个子 Agent”交给模型去根据语义和上下文判断代码的维护负担会小很多决策的泛化能力也会强很多。1.2 模型自主决策的本质把“路由”本身当任务模型自主路由的核心思想并不复杂路由不是一个固定的函数调用链而是让大模型像“总控调度员”一样理解当前的用户请求、可用工具有哪些、上下文状态是什么然后输出一个结构化决策结果。这个结果里包含“应该走哪个 handler”和“这个 handler 需要什么参数”。这就好比你是一个大公司的前台过去你手里有一本厚厚的分机表靠关键字锁定要找的部门现在换了一个经验丰富的秘书他能听懂对方模糊的表达甚至能根据上下文推断出该转到哪个部门。模型的泛化能力让它不需要把每个场景都写进规则里。这一点在 Agent 场景里尤其重要。Agent 面对的任务本来就不是封闭集用户的语言千变万化同一个意图可以有几百种说法。硬编码路由要求你把每一种说法都抽象成关键词和规则而模型自主决策只需要你描述清楚“有哪些工具、分别能做什么”它自己就能完成语义映射。当然这不是说完全不需要规则了。规则更适合做“白名单”和“安全校验”比如某个操作只能在特定权限下执行这类内容不应该交给模型自由判断。我的方案是把模型路由放在“决策层”把规则校验放在“执行层”各管一段。1.3 方案选型工具调用与结构化输出的取舍真正动手改造之前要定一个技术方案。主流的做法有两条路一条是使用平台的原生 Function Calling / Tool Calling另一条是让模型直接输出一个结构化 JSON然后自己在代码里解析执行。我后来实际采用的是“Function Calling JSON 兜底”的双轨方案。下面这张表是我当初做对比时整理出来的方案实现难度输出稳定性参数校验模型兼容性Function Calling中等较高格式由模型保证依赖框架部分模型支持有限自定义 JSON 指令较低不稳定需额外校验可完全控制几乎所有模型都可用Function Calling JSON 兜底中等高兼顾两者可完全控制更通用Function Calling 的优点是模型在训练阶段就见过这类格式输出很少乱掉但缺点也很明显不同模型的服务商对 function call 的格式定义不完全一致换一家服务商可能就要重新适配。自定义 JSON 指令则更灵活缺点是要处理格式不合法的情况。我最终采取的策略是优先用平台原生的 function calling 拿到标准化的参数结构如果拿不到或者模型没有启用 function call 能力就退化成“输出 JSON”的指令再加上一层 Pydantic 校验。这样既保证了主流模型的体验也能兼容没有 function call 功能的备用模型。路由决策的“输出目标”不只是选工具还要顺带抽出参数。比如用户说“帮我把明天的会议推到下午三点”那么路由结果应该是{ handler: calendar_tool, action: reschedule, params: { event_time: 明天, new_time: 15:00 } }一次性完成“选路”和“填参”能省掉一次额外的模型调用延迟也少一截。这个设计在后面的实操部分会细说。2. 核心细节解析与实操要点2.1 路由决策的输入任务描述、工具清单与上下文要让模型做出靠谱的路由决策输入侧的信息至少要包含三样东西用户的任务描述。这个不只是用户最新输入的文本最好把最近几轮对话都带上。因为用户经常会在多轮对话中省略主语比如上一轮聊了“北京出差”这一轮只说“帮我订酒店”如果不把上一轮信息给模型很容易路由到通用搜索而不是差旅酒店工具。当前可用工具的清单。清单里包含工具的名字、描述、参数 schema以及这个工具适合解决什么问题。不是说把所有工具一股脑全塞进去而是要做一个“候选池”根据用户所处场景或历史行为先粗筛一遍减少模型的选择压力。其他上下文信息。例如当前时间、用户 ID、用户权限、所在城市、设备类型等。有些路由决策依赖这些信息。比如用户在手机端说“打开手电筒”应该路由到手机控制工具在车机端说同样的话则要路由到车控工具。我实现了一个build_context()函数负责把这些信息组装成一份上下文字典再格式化成模型能理解的自然语言或结构化字段。def build_context(user_request, history, session): tools get_candidate_tools(session.user_scene) context { task: user_request, history: history[-5:], tools: [t.to_model_description() for t in tools], time: get_current_time(), user_scene: session.scene, permissions: session.permission_tags, } return context这里有个容易被忽略的细节工具描述的质量直接影响路由准确率。我一开始用“查询天气”这种极简描述模型经常把“今天需要带伞吗”路由到通用对话工具后来改成“查询指定城市和日期的天气返回温度、风力、降水概率可回答‘带不带伞’‘冷不冷’这类衍生问题”准确率立刻提升。工具描述里最好直接带上典型使用场景和同义触发词这是投入产出比最高的一项改进。2.2 路由指令的结构化设计从自由文本到机器可执行模型输出的路由指令不能是自然语言必须是机器可解析的结构化数据。我定义了一个统一的RouteDecision结构包含以下核心字段字段类型说明handlerstring路由目标的唯一标识actionstring在目标工具内部执行的动作如query、create、updateparamsobject动作所需的具体参数键值类型由工具的 schema 决定confidencefloat模型对本次路由决策的置信度0到1之间reasonstring一段简短的中文决策理由用于日志和调试reason字段看起来不起眼但非常关键。模型在输出决策理由时往往能约束自己“想清楚再做选择”。我在实验中发现加了reason字段之后路由准确率能提高 3% 到 5%。原因可能是强迫模型经过一步内部推理而不是直接蹦结果。参数部分要特别注意类型约束。比如日期参数模型可能输出“明天”“下周一”“2025-06-01”等多种格式我统一要求输出标准 ISO 日期如果模型给的是自然语言相对日期则在参数解析层再转换一次。为了让模型少犯错我会在 prompt 中给一个参数示例并且强调“如果需要的信息缺失不要捏造将 params 留空并在 action 中设为 need_more_info”。2.3 路由决策失败时的兜底策略模型总有不听话的时候所以兜底策略不是可选项而是必须项。我总结了一套分级兜底第一级默认路由。如果模型返回的 handler 不在可用工具候选池里或者 confidence 低于 0.4就直接走默认路由。默认路由一般是“通用对话模型”或“人工客服提示”避免用户请求被卡死。第二级格式化重试。如果模型输出无法解析成合法 JSON或者 Pydantic 校验失败我会把“解析失败原因”拼到原始 prompt 里让模型重新生成一次。例如“上次输出中 params.event_time 格式不合法请使用 ISO 日期再次输出 JSON。”重试一般只做 1 到 2 次防止死循环和延迟浪费。第三级用户澄清。如果重试之后仍然失败或者模型主动把 action 设为need_more_info就转发给一个澄清流程由 Agent 主动向用户询问缺失的关键信息而不是硬着头皮执行一个残缺的参数。兜底逻辑看起来简单但它是整个系统稳定性的压舱石。没有兜底的时候我遇到过模型直接把 handler 输出成undefined导致执行器抛异常整个 Agent 会话直接中断。现在有了兜底这类问题基本都被悄悄消化了。2.4 延迟与成本优化不能什么都调大模型路由决策本质上就是“一次额外的模型推理”所以延迟和成本是需要认真算账的。我当时的基准测试数据是小规模候选池5 个工具以内的模型决策耗时约 200ms 到 400ms中规模候选池10 到 20 个工具约 500ms 到 900ms成本视模型规格而定。为了不让路由决策拖垮整体响应时间我做了几个优化用小模型做初筛。路由决策不需要满血版本我大部分场景用的是中等规格模型。只有遇到低置信度或复杂参数抽取时才升级到更大模型重试一次。候选池裁剪。用户处于什么场景就只把该场景下可能用到的工具塞进 prompt。比如用户正在“订酒店”流程候选池里基本不会出现“开灯”这样的家居工具。这样既让模型更容易选对也减少了 prompt 长度降低成本。路由结果缓存。对已经成功路由过的“意图 参数模板”进行缓存。比如用户每天都问“今天广州天气”第二次就可以直接走 hash 命中不需要再让模型跑一遍。当然缓存要带过期机制避免时间敏感类请求拿到旧路由结果。合并决策。前面提到过一次调用同时完成路由和参数抽取这比“先调用一个模型路由到工具再调用另一个模型抽取参数”要省一半延迟。实测下来合并决策比分散决策平均省 40% 到 50% 的路由链路耗时。成本上也有一些意外收获。因为路由决策让后续工具调用更准确无效的通话变少了整体调用次数反而下降。以前硬编码路由匹配错了后面还要再调用一次模型重新澄清现在一次路由基本到位综合成本没有明显上升。3. 实操过程与核心环节实现3.1 改动前后的代码对比从 if-else 到声明式路由直接看重构前后的代码对比最直观。重构前路由逻辑是一长串条件判断每次新加一个工具就叠一层def route(user_input, session): if keyword_contains(user_input, [天气, 降雨, 温度]): city extract_city(user_input) return route_to(weather_tool, {city: city}) elif keyword_contains(user_input, [提醒, 闹钟, 待办]): return route_to(todo_tool, {text: user_input}) elif ...重构后路由变成一个“决策 执行”的两段式结构def route(user_request, session): context build_context(user_request, session) decision route_engine.decide(context) # 模型自主决策 validated validate_decision(decision) # Pydantic 校验 return executor.run(validated) # 执行路由主流程变得很短所有复杂性都封装进route_engine和executor。新加一个工具时只需要注册工具的描述和参数 schema路由逻辑不用动。这个变化对团队协作意义重大——不再需要业务开发去理解“怎么改路由规则”只需要提供工具定义就行。3.2 模型路由的 Prompt 设计与 few-shot 示例Prompt 设计是模型路由效果好坏的一条生命线。我这里提供一套可直接参考的 prompt 模板核心包括角色设定、候选工具描述、严格的输出格式约束和 few-shot 示例。系统提示词的简化版本如下你是一个智能 Agent 的路由决策引擎。你需要根据用户的请求从可用工具中选择一个最适合的工具并生成该工具的目标动作和参数。 工具列表如下 {tools} 约束 1. 只输出 JSON不要包含任何解释。 2. JSON 格式{handler: ..., action: ..., params: {...}, confidence: 0.0-1.0, reason: ...} 3. handler 必须是工具列表中出现过的名字。 4. 如果没有合适的工具handler 填 defaultparams 为空。 5. 如果用户请求缺少必要参数不要编造把 action 设为 need_more_info。few-shot 示例也非常重要尤其是针对模糊场景和不常见说法。我会在 prompt 里放两个示例示例 1 用户请求帮我看看明天北京会不会下雨 输出{handler: weather_tool, action: query, params: {city: 北京, date: 2025-07-11}, confidence: 0.97, reason: 用户询问降雨天气属于天气查询场景} 示例 2 用户请求明天早上提醒我带身份证 输出{handler: todo_tool, action: create_reminder, params: {text: 带身份证, time: 早上}, confidence: 0.95, reason: 涉及提醒事项需要创建待办提醒}实践里我发现few-shot 示例不需要太多2 到 4 个足够。关键是要覆盖“容易混淆的相似意图”让模型知道边界在哪里。比如“把明天会议改成下午三点”和“帮我约一个下午三点的会”一个路由到calendar_tool/reschedule一个路由到calendar_tool/create如果只给了一个示例模型容易混淆。3.3 输出校验与执行器的实现用 Pydantic 把模型输出变成合法数据模型给出的 JSON 终究是字符串直接拿来执行不安全。我用 Pydantic 定义了一个RouteDecisionModel用来做强类型校验from pydantic import BaseModel from typing import Any, Optional class RouteDecisionModel(BaseModel): handler: str action: str params: dict {} confidence: float 0.5 reason: str 在validate_decision里我会先尝试解析 JSON再做模型校验并且处理常见的不合法情况def validate_decision(raw_decision: str): try: data json.loads(raw_decision) decision RouteDecisionModel(**data) return decision except (json.JSONDecodeError, ValidationError) as err: # 记录错误原因后续用于重试 raise RouteValidationError(str(err))如果校验失败我会把错误信息传回给模型让它重试一次。这里有个小技巧不要直接把整个原始输出丢给模型看而是告诉它“哪个字段校验失败、为什么失败”。比如“params.date 格式应为 YYYY-MM-DD当前是 2025年7月11日”模型的修正成功率会高很多。执行器这块我用的是一个简单的注册表模式executor.register(weather_tool, weather_handler) executor.register(todo_tool, todo_handler) def run(decision: RouteDecisionModel): handler_instance executor.get(decision.handler) if not handler_instance: return default_handler(f未找到工具 {decision.handler}) # 执行前做权限验证 if not check_permission(decision): return permission_denied() return handler_instance.action(decision.action, decision.params)执行器里我特意加了一行权限验证。因为模型可能根据上下文选择了某个工具但用户的账号不一定具备对应权限。比如免费用户请求查高级报表路由决策可能正确命中报表工具但权限校验会拦截住返回友好的升级提示。这个防线不能省。3.4 灰度上线与效果评估别一把梭直接替换改成模型路由之后我没有直接在线上全量替换而是做了一个灰度对照。新老双路由并行跑了大概两周逐步把流量从“写死路由”切到“模型路由”。我定义的评估指标有这几个指标说明路由准确率路由到的工具是否正确由人工抽检 用户反馈判断任务完成率单个会话中任务是否顺利执行完成含工具调用成功平均响应时延从用户发起请求到最终回复的总时长工具调用失败率路由参数错误或工具执行异常的比例无效澄清率因决策信息不足而需要向用户追问的比例灰度期间我用离线标注的 200 条典型请求做了一次对照测试。结果是老路由的准确率大约 84%模型路由可以达到 95% 左右。任务完成率从 78% 提升到 91%。比较意外的收获是无效澄清率降低了接近一半因为模型在参数抽取阶段就能识别出信息缺失主动选择need_more_info而不是错误地触发某个工具然后执行失败。当然灰度期间也暴露了很多问题比如有几类偏冷门的请求模型总是选错工具这让我意识到单纯调 prompt 是不够的还需要在候选池侧下一番功夫。后面常见问题部分再详细说。4. 常见问题与排查技巧实录4.1 模型频繁选错工具怎么办工具描述与候选池优化灰度初期我遇到的最典型问题是用户问“帮我查一下明天几点的飞机”模型经常路由到搜索工具而不是航班工具。排查日志时发现候选池里航班工具的排序相对靠后而描述里写的又是“航班查询”这种太窄的词。模型看不出“明天几点的飞机”属于“航班工具”的语义范畴。我的解决办法有三个重写工具描述加入场景化语言。比如航班工具的描述改成“查询航班时刻、起降时间、机场信息、机票价格适用于‘几点飞’‘航班号’‘机票查询’等场景”加入常见的口语表达覆盖率立刻提升。动态调整候选池顺序。根据会话历史中的会话主题把可能用到的工具往前排。实测把工具列表顺序从“按注册顺序”改成“按用户高频使用排序”后路由准确率又涨了 2% 左右。这背后的原理也很简单Transformer 对序列前部注意力更集中候选顺序不能被忽略。限制候选池大小。如果候选池超过 20 个工具误选率会明显上升。我把工具按场景分组先粗筛场景再路由。比如用户当前在“出行场景”候选池只会包含出行相关工具不会塞一堆无关的家居工具。4.2 模型输出不是合法 JSON 或格式不稳定虽然我用的是 Function Calling 优先但备用 JSON 模式下还是经常遇到模型输出多余内容。比如模型会在 JSON 前后加“json”标记偶尔还会补一句“这是为您生成的路由结果”。我的处理办法是写一个extract_json函数专门从模型输出里提取第一段合法 JSONdef extract_json(text: str): start text.find({) end text.rfind(}) if start -1 or end -1: raise JsonExtractError() raw text[start:end1] return json.loads(raw)同时我会在 prompt 里加强硬约束明确“禁止使用 markdown 代码块只输出裸 JSON”。还可以利用模型的response_format {type: json_object}参数如果服务商支持的话这个参数能大幅提升 JSON 输出的稳定性。如果重试两次仍然失败就不再依赖模型了走 default 路由。宁可让通用对话模型接手也不能让 Agent 卡死在解析异常上。4.3 路由抖动导致同一句话出现不同结果有一段时间同一个用户的同一句话隔几分钟调用路由结果却不同。第一次路由到创建待办第二次却路由到日历工具。这个问题很影响体验也让测试用例不稳定。排查后确认主要原因是模型温度设置过高和上下文里有无关的历史消息干扰。解决方案把路由决策阶段的模型温度降到 0 或接近 0。路由决策是低创造性任务不需要发散温度越低越稳定。对历史消息做一次“相关性截取”只保留最近几轮和当前请求相关的消息。有些无关闲聊会被模型错误当成路由线索。加入会话级路由偏好。比如用户在对话中已经明确说过“帮我安排行程”之后只要涉及“安排”类动词就优先路由到日历工具。这种偏好可以动态注入 prompt。这套组合拳下来路由抖动率从 10% 多降到了 2% 以内。4.4 日志与链路追踪把“错误决策”变成可复盘的数据模型路由的黑盒性比规则路由更强所以日志和链路追踪比以往更重要。我的日志设计围绕“决策可复盘”展开{ request_id: 7f3c9a..., user_id: u_12345, scene: trip, task: 帮我订明天去上海的机票, chosen_handler: flight_tool, chosen_action: book, params: {date: 2026-07-12, destination: 上海}, confidence: 0.92, decision_reason: 用户明确表示订机票目的地为上海, model_input_snapshot: ……截断后的上下文, model_output_raw: ……模型原始输出, latency_ms: 680, success: true }这里最花功夫的是model_input_snapshot和model_output_raw它们能帮我在模型选错工具时回溯“模型看到了什么、为什么这么选”。我曾通过日志发现某次选错是因为候选列表中出现了两个同义工具导致模型随机选了一个。后来把重复功能归并问题就消失了。可视化追踪工具我也试过比如 Langfuse、LangSmith能直接展示每一次路由决策的轨迹。如果团队有条件建议接一个排查效率会高很多。没有条件的话先用结构化日志加简单查询也能满足大部分需求。4.5 安全与权限边界模型决策不能覆盖系统红线最后也是最重要的一点模型自主决策不等于把“能不能做”的决定权也交给模型。我在执行器里加了一道独立的授权校验层专门做两件事校验用户是否有权调用该工具/动作。比如“删除所有数据”这类危险操作即使模型根据用户请求正确路由到了删除工具执行前也必须二次检查账号权限和操作风险等级。对高风险操作启用二次确认。模型决定路由到“发送邮件”“删除文件”“支付订单”等动作时我不会直接执行而是先回复用户一句确认信息等用户明确说“确认”之后再真正执行。有的场景还需要限制“模型能感知的工具范围”。比如普通用户身份下即便候选池里包含“管理员工具”也不应该在 prompt 中出现。这样从源头避免模型误路由到敏感工具。安全这一块没有太多可妥协的空间哪怕是灰度阶段也应该从一开始就带上权限校验而不是等出了问题再补。改造完成之后我最大的感受是路由从一叠死板的规则变成了一层带有“判断力”的胶水系统的扩展性上了不止一个台阶。新接入一个工具不再需要我去改路由分支代码只需要写好描述和参数 schema剩下的适配交给模型。但我也必须承认模型自主决策不是银弹它需要你在候选池、描述、校验、兜底、日志这些看不见的地方下足功夫才能跑得既聪明又稳定。最后再分享一个小技巧候选工具列表的排序真会影响路由准确率把高频工具往前放模型选对的概率会有肉眼可见的提升。别小看这种“细碎”的优化累积起来就是 1% 到 2% 的准确率差距。如果你也在折腾 Agent 路由先从小范围灰度开始把日志记录好你的每一步踩坑都会变成系统稳定性的垫脚石。
网站建设高端定制企业官网