ARTICLE DETAIL

资讯详情

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

Agent技能路由:分层架构让大模型只做精排,工程落地详解

Agent技能路由:分层架构让大模型只做精排,工程落地详解 最近在整理 Agent 相关的面试问题时看到这样一个提问一个 Agent 系统里挂了上百个技能用户的请求进来之后到底应该让大模型自己决定调用哪个技能还是先用检索的方式把候选技能筛出来如果你的第一反应是“让模型自己选不就行了”那这道题大概率已经是一个减分项了。这个问题的专业叫法是技能路由Skill Routing它是 Agent 架构里最不起眼但最影响体验的决策层。从表面看它只是“选择哪个技能”这么简单实际上它决定了系统的延迟、成本、稳定性、可解释性甚至安全边界。很多人把 ReAct 这类框架里“模型每步自己决定工具调用”当成默认答案忽略了在技能规模变大之后这条路会越走越窄。这篇文章我会从面试官想听什么讲起再拆解检索式路由和模型式路由的本质区别最后给出一套工程上可落地的分层路由实现。你不需要背八股照着这套思路去理解 Agent 技能路由就能回答得比大多数候选题更扎实。1. 面试官真正想听的不是“二选一”1.1 一个容易被低估的面试题很多 Agent 相关岗位的面试题看起来是在问工具选型其实是在考察系统设计能力。技能路由这个问题最有意思的地方在于它没有一个标准答案但有一个标准的思考框架。一个候选人如果只说“用大模型选更智能”面试官立刻会追问你的 Agent 挂了 500 个技能上下文放不下怎么办模型选错了技能你怎么发现、怎么回滚一次路由调用要花多少 Token、多少延迟高风险技能被模型误调用了谁负责所以面试官真正想听到的是你对“决策成本”“可控性”“可观测性”这三个维度的理解而不是单纯的技术名词。1.2 无脑让模型选错在哪里“让模型自己选”这个回答之所以显得业余是因为它把路由问题简化成了一个提示词问题。在只有三五个技能的原型项目里这种方案确实可行。但一旦进入生产环境技能数量增长到几十甚至几百模型面对一长串工具描述时选择准确率会明显下降。更关键的是模型决策是一个概率过程。同一个请求今天可能会选技能 A明天模型服务升级后可能就选技能 B。这种不确定性对 Agent 产品来说是不可接受的。用户不会因为“模型今天心情不好”而原谅你调错了工具。1.3 技能路由的本质是决策问题剥开所有技术术语技能路由的本质是一道决策题给定一个用户请求如何从技能集合中选出最合适的那个执行单元。这道决策题有三个约束要快。路由层不应该成为 Agent 响应链路的主要瓶颈。要准。选错技能的代价不只是浪费一次调用还可能导致后续流程全部走偏。要能解释。线上出问题时必须能回答“为什么调用了这个技能”。带着这三个约束再去看检索式路由和模型式路由思路就会清晰很多。2. 先搞清楚技术路由到底在路由什么2.1 Agent 技能到底是什么在讨论路由之前先统一概念。所谓技能Skill在 Agent 系统里通常指一个可以被模型或程序调用的原子能力比如查询订单接口天气查询 API发送即时消息调用内部工单系统执行一段 SQL调用第三方 SaaS 服务工程实现上技能往往被描述为一个函数、一个工具 Schema、一个 MCPModel Context Protocol服务或者一个子 Agent。技能注册表Skill Registry就是维护这些技能元数据的中心化组件。2.2 路由层的输入和输出路由层的输入是用户请求可能还包含当前对话上下文、用户身份、环境信息。路由层的输出通常是一个技能名或技能 ID。需要特别注意路由层不一定只做一次决策。复杂 Agent 中路由可能发生在多个层级主 Agent 决定先调用哪个业务域业务域内部再决定调用哪个具体技能技能执行过程中可能还会涌现新的子任务所以一个健壮的技能路由设计必须支持多级路由并且每一级都能独立观测、独立降级。2.3 技能数量膨胀是路由问题的导火索很多人对技能路由重视不足是因为做 Demo 时只有两三个技能模型闭着眼睛都能选对。一旦技能数量膨胀问题立刻暴露所有技能描述加起来可能超过模型上下文窗口相似技能变多模型难以区分新技能上线后模型没有稳定机制去学会使用它路由日志不规范选错后难以复盘因此技能路由不是一个“有就行”的模块而是决定 Agent 能否从原型走向生产的核心组件。3. 检索式路由把选择问题变成匹配问题3.1 检索式路由的三种形态检索式路由的核心思想是不直接让大模型做选择题而是通过事先构建好的索引或规则快速缩小技能候选范围。常见形态有三种。第一种是规则匹配。通过关键词、正则、用户意图映射表来匹配技能。比如请求中包含“订单”直接命中订单查询技能。这种方式最简单准确率高但覆盖不全无法处理语义变化。第二种是分类器路由。训练一个轻量级文本分类模型将用户请求分类到不同技能。这种方式比关键词灵活但需要训练数据而且分类器本身需要持续维护。第三种是向量检索路由。把技能描述和用户请求都转换成向量通过余弦相似度或点积计算相关性返回 Top-K 个技能。工程实践上通常会结合 BM25 做混合检索实现“向量召回 关键词召回 多路融合”这样召回效果会更稳定。3.2 检索式路由的优点检索式路由最大的优势是快。规则匹配和向量检索的耗时通常是毫秒级远低于大模型推理的耗时。成本上检索式路由不需要消耗 Token尤其是向量检索在离线建好索引后在线只需要做一次向量计算。另一个优势是可解释性强。规则命中就是命中向量检索可以输出得分这些信息很容易写入日志方便排障。相比之下模型的解释文本往往只是“事后补理由”并不能真正反映决策过程。3.3 检索式路由的短板检索式路由也有明显边界。它本质上是在做相关性匹配无法完成复杂的语义推理。比如用户问“订单被取消了钱什么时候退回来”这句话里没有“订单”和“退款”关键词的直接组合规则匹配会很吃力向量检索也可能召回多个相似技能无法精确判断。所以检索式路由往往不能单独作为最终决策器它是“召回层”而不是“精排层”。4. 大模型式路由靠推理做最终决策4.1 什么是模型式路由所谓大模型路由就是把技能列表和用户请求一起交给大模型让模型输出要调用的技能名。实现方式有两种主流做法。一种是纯提示词路由。在系统提示词中列出所有技能的名称和描述让模型从列表中选择一个。另一种是基于 Function Calling 的路由把每个技能定义成函数模型在推理过程中返回函数名和参数。基于 Function Calling 的做法更可靠因为模型输出的格式被约束住了不需要再从文本中解析技能名。现在主流的 Agent 框架比如 LangChain、LlamaIndex底层都是这么做的。4.2 大模型路由的优点模型式路由最突出的优势是语义理解能力强。它能处理自然语言表达中的省略、指代、意图转换不需要人工为每个技能设计关键词和规则。在技能数量较少、描述清晰、用户表达比较多样化的场景下模型路由的效果很好。另外模型路由天然具备泛化能力。新技能上线时只要把技能描述补充到提示词里模型大概率能正确使用不需要重新训练分类器。4.3 大模型路由的短板模型路由的短板也很致命主要体现在三个方面。第一延迟和成本高。一次路由调用就是一次大模型推理按照目前主流模型的响应速度通常会比检索多出几百毫秒到数秒。如果需要反复调用Token 成本会持续累积。第二稳定性不可控。模型服务升级、提示词微调、上下文长度变化都会影响路由结果。你可能上线前一天测试全通过第二天模型服务商调整了参数路由结果就变了。第三技能数量有限制。所有技能描述都塞进提示词会快速消耗上下文窗口。即使上下文足够模型也会在大量候选技能之间出现“注意力稀释”选错概率上升。所以模型式路由更适合做“最终决策器”而不是“全量选择器”。5. 为什么不能无脑让大模型自己选5.1 技能列表膨胀后选择准确率会下降这是最关键的一点。假设你只有 5 个技能每个技能描述 50 个字模型很容易选对。当技能数量增加到 200 个技能描述总字数可能超过 1 万甚至更多模型需要在长文本中找到最合适的那个。人类的注意力在长列表场景下会下降模型同样如此。更麻烦的是相似技能。比如“查询订单详情”和“查询订单物流”描述非常接近模型很容易混淆。此时如果只靠模型自由选择错误率会随着技能数量的增加而快速上升。5.2 延迟和成本不可控每做一次路由就相当于多一次模型调用。在普通对话场景中一次 Agent 完整执行可能需要打多个电话如果每次都要模型路由叠加起来延迟就会非常明显。对于实时性要求高的场景比如客服机器人、语音助手几百毫秒的差距就是完全不同的体验。成本上也是如此。大模型的 API 按 Token 计费技能描述、用户请求、模型输出都会产生 Token 消耗。一次两次不多一旦 Agent 请求量上来这笔费用会非常可观。5.3 上下文会被技能清单占满如果把所有技能描述都放在系统提示词里那么这个提示词可能比真正的用户问题长几十倍。这会导致两个问题一是用户上下文被压缩模型难以记住关键信息二是每轮请求都要重复发送这些技能描述浪费大量算力。这也是为什么很多生产级 Agent 不会把所有技能一股脑塞给模型而是先做一轮召回再把候选技能传给模型。5.4 安全边界和权限管控缺失让模型自由选技能还有一个隐蔽问题——安全。模型可能因为提示词注入而被诱导调用高风险技能比如删除数据、发送消息、修改配置。如果路由层没有权限控制和规则拦截模型的一次错误决策就可能造成生产事故。比较稳妥的做法是高风险技能必须走规则或人工审批不能完全交给模型自动决策。安全这条线永远不能依赖模型的“自觉”。5.5 可观测性和复盘困难当线上 Agent 行为异常时你需要回答“为什么调用了这个技能”。检索式路由的答案很明确因为关键词命中或者向量相似度最高。模型路由的答案却很模糊模型说它认为应该调用这个技能。这种模糊一旦变成常态Agent 系统的调试就会非常痛苦。你很难判断是技能描述不清晰、上下文被截断还是模型服务本身发生了变化。6. 工程上的正确答案分层路由6.1 一条核心原则技术路由设计的核心原则可以浓缩成一句话能用规则解决的不交给检索能用检索解决的不交给大模型。这不是否定大模型的价值而是把大模型用在不可替代的地方。规则适合处理高确定性场景检索适合处理模糊匹配场景大模型适合处理复杂语义决策和精排。三层各司其职整个路由系统才能在准确率、延迟、成本之间取得平衡。6.2 三层路由架构生产环境中比较稳妥的架构是“规则层 召回层 精排层”。规则层精确命中关键词、正则可以一步到位的请求直接返回技能不走后续流程。例如包含“订单号”的请求直接进入订单查询技能。召回层规则没有命中时通过向量检索、BM25 等多路召回方式把候选技能缩小到 3 到 5 个。这一步要配合阈值设置召回数量不能太大否则交给模型的候选太长也不能太小否则容易漏掉正确技能。精排层如果召回结果中 top1 的得分显著高于 top2可以直接采用 top1。如果 top1 和 top2 得分接近说明存在歧义这时候才把候选技能列表交给大模型做最终决策。最后还要有兜底策略。当所有层级都无法确定时应该返回“未匹配到技能”或转人工而不是硬选一个大概率错误的技能。6.3 什么情况下让大模型介入大模型介入的时机非常关键。按照我的实践经验以下三种情况适合让模型做精排召回结果中 top1 和 top2 分数差距很小用户请求包含多层意图例如“下雨导致订单取消”技能描述相似度高检索方式无法有效区分只要满足其中一种就可以让模型做最终裁决。但在模型决策前候选技能列表必须已经缩小到个位数这样才能保证决策的准确率和 Token 成本都可控。7. 完整示例从零实现一个分层技能路由下面我用一个可运行的 Python 示例把分层路由的完整流程跑通。这个示例只依赖 numpy不需要真实的大模型 API本地就能运行。7.1 定义技能注册表# skill_registry.py from dataclasses import dataclass, field from typing import List dataclass class Skill: name: str description: str tags: List[str] field(default_factorylist) examples: List[str] field(default_factorylist) enabled: bool True class SkillRegistry: def __init__(self): self._skills {} def register(self, skill: Skill): self._skills[skill.name] skill def get(self, name: str): return self._skills.get(name) def all(self): return list(self._skills.values())这里把技能拆成了 name、description、tags、examples 四个字段。description 给向量检索和模型精排使用tags 给规则路由使用examples 可以用于构建评测集和路由索引。这四个字段在工程中非常重要它们决定了后续每层路由的信息基础。7.2 实现规则路由和向量检索路由# routers.py import numpy as np class RuleRouter: def __init__(self, registry: SkillRegistry): self.registry registry def match(self, query: str) - list: hits [] for skill in self.registry.all(): if not skill.enabled: continue for tag in skill.tags: if tag in query: hits.append(skill.name) break return hits class VectorRouter: def __init__(self, registry: SkillRegistry, embed_func): self.registry registry self.embed_func embed_func self.index {} for skill in self.registry.all(): self.index[skill.name] embed_func( f{skill.name} {skill.description} { .join(skill.examples)} ) def _cos_sim(self, vec1, vec2): vec1 np.asarray(vec1, dtypenp.float64) vec2 np.asarray(vec2, dtypenp.float64) norm1 np.linalg.norm(vec1) norm2 np.linalg.norm(vec2) if norm1 0 or norm2 0: return 0.0 return float(np.dot(vec1, vec2) / (norm1 * norm2)) def match_with_scores(self, query: str, top_k: int 3, threshold: float 0.5): q_vec self.embed_func(query) scores [] for name, vec in self.index.items(): score self._cos_sim(q_vec, vec) if score threshold: scores.append((name, score)) scores.sort(keylambda x: x[1], reverseTrue) return scores[:top_k]RuleRouter 的逻辑很简单遍历技能所有标签只要有一个标签出现在 query 中就命中。VectorRouter 则把所有技能描述和示例 embedding 成向量再计算 query 和技能向量的余弦相似度。这里用字符 n-gram 向量模拟 embedding目的是让你先把流程跑通。生产环境应该换成真实的 embedding 模型比如接入本地部署的 Ollama 或 vLLM 提供的向量接口。7.3 实现大模型精排路由# llm_router.py import json class LLMRouter: def __init__(self, client, modelgpt-4o-mini): self.client client self.model model def decide(self, query: str, candidates: list) - dict: prompt ( 你是一个 Agent 技能路由助手。请根据用户请求从候选中选择最合适的一个技能。\n f用户请求{query}\n f候选技能\n{json.dumps(candidates, ensure_asciiFalse, indent2)}\n 只输出 JSON格式为{\skill\: \技能名\, \reason\: \简短理由\}\n ) resp self.client.chat.completions.create( modelself.model, messages[{role: user, content: prompt}], temperature0, ) content resp.choices[0].message.content return json.loads(content) class MockLLMRouter: 本地验证用的模拟路由生产环境请替换为 LLMRouter。 def decide(self, query: str, candidates: list) - dict: for c in candidates: if c[name] get_weather and ( 天气 in query or 下雨 in query or 带伞 in query ): return {skill: get_weather, reason: 命中天气关键词} return {skill: candidates[0][name], reason: 默认选择第一个}LLMRouter 的核心是把“让模型选技能”变成“让模型做候选精排”。注意这里传给模型的只有候选技能列表而不是全部技能。这解决了上下文过长和注意力稀释的问题。temperature 设为 0并要求只输出 JSON是为了尽可能让路由结果稳定可解析。7.4 组装成分层路由主流程# hybrid_router.py class HybridRouter: def __init__(self, rule_router, vector_router, llm_router, registry, min_margin0.05): self.rule_router rule_router self.vector_router vector_router self.llm_router llm_router self.registry registry self.min_margin min_margin def route(self, query: str): trace [] # 第一层规则路由 rule_hits self.rule_router.match(query) trace.append({step: rule, candidates: rule_hits}) if len(rule_hits) 1: return self.registry.get(rule_hits[0]), trace # 第二层向量召回 vector_hits self.vector_router.match_with_scores(query, top_k3, threshold0.5) trace.append({step: vector, candidates: [h[0] for h in vector_hits]}) if len(vector_hits) 1: return self.registry.get(vector_hits[0][0]), trace if len(vector_hits) 2: top1, score1 vector_hits[0] _, score2 vector_hits[1] if score1 - score2 self.min_margin: return self.registry.get(top1), trace # 第三层大模型精排 if len(vector_hits) 0: candidates [s.name for s in self.registry.all() if s.enabled] else: candidates [h[0] for h in vector_hits] trace.append({step: llm, candidates: candidates}) llm_candidates [] for name in candidates: skill self.registry.get(name) llm_candidates.append({name: skill.name, description: skill.description}) decision self.llm_router.decide(query, llm_candidates) return self.registry.get(decision[skill]), trace这个 HybridRouter 就是分层路由的骨架。规则层命中一个技能就直接返回向量召回只有一个高置信度命中也可以直接返回如果 top1 和 top2 得分接近才让大模型介入。route 方法返回了 skill 和 trace。trace 记录了每个层级命中的候选列表这是路由可观测性的关键。线上排查时只要看 trace 就能知道决策路径。需要提醒的是如果技能数量特别大例如超过 500 个vector_hits 为空时把所有技能传给模型仍然不合理。这种情况下应该再加一层粗排或者设置默认拒绝策略。7.5 完整的本地演示代码# demo.py import hashlib import numpy as np from skill_registry import Skill, SkillRegistry from routers import RuleRouter, VectorRouter from llm_router import MockLLMRouter from hybrid_router import HybridRouter def char_ngram_embed(text: str, dim: int 64) - np.ndarray: vec np.zeros(dim) for i in range(len(text) - 1): gram text[i:i 2] idx int(hashlib.md5(gram.encode(utf-8)).hexdigest(), 16) % dim vec[idx] 1 return vec def build_registry(): registry SkillRegistry() registry.register(Skill( namequery_order, description查询订单的状态、金额、数量、物流进度, tags[订单], examples[我的订单到哪了, 查询订单金额], )) registry.register(Skill( nameget_weather, description查询城市实时天气、气温、降水概率, tags[天气], examples[北京今天天气, 上海会下雨吗], )) registry.register(Skill( namesend_message, description向指定联系人发送消息, tags[发送, 消息], examples[给张三发消息, 把文件发给李磊], )) return registry def main(): registry build_registry() rule_router RuleRouter(registry) vector_router VectorRouter(registry, embed_funcchar_ngram_embed) llm_router MockLLMRouter() hybrid_router HybridRouter(rule_router, vector_router, llm_router, registry) queries [ 帮我查一下昨天订单金额, 北京明天天气怎么样, 把项目周报发给李磊, 明天出门要不要带伞, ] for query in queries: skill, trace hybrid_router.route(query) print(f请求: {query}) print(f路由结果: {skill.name}) print(f路由轨迹: {trace}) print(- * 50) if __name__ __main__: main()这里特意做了一个 MockLLMRouter这样你不需要申请任何模型 API 就能在本地看到完整的分层路由效果。生产环境只需要把 MockLLMRouter 换成真实的 LLMRouter并传入对应的模型客户端。8. 运行结果与效果验证8.1 运行方式环境要求是 Python 3.9 及以上安装 numpy 即可pip install numpy python demo.py8.2 预期输出结构运行后你看到的输出应该是一个结构化的路由轨迹。以“帮我查一下昨天订单金额”为例请求: 帮我查一下昨天订单金额 路由结果: query_order 路由轨迹: [{step: rule, candidates: [query
返回列表