
面试里问到 Agent 技能路由时最怕的是只背了一句话答案让大模型用 function calling 自己选。这个回答在技能只有五六个的场景里没问题但一旦 Agent 挂了十几、几十甚至上百个技能技能描述高度相似线上对延迟和成本又有硬约束让模型从全部技能里硬选就会暴露出准确率、耗时、可解释性三方面的缺陷。技能路由本质上不是“谁的语义理解更强”的问题而是“在什么条件下用哪种决策方式”的问题。这篇文章会拆解检索式路由和大模型路由的原理、成本与边界给出一个检索召回加模型精排的混合路由框架并整理一套可以直接用到面试回答和线上排查里的判断标准。1. 先搞清楚技能路由到底在解决什么问题1.1 什么是 Agent 技能路由技能路由指 Agent 在收到用户请求后决定调用哪个技能、工具或插件的过程。这里说的技能可以是查询订单、查天气、查知识库、生成报表、操作日历等任何可以被封装成接口的能力。一个最简单的 Agent 只有一两个技能路由问题几乎不存在模型看一眼用户问题就能确定意图。但当技能数量增长到几十个甚至上百个时问题就变了用户说“帮我看看上次买的手机什么时候到”应该调用订单查询还是物流查询还是先去知识库搜一下退货政策。用户说“帮我订一家周五晚上适合带孩子的餐厅”应该调用餐厅搜索还是先调用日历确认空闲时间。用户说“把上周的销售数据做成图表”应该直接调报表技能还是先检索有哪些报表模板。路由的输入是用户请求和上下文输出是“选中的技能名 参数 是否需要用户补充信息”。路由的质量直接决定后续技能调用的成功率也决定用户对整个 Agent 的体验。1.2 路由问题的本质是缩小决策空间很多方案设计不下去是因为把路由当成了“让模型读题”而不是“缩小决策空间”。检索式路由的思路是先通过向量相似度、关键词、标签、用户权限等条件把几百个技能缩小到几个候选大模型路由的思路是在候选技能集合上做意图判断。两者真正的差别不是“谁更聪明”而是“把决策成本放在哪一层”。缩小决策空间还有另外一层含义从技能注册表里过滤掉当前用户无权调用的技能。比如一个普通用户不应该路由到“管理员重置密码”技能一个未登录用户不应该路由到“查看我的订单”技能。这些约束如果靠模型自觉就会不定期出现越权选择如果在路由前置阶段用元数据过滤则是稳定的硬规则。1.3 为什么“让模型自己选”不是标准答案“让模型自己选”这个答案在小规模场景下成立但从工程角度看有三个问题。第一是提示词膨胀。把 100 个技能的 name、description、参数 schema 全部塞进一次模型调用输入 token 会变得很大延迟和成本都会上涨。此时模型还要在超长上下文中保持对每个技能边界的准确判断出错的概率明显增加。第二是模型选择结果不可控。模型可能输出一个不存在的技能名可能选一个权限之外的技能也可能在几个相似技能之间摇摆。如果路由逻辑没有校验和回退错误会直接传导到下游。第三是问题不可调试。模型选错了你很难说清楚是因为描述不清楚、上下文缺失还是模型本身判断失误。检索式路由把选择过程变成了可看分数、可调阈值、可做离线评测的过程。所以面试题里那句“千万别无脑回答让模型自己选”真正的意思是要先分析路由问题的约束条件再决定用检索、模型还是混合方案。2. 检索式路由与大模型路由的完整对比2.1 检索式路由的工作方式和适用条件检索式路由把技能选择变成一次搜索任务常见实现有三类向量召回把用户请求和技能描述分别转成向量用余弦相似度排序取 TopK。关键词召回把技能配置里的关键词列表和用户请求做匹配命中数量越多排名越靠前。元数据过滤根据用户权限、技能分组、地域、版本等字段直接过滤。检索式路由的优势是快、便宜、可解释。向量检索一次通常在几十毫秒以内关键词命中几乎不增加成本而且每个技能都能看到分数哪个环节导致选择错误很容易定位。它的问题也很明显路由质量高度依赖技能描述和关键词的质量。如果两个技能描述都写得很泛比如“查询用户数据”和“查询订单数据”向量区分度就会不足。如果用户请求里的词汇和技能描述完全不重叠纯关键词召回会漏掉。2.2 大模型路由的工作方式和适用条件大模型路由把技能选择交给模型做意图判断典型实现是 function calling 或结构化输出。模型拿到技能列表后通过语义理解判断应该调用哪个技能并生成参数。它适合技能数量少、技能边界复杂、需要结合多轮上下文判断的场景。例如用户先说“改一下明天的会议”再补充“还是改到后天吧”路由需要理解“还是”指代的是会议技能而不是新建日程技能。这种指代消解和上下文理解靠纯检索很难做好。代价是每一次路由都增加一次模型调用token 消耗随技能列表变长而上升且选择结果有随机性。技能数量从 10 个涨到 100 个时即使使用支持 function calling 的模型路由准确率也会明显下滑。这类问题在不同模型上表现不同但整体趋势是一致的候选越多模型越容易混淆相似技能。2.3 两种路由的选型对比表下面的表格可以直接用于面试回答和方案评审。对比维度检索式路由大模型路由混合路由决策依据向量相似度、关键词、元数据语义理解、上下文、意图判断检索召回后模型精排延迟低毫秒级高增加一次模型调用低延迟检索加一次精排Token 成本低随技能数量线性增长中低只输入候选技能技能数量扩展性好几百个也能处理差几十个后准确率下滑好可解释性高可以看到分数和命中词低黑盒判断中高召回阶段可观测可控性高可以用规则硬约束低可能输出非法技能名高多级校验对技能描述要求高描述和关键词要覆盖中模型能理解跳跃表达高典型适用场景技能多、对成本和延迟敏感技能少、语义复杂、有上下文生产环境主流2.4 从 10 个技能到 200 个技能问题会暴露在哪里拿“10 个技能”和“200 个技能”各推演一遍能更清楚两种方案的边界。10 个技能时把所有技能描述塞进 prompt让模型用 function calling 选择准确率通常不错单次调用延迟也还能接受。此时检索路由反而要额外搭建向量索引和阈值体系收益不明显。50 个技能时prompt 已经开始变长部分相似技能会被模型混淆。比如“退款申请”和“退款进度查询”是两个不同技能模型可能只凭“退款”两个字选错。200 个技能时如果不做检索召回直接把全部技能暴露给模型prompt 可能超过模型的上下文限制即使不超过准确率和 Token 成本也会同时出问题。这个阶段几乎必然要引入检索召回让模型只从 Top5 或 Top10 候选里做最终决策。所以判断标准不是“该用检索还是大模型”而是“我的技能数量和技能相似度到了哪个阶段要不要分层”。3. 检索式路由的最小落地实现3.1 准备环境和技能注册表先用一个 Python 环境和一套技能注册表把检索路由跑通。依赖可以只使用 numpy向量模型接口留成占位函数实际项目换成自己接入的 embedding 服务。技能注册表是路由的基础字段建议包含唯一名称、描述、关键词、参数 schema、权限标签和分组。下面是一个示例[ { name: order_query, description: 查询用户的历史订单、物流进度、预计送达时间适用于用户询问购买商品状态, keywords: [订单, 物流, 快递, 发货, 送达, 快递单号], parameters: { type: object, properties: { order_id: {type: string} }, required: [] }, permission: user_own_order, tags: [order, logistics] }, { name: weather_query, description: 查询指定城市当天和未来几天的天气、温度、降雨概率、风力, keywords: [天气, 温度, 降雨, 风力, 城市], parameters: { type: object, properties: { city: {type: string} }, required: [city] }, permission: public, tags: [life] } ]这里描述不要写成一句话而要写清楚触发条件、能力边界和典型参数。描述质量直接决定向量召回的区分度。3.2 用向量相似度做技能召回向量召回的核心是把用户请求和技能描述映射到同一向量空间计算相似度排序。示例代码如下import numpy as np def embed(text: str) - list[float]: # 实际项目中替换为具体的向量模型接口 raise NotImplementedError def _cosine_similarity(a: list[float], b: list[float]) - float: a np.asarray(a, dtypenp.float32) b np.asarray(b, dtypenp.float32) dot float(np.dot(a, b)) norm float(np.linalg.norm(a) * np.linalg.norm(b)) return dot / (norm 1e-9) def build_index(skills: list[dict]) - list[dict]: for skill in skills: skill[embedding] embed(skill[description]) return skills def retrieve_skills(query: str, skills: list[dict], top_k: int 3) - list[dict]: query_vec embed(query) for skill in skills: skill[score] _cosine_similarity(query_vec, skill[embedding]) ranked sorted(skills, keylambda s: s[score], reverseTrue) return ranked[:top_k]关键点有两个。第一embedding 的对象建议是“描述加关键词拼接后的文本”比单独 embedding description 效果更稳定。第二向量召回的分数是相对排序分数不能直接当绝对置信度使用阈值需要单独标定。3.3 用关键词和元数据规则兜底向量召回对同义表达比较强但遇到专有名词、缩写或稀疏词汇时可能漏掉。常见做法是加入 BM25 多路召回和关键词命中再把几路结果做分数融合。def keyword_hits(query: str, skills: list[dict]) - list[dict]: hits [] for skill in skills: hit_count sum(1 for kw in skill.get(keywords, []) if kw in query) if hit_count 0: skill[keyword_score] hit_count hits.append(skill) return sorted(hits, keylambda s: s[keyword_score], reverseTrue)元数据过滤则更硬。比如未登录用户直接过滤掉所有需要登录权限的技能企业租户只保留当前租户启用的技能。过滤应该在召回之前做既能减少计算量也能避免权限越界。多路召回后的融合没有标准公式常见方案有两种一是把向量分数归一化后与关键词命中数加权求和二是向量召回 Top20、关键词命中 Top10取并集后再让排序层统一打分。第一种简单第二种更可控。3.4 TopK 和阈值的调参逻辑TopK 和阈值是检索路由最容易拍脑袋的两个参数。TopK 不是越大越好。TopK 太大后续精排或人工兜底要处理的候选太多TopK 太小正确技能可能根本进不了候选。一般从 5 开始调观察 Top5 召回率。如果正确技能经常出现在第 6 到第 10 位说明技能描述区分度不足先优化描述而不是盲目调大 TopK。阈值的作用是判断“没有合适技能”。当最高分低于某个阈值时不要硬选一个技能而是返回“需要澄清”或“建议用户换个说法”。阈值标定依赖离线评测集没有评测集就人工标注几百条请求画出分数分布选一个能覆盖绝大多数正例又不引入明显误召回的分数。参数含义常见值调大影响调小影响top_k召回候选数量5 到 10精排成本上升召回率提高可能漏掉正确技能sim_threshold向量相似度阈值0.5 到 0.7拒绝更多请求澄清率升高误召回增加keyword_weight关键词权重大小0.2 到 0.4更偏字面匹配更偏语义匹配4. 大模型路由的最小落地实现4.1 用 function calling 让模型选技能大模型路由最常见的方式是使用兼容 OpenAI 协议的 tools 接口。把技能定义成 function模型在回答中返回工具调用信息。tools [ { type: function, function: { name: order_query, description: 查询用户的历史订单、物流进度、预计送达时间, parameters: { type: object, properties: { order_id: {type: string} } } } }, { type: function, function: { name: weather_query, description: 查询指定城市的天气、温度、降雨概率、风力, parameters: { type: object, properties: { city: {type: string} } } } } ] response llm.chat( modelyour-model, messages[{role: user, content: 我的手机什么时候到}], toolstools, tool_choiceauto, ) tool_calls response.choices[0].message.tool_calls这段代码在技能少时跑得很顺但要注意两点。第一tools 列表一旦很长每次请求都会把全部参数 schema 发给模型Token 消耗不可忽略。第二工具名和参数名过长也会占用上下文建议技能名短、参数结构简单。4.2 用结构化输出约束路由结果如果接入的模型不支持 function calling可以改成结构化输出让模型返回 JSON再用代码校验。{ skill_name: order_query, arguments: { order_id: 20250115001 } }拿到 JSON 后不要直接信任必须做三层校验技能名是否在注册表中、参数是否符合 schema、用户是否有权限。校验失败就重新采样一次或走兜底逻辑。SKILL_REGISTRY {s[name]: s for s in load_skills()} def validate_routing_result(result: dict) - bool: name result.get(skill_name) skill SKILL_REGISTRY.get(name) if skill is None: return False arguments result.get(arguments, {}) required set(skill.get(required_params, [])) return required.issubset(arguments.keys())结构化输出的优点是结果可解析、可校验缺点是模型仍然可能生成非法 JSON 或非法技能名。因此生产环境一定要有解析异常处理不能假设模型永远输出合法内容。4.3 大模型路由的失败形态和校验方式大模型路由常见的失败形态有三种。第一种是输出不存在的技能名。原因是模型把相似技能名记混了或者技能列表太长导致幻觉。解决办法是校验后触发一次性重试重试仍失败就回退到澄清。第二种是参数缺少必填项。模型选对了技能但没提取出所有必填参数。解决方案是在技能 schema 里写清楚每个参数的说明和示例值并在路由结果校验失败时反问用户补充信息。第三种是用户意图边界模糊时模型硬选。比如用户只问“怎么申请退款”系统在“退款政策查询”和“创建退款申请”之间选错了。这种情况不只是模型问题技能边界本身就需要在描述里写明白“本技能只查询政策不创建申请”。5. 生产环境更推荐混合路由5.1 两级流水线召回、精排、兜底单靠检索或单靠模型都有明显短板生产环境更常见的是混合路由分成三级。第一级是召回。用向量检索、BM25、关键词、元数据过滤多路并行把几百个技能压缩到 5 到 10 个候选。这一级要求快延迟控制住同时保证正确技能大概率在候选里。第二级是精排。把候选技能的描述、参数 schema 和用户请求、历史对话一起交给大模型让模型从中选一个。由于候选数量小模型判断准确率比从全量技能里选高得多Token 消耗也可控。第三级是兜底。无论召回还是精排失败都要有明确处理路径澄清请求、推荐相近技能、返回人工客服入口、记录失败样本。5.2 混合路由的代码骨架下面是一个不依赖具体框架的骨架实现def route(query: str, user_id: str) - RoutingResult: # 第一级先做权限过滤和多路召回 allowed_skills filter_by_permission(user_id, skill_index) candidates recall(query, allowed_skills, top_k5) if not candidates: return RoutingResult(actionclarify, reasonno_candidate) # 第二级大模型从候选集里精排 chosen rerank_with_llm(query, candidates) if chosen is None or not validate_routing_result(chosen): return RoutingResult(actionfallback, reasoninvalid_selection) # 第三级分发执行失败走兜底 return dispatch(chosen)召回函数里可以同时返回每个候选的分数方便日志排查。精排函数在模型返回为空或非法时不要原地重试太多次一次重试后直接兜底避免把单个请求的延迟无限拉高。5.3 回退策略与异常处理混合路由的回退策略要覆盖四个场景召回为空、精排非法、参数校验失败、技能执行失败。召回为空时不要直接告诉用户“没有这个功能”而是返回一个澄清话术列出当前可用技能分类引导用户重新表达。精排非法时可以先做一次“只返回合法技能名”的重试。如果重试仍然非法说明模型对候选技能边界理解有问题把这次请求计入失败样本后续用于优化技能描述。技能执行失败时需要区分是参数错误还是技能本身不稳定。参数错误可以回传给模型修正技能内部异常则要记录错误码返回用户友好提示。注意路由的兜底逻辑要和正常业务异常分开。路由失败不等于业务失败要单独记录路由错误码方便统计路由链路的质量。5.4 路由日志和指标采集路由日志至少要包含以下字段字段说明request_id一次完整请求的链路 IDquery路由阶段使用的用户输入user_id / tenant用户和租户维度candidates候选技能及召回分数route_result最终选择的技能名route_reason选择依据或失败原因llm_latency_ms精排模型调用耗时retrieval_latency_ms召回阶段耗时statussuccess / clarify / fallback / error有了日志可以持续统计路由准确率、召回率、澄清率、兜底率和 P95 延迟。这些指标不仅是线上监控的抓手也是做路由策略优化的数据来源。6. 常见问题排查表6.1 路由选错技能现象用户请求明显应该触发 A 技能系统却路由到了 B 技能。排查顺序查看日志中 A、B 两个技能在召回阶段的分数差。如果 A 的分数和 B 非常接近说明技能描述区分度不足。检查两个技能的关键词是否有大量重叠。例如“退款申请”和“退款查询”都有“退款”一个关键词。检查精排阶段的候选里是否包含 A。如果 A 根本没进候选问题在召回如果 A 在候选但模型选了 B问题在精排。处理方式分别对应优化技能描述、削减重叠关键词、补充边界说明。6.2 模型输出不存在的技能名现象精排结果里 skill_name 不在注册表中。原因候选技能描述里可能出现相似名称模型混淆后生成了新名字也可能是结构化输出格式错误。处理先做注册表校验校验失败重新采样一次再失败走兜底。长期看要检查候选技能名称是否过于相似必要时给技能名加上业务前缀例如 order_query_refund 和 order_query_progress。6.3 路由链路变慢现象单次路由耗时从 200 毫秒涨到 1 秒以上。可能点有三个召回阶段同时调用了多个 embedding 服务出现串行等待精排阶段 prompt 太长模型生成耗时上升兜底阶段反复重试。处理召回多路并行化精排只传候选技能的最小必要字段重试次数限制为一次并给整个路由链路设置超时阈值。6.4 技能数量增长后准确率下滑现象技能从 20 个增加到 80 个后Top1 准确率明显下降。这种情况说明单级路由已经承受不住技能规模。不要继续往 prompt 里塞更多技能而是改成检索召回加模型精排并把技能按业务域分组支持在召回阶段按分组先过滤。问题现象可能原因检查方式处理建议路由选错技能技能描述或关键词区分度不足对比候选分数和关键词重叠度重写描述削减重叠关键词输出非法技能名模型混淆或结构化输出异常日志中 skill_name 是否在注册表校验加重试技能名加前缀路由延迟升高串行召回、prompt 过长、重试过多查看 retrieval_latency_ms 和 llm_latency_ms并行召回精简 prompt限制重试技能增加后准确率下降单级路由承载过多候选按召回率和 TopK 分布评估引入两级路由技能分组7. 面试怎么回答这个问题7.1 先给结论再给判断依据面试官问“技能路由该用检索还是大模型”不要一上来就二选一。推荐的回答结构是先给结论不建议无脑让大模型从全量技能里选。正确做法是根据技能数量和技能相似度选择方案生产环境更常用“检索召回 大模型精排 规则兜底”的混合路由。再给判断依据从四个维度展开技能数量、技能相似度、延迟和成本、可控性。技能数量少比如 10 个以内直接让模型选没问题技能数量多必须做召回。技能之间描述高度相似要先用检索拉出候选再让模型做区分。路由本身对延迟和成本敏感就要控制精排调用次数和 prompt 长度。可解释性要求高检索阶段的分数和日志能提供审计依据。7.2 按场景拆解不要只背方案面试官可能会追问“那你们线上到底用的哪种”。这时候要把场景和方案绑定而不是背一个标准答案。可以说线上技能数量超过 50 个我们先把不可用技能按权限过滤掉再用向量加关键词多路召回拿到 Top5 候选然后把候选技能配置交给大模型精排最后做参数校验和权限校验。模型返回非法结果时重试一次再失败就返回澄清话术并记录失败样本。如果能补充离线评测数据比如 Top5 召回率、Top1 准确率、P95 延迟会更有说服力。没有真实数据时明确说明“当前方案是工程上的折中需要靠离线评测集持续调优”这是更符合实际工作的表达。7.3 容易被追问的细节这个问题延伸出来的追问通常有三个。第一个是“技能描述怎么写”。回答要点是描述要写清楚触发条件、能力边界、典型参数和反例避免所有技能都写成“查询 XX 信息”。第二个是“top_k 怎么定”。不要拍脑袋要说用离线评测集观察正确技能出现在第几位再结合成本定初始值线上监控召回率后再调整。第三个是“模型选错了怎么办”。要对齐兜底链路校验、重试、澄清、记录失败样本、持续优化技能描述。能说出这条闭环比只强调模型能力要专业得多。8. 落地前的检查清单和扩展方向8.1 上线前检查清单检查项检查内容技能注册表每个技能是否有唯一名称、清晰描述、关键词、参数 schema、权限标签权限过滤路由前是否按用户身份过滤了不可用技能召回策略是否配置了向量召回、关键词召回、元数据过滤多路通道阈值与 TopK是否基于评测集设置了阈值和候选数量精排校验模型输出是否经过注册表和参数 schema 校验兜底路径召回为空、精排非法、参数缺失是否都有明确处理日志指标是否记录路由候选、分数、结果、延迟和失败原因灰度开关是否支持检索、模型、混合策略之间切换8.2 离线评测和线上灰度路由上线前要建一个离线评测集格式是 query 加 expected_skill。评测集至少覆盖三类样本常规请求、边界请求、无技能匹配请求。常用指标包括 Top1 准确率、Top5 召回率、澄清率、兜底率。Top5 召回率反映召回阶段有没有漏掉正确技能Top1 准确率反映最终路由质量澄清率和兜底率反映系统在不确定时是否表现合理。线上不建议直接切全量而是新老策略灰度对比观察同一批请求下两种策略的路由分布和技能执行成功率。策略切换做成配置开关不要写死在代码里。8.3 可以继续深入的方向路由方案跑稳定后可以继续做三件事。第一把失败样本沉淀成评测集持续反向优化技能描述。模型选错往往不是模型不行而是技能描述没有把边界写清楚。第二对高频技能做路由缓存。同一类请求在短时间内重复出现时可以直接跳过模型精排降低延迟和成本。第三引入用户反馈闭环。路由结果不理想时用户可以表达“这不是我要的”把反馈计入数据每周复盘路由错误类型再决定优化描述、调整召回权重还是拆分技能边界。回到面试题本身真正专业的回答不是“用检索”或“用大模型”而是讲清楚路由问题是分层决策问题先用规则和检索缩小决策空间再用模型做精细判断最后用校验和兜底保证系统在异常情况下仍然可控。这套思路既能回答面试也能直接指导线上系统设计。