当面试官问起 Agent 技能路由该用检索还是大模型时,很多人的第一反应是:直接让大模型自己选不就行了?我见过不少候选人这样回答,然后下一句就被追问得哑口无言。因为这不是一个“选边站”的问题,而是一个工程决策问题。真正合理的答案不是二选一,而是分层设计:检索负责确定性和效率,大模型负责复杂意图和兜底。
先别急着反驳。如果你负责过一个真实的上线 Agent,大概率遇到过这些场景:用户说“帮我算一下 23*17”,模型非要先分析一段再调计算器;用户说“设个明天早上 8 点的闹钟”,模型把提醒技能和闹钟技能搞混;技能列表从 20 个涨到 200 个之后,把所有技能描述塞进 prompt,模型开始自己编技能名。这些问题不是模型不够聪明,而是你把一个本可以用确定性方式解决的映射问题,硬生生变成了一个概率生成问题。
这篇文章不打算帮你背面试答案,而是把“技能路由”这个系统设计问题拆开讲清楚。看完你可以判断:什么场景该用检索,什么场景该用大模型,以及为什么“检索优先、模型兜底”才是大多数 Agent 的正确打开方式。
1. 先搞清楚“技能路由”到底在解决什么问题
1.1 技能路由不是搜索,是决策分发
Agent 系统里,技能路由通常出现在意图识别之后、工具调用之前。用户输入进来之后,系统要先判断“这句话应该交给哪个技能处理”。比如天气查询、定时提醒、订单查询、知识库问答、代码执行,每个都对应一个独立技能。把输入正确分发到对应技能,就是路由要做的事。
它和传统的信息检索不一样。检索解决的是“哪些文档和 query 相关”,输出是一堆候选;路由解决的是“这个输入应该由哪个处理模块执行”,输出是唯一决策或有序候选。你可以把路由想象成公司的前台:来访者说明来意后,前台要决定带到哪个部门。如果带错了,后面的部门能力再强也没用。
所以,路由的核心指标不是“检索到了相似内容”,而是“分发对了模块”。这也是为什么很多团队在做 Agent 时,一开始会把路由做成一个独立模块,而不是让模型在工具调用里自由发挥。
1.2 从“让模型自己选”的直觉陷阱说起
现在的大模型确实支持 function calling,你给它几个工具的 JSON Schema,它能输出应该调用哪个工具。这给人的直觉是:技能路由根本不用单独做,模型天然会选。
但这个直觉忽略了一个关键前提:function calling 是一个生成任务,不是映射任务。生成任务的特点是,模型会基于概率预测输出最可能的 token。当工具定义清晰、语义边界明显、工具数量少时,这个概率预测很准;一旦工具数量变多、工具描述之间出现重叠、用户表达比较口语化,模型就可能输出一个“看起来合理但实际不对”的工具名。
我见过一个典型例子:技能列表里有“日历查询”和“日程创建”两个工具,描述分别是“查询用户的日历安排”和“为用户创建新的日程”。用户说“看看我这个周末有没有空”,模型直接选中了“日程创建”,因为它把“有空的安排”理解成了要创建东西。这种错误在检索式路由里很容易避免,因为你会发现“看看有没有空”和“查询”更相关,并且规则层可以直接把“有没有空”“哪天有空”映射到日历查询。
1.3 为什么说“无脑让模型选”是错的
无脑让模型选有三个问题,每一个在上线后都会变成严重的事故。
第一,不可观测。模型给出了一个工具名,但你不容易知道它为什么选这个。它不会告诉你“因为我看到用户提到了周末”,你只能猜。一旦路由错误,排查链路就断了。
第二,不稳定。同一个用户输入,模型可能这次选技能 A,下次选技能 B。不是模型“变了”,而是采样温度、上下文长度、工具描述顺序都可能影响输出。放到生产环境里,这会导致用户体验完全无法预测。
第三,成本不可控。模型每次路由都是一次推理,延迟普遍在几百毫秒到秒级。如果有 10 个技能,你可能要传几千 token 的工具描述;如果有 100 个技能呢?要么放弃部分工具描述,要么被 token 和延迟拖死。
这里要区分一下:我并不是说大模型完全不能用于路由。我是说,不能“无脑”让它选。面试官想要的,恰恰是你对这三个问题的识别和应对,而不是一句“用模型选”。
2. 检索式路由和模型式路由的本质差异
2.1 检索式路由:规则、倒排索引、向量匹配、意图槽位
检索式路由的核心思路是:把用户输入和技能定义都变成可比较的内容,然后用确定性的方式做匹配。
最基础的是规则路由。关键词、正则、词典,都是规则。比如用户输入包含“天气”且包含城市名,就路由到天气技能。规则的好处是快、可控、好解释,缺点是覆盖不了自然语言的变体。用户说“今天适合出门吗”,你如果不维护“适合出门”这种口语化关键词,规则就会漏。
再进一步是倒排索引和 BM25。把每个技能的描述拆成关键词,建一个倒排索引,用户 query 进来后计算相关性得分。BM25 对精确词匹配友好,所以它适合你明确知道用户会怎么说的场景,比如“查询订单”“查快递”。
然后是向量检索。将 query 和技能描述都做 embedding,计算余弦相似度,取 top-k。向量检索的优点是能处理语义相似但用词不同的表达,比如“今天适合出门吗”和“天气”在语义上相关。缺点是需要维护 embedding 模型和向量库,并且相似度分数不是一个绝对可解释的“概率”。
实际工程中,大家通常会用“多路召回”这套思路:既有 BM25 的字面匹配,又有向量的语义匹配,再做结果融合。这也是很多人提过的“向量混合检索加 BM25 多路召回”。它不是某个特定库的名字,而是一种常见工程组合。
2.2 大模型路由:自然语言理解、开放意图、复杂场景
大模型路由的本质,是把“选择哪个技能”变成一个阅读理解加推理任务。你给模型一段用户输入、技能列表和约束,让它输出技能 ID。
模型的优势在于,它能处理很复杂的意图。比如用户说“帮我订一个周三下午能顺便见客户的商务餐厅”,这里至少包含“订餐厅”“筛选时间”“结合客户位置”三个隐含动作。如果用规则和检索,你需要提前定义大量槽位和组合逻辑;用模型,它可以从语义里推断出应该调用“餐厅预订”技能,甚至可以把“周三下午”“客户位置”作为参数提取出来。
模型还擅长多轮上下文。用户上一轮说“帮我查一下北京的天气”,这一轮问“那上海呢”,模型结合上下文知道这还是天气查询;检索式路由如果只处理当前轮,就会丢失“上海也要查天气”这个隐含信息。
2.3 核心维度对比
| 维度 | 检索式路由 | 大模型路由 |
|---|---|---|
| 延迟 | 通常毫秒级 | 几百毫秒到秒级 |
| 成本 | 几乎为零 | 每次推理都有 token 成本 |
| 可控性 | 规则明确,可精确调整 | 依赖模型输出,结果概率性 |
| 可解释性 | 能追溯到命中的关键词/分数 | 只能看到“模型认为” |
| 长尾/开放意图 | 弱,需要维护特征 | 强,能泛化 |
| 技能数量很多时 | 用索引扩展,性能稳定 | Prompt 膨胀,选择准确率下降 |
| 维护方式 | 维护规则、描述、索引 | 维护样本、Prompt、评测集 |
这个表格看起来是“各有优势”,但注意最后一行的“维护方式”:检索式路由更接近传统工程,大模型路由更接近机器学习项目。你如果所在团队缺少算法经验,就要格外慎重地让模型承担路由主责。
3. 实际落地时怎么选:先走确定性,再上模型
3.1 第一步:把技能定义成可检索的路由表
不要直接写代码调模型。先把所有技能整理成一个结构化路由表,这是整个方案的地基。
每个技能至少需要这些字段:
{ "skills": [ { "id": "weather_query", "name": "天气查询", "description": "查询指定城市当前天气和未来几天天气预报", "keywords": ["天气", "气温", "下雨", "降温", "适合出门"], "required_entities": ["city"], "domain": "生活服务" }, { "id": "alarm_set", "name": "设置闹钟", "description": "为用户设置一个指定时间的闹钟提醒", "keywords": ["闹钟", "叫我", "提醒我", "起床"], "required_entities": ["time"], "domain": "生活服务" } ] }这个路由表不只是给程序看的,也是给大模型看的。后面所有检索和模型推理,都依赖 description 写得准不准确。描述里要写“这个技能负责什么”,而不是“这个技能能够做什么”。负责什么更接近意图边界,能够做什么容易让模型误选。
3.2 第二步:用规则处理高频确定意图
上线最怕的是把所有流量都丢给路由模块。建议先看历史日志,找出出现频率最高的 5 到 10 个意图,用规则直接命中。
比如“闹钟”技能,你可以维护一个正则集合:
import re def find_skill_by_rules(text: str): pattern = r"(设置|定|调).{0,4}(闹钟|提醒|叫我)" if re.search(pattern, text): return "alarm_set" return None这里要注意两点。第一,规则要留别名和同义词,比如“叫醒我”“喊我起床”都应该进闹钟。第二,规则不等于“最好”,它是为了守住高频带。真正的长尾,才交给后面的层。
为什么先上规则?因为规则层可以做到零延迟、零成本、完全可解释。你不需要模型来解释为什么“设置闹钟”匹配到了 alarm_set,正则就是证据。用规则兜住 70% 的高频流量,剩下 30% 才需要更复杂的路由逻辑。
3.3 第三步:用向量检索召回应题
当规则层没有命中,就可以进入召回层。将用户 query 和路由表中的技能描述分别做 embedding,计算相似度。Python 里的大致过程是:
from sentence_transformers import SentenceTransformer model = SentenceTransformer("your-embedding-model") query_emb = model.encode("明天早上叫我起床") desc_emb = model.encode("为用户设置一个指定时间的闹钟提醒") similarity = query_emb @ desc_emb.T这里有两个关键问题。
第一,embedding 模型不是越贵越好,而是越匹配你的领域分布越好。通用模型在电商、客服、办公场景里表现差异很大,一定要拿真实 query 样本去验证。
第二,不要只用向量检索。我建议做 BM25 加向量检索的多路召回,因为两路补充:BM25 擅长精确词匹配,向量擅长语义泛化。你可以各取 top 20,然后按加权分数融合。这个融合函数一开始用简单加权就够了,不要一上来就上模型排序。
3.4 第四步:不确定/复杂意图才交给大模型,并设置兜底
召回层会给你一个带分数的候选列表。这时候你该做阈值判断,而不是直接把候选全部丢给大模型。
我的经验是分三段处理:
- 如果 top1 分数大于 0.9,且领先第二名较大,直接返回 top1,不调用大模型。
- 如果 top1 分数在 0.6 到 0.9 之间,说明有点模糊,但还不是完全无信号,可以把 top3 候选交给大模型做最终裁决。
- 如果 top1 分数低于 0.6,说明用户意图很可能不在技能表里,不要硬选,走兜底流程。
注意:0.9 和 0.6 不是标准值,不同 embedding 模型的分数分布差异很大。上线前要拿一批标注数据画出分数分布,再确定阈值。
兜底流程通常包括:返回“我没理解你的意思,请换一种说法”,或者推荐几个可能相关的技能让用户选择。硬选一个错误技能,比不选择更糟糕。
4. 一个可复用的混合路由设计
4.1 四层路由架构
把前面的步骤汇总,就是一个四层路由架构:规则层、召回层、裁决层、兜底层。
规则层处理高确定性意图,召回层缩小候选范围,裁决层做模糊意图终选,兜底层保护不发生错误分发。这四层不是并列关系,而是顺序执行的关系。每层都有出口,也都有转下层的条件。
用导航来类比:规则层像高速路主路,正常情况下直接到终点;召回层像下了高速后的城市道路,会经过多个路口;大模型裁决层像复杂环岛,需要交警人工指挥;兜底层像导航失效时,提醒司机停车问路。
4.2 特征工程和候选召回
在实际工程里,路由不能只看当前一句话。我建议把以下特征加入考虑:
- 归一化后的 query 文本:去掉标点、统一大小写、替换同义词。
- 对话上下文:前几轮的用户输入和系统响应,有时候单轮缺失关键信息。
- 用户静态属性:比如地域、会员等级,会影响技能候选。
- 会话状态:是否已经在某个技能内部,如果用户在“天气技能”里追问“那明天呢”,应该直接继续走天气技能,而不是重新路由。
候选召回这块,可以用一个函数把规则、BM25、向量检索统一起来:
def recall_skills(query, skill_table, top_k=20): candidates = set() candidates.update(rule_match(query, skill_table)) candidates.update(bm25_search(query, skill_table)) candidates.update(vector_search(query, skill_table, top_k)) return ranked_candidates(candidates)这个函数不要做得太重。先确保召回率足够高,后面裁决模型才有意义。如果召回层已经把正确技能排除了,大模型再怎么选也选不回来。
4.3 大模型裁决
大模型裁决的输入应该是“候选技能列表 + 用户输入 + 必要的上下文”,而不是全量技能表。全量技能表会让 Prompt 膨胀,模型容易看到自己更熟悉的技能名,产生误导。
一个常见的 Prompt 结构是:
给定用户输入和候选技能,请从候选中选择一个技能,并给出理由。 只能输出 JSON,格式为 {"skill_id": "...", "reason": "..."} 用户输入:{query} 候选技能: {skill_candidates}这时你可以强制要求模型输出 JSON,然后解析。要注意的是,模型输出不一定合法,一定要做格式校验和重试。我一般会加一个简单函数:
def llm_judge(query, candidates): result = llm_call(propmt(query, candidates)) try: return json.loads(result)["skill_id"] except: return fallback_rule(query, candidates)这个 fallback_rule 很重要。模型一旦拒绝输出、输出乱码、或者超时,你必须有一个规则兜底。比如返回候选里相似度最高的那一个,或者让用户确认。
4.4 兜底策略和异常处理
兜底不是一个简单的“未识别”,而是有层次的设计:
- 技能内部兜底:如果当前候选里有一个是默认技能,比如闲聊或通用问答,可以直接路由过去。
- 用户确认:返回“你是想查天气,还是设置提醒?”让用户选择。
- 人工接管:对于客服式 Agent,如果置信度极低,可以转人工客服。
- 全链路日志:把每一层的输入、候选、分数、最终选择、用户后续反馈记录下来。
这些日志不只是为了排查,更是为了迭代。你可以每周统计一次:哪些 query 被路由错了?错误发生在规则层、召回层还是模型裁决层?然后针对性更新。
5. 面试中怎么回答才能不踩坑
5.1 面试官真正想考察的东西
回到开头的面试场景。面试官问“Agent 技能路由该用检索还是大模型”,他大概率不是在找你背结论,而是看你脑子里有没有一个系统设计的框架。
他想知道:你能不能意识到大模型不是万能的?你能不能区分确定性逻辑和概率生成?你有没有处理过“技能数量变多之后路由退化”的问题?你会不会为了追求智能而牺牲可控性和可观测性?
如果你回答“用大模型自己选”,等于告诉对方:我把一个工程问题简化成了一个模型问题。很多做 Agent 的团队就是这样被坑的:Demo 阶段看起来很好,上线后用户问得稍微偏一点,路由就错,然后所有能力都跟着错。
5.2 回答框架和关键词
一个比较稳的回答框架是:
“我会先把技能整理成结构化路由表,用规则层处理高频确定意图;然后用 BM25 加向量检索做多路召回,候选控制在 top3 到 top5;只有候选之间分数接近、或者用户表达明显是多轮复杂意图时,才把候选交给大模型做最终裁决;最后保留置信度阈值和兜底流程,并把所有路由日志用于后续评测和更新。”
这句话覆盖了五个关键词:结构化路由表、规则层、多路召回、模型兜底、可观测性。面试官大概率会顺着这些关键词继续问,而你只要把每一层为什么存在讲清楚,就比只回答“混合”要高一个层次。
5.3 常见追问和应对
追问一:如果大模型选错了怎么办?
回答要点:不要只靠模型。先看日志定位是哪一层的问题,是召回层没把正确答案召回来,还是模型裁决错了。然后更新技能描述、调整阈值、补充规则或增加负面样本。一句话:模型选错不可怕,可怕的是没有观测和回流机制。
追问二:技能有 100 个甚至 1000 个,怎么处理?
回答要点:把所有技能描述塞进 prompt 是不现实的。要靠索引和召回。先建技能描述的倒排索引和向量索引,query 进来先快速召回几十个候选,再用更精细的层裁决。技能多的时候,反而更需要检索优先,而不是模型优先。
追问三:有些意图必须用模型,比如“帮我综合分析一下”,怎么办?
回答要点:这是“复杂意图”场景,可以用模型,但要给模型限定输出范围,比如只能从可用的分析类技能里选。同时,这种高成本路由只用于长尾或高价值流量,而不是所有流量。
追问四:怎么评测路由效果?
回答要点:构建一个路由评测集,包含用户 query、期望技能 ID、场景标签。离线测准确率、召回率、延迟、成本、兜底率;线上加 A/B 或者按日志抽检。注意评测集要持续更新,把线上真实错误样本回流进去。
6. 真实项目中的边界与长期维护
6.1 适合检索优先的场景
如果满足以下条件,我建议先不要上大模型路由:
- 技能集合相对固定,比如 20 个以内。
- 用户输入命令感强,比如“查天气”“设闹钟”“查订单”。
- 延迟要求高,比如对话机器人要求秒回。
- 业务要求强可控,比如金融、医疗、客服领域。
这些场景里,规则和检索能覆盖掉绝大多数情况,而且出了问题可以快速定位和修复。
6.2 必须用模型判断的场景
也有一些场景,检索式路由很难独立承担:
- 用户表达高度开放,没有固定句式,比如“帮我安排一下这周的效率计划”。
- 需要多轮上下文,当前指令不完整,比如“换一个更近的”。
- 技能之间边界模糊,需要综合语义判断。
- 用户可能同时触发多个技能,需要做组合路由。
这些场景可以把大模型放进决策链路,但依然建议用候选召回限制范围,而不是让模型面向整个技能库做自由选择。
6.3 从规则到检索再到模型:演进不是替代
我的观点是:这不是一个“非此即彼”的选型,而是一个持续演进的路径。新系统往往从规则开始,因为规则最快、最透明;技能多了以后,引入向量检索和 BM25;再往后,当长尾意图占比明显增加,才把大模型作为一种裁决器放进去。
每一层增加,都不是为了替换上一层,而是在上一层覆盖不了的边界上补齐能力。这种“确定性护栏 + 模型智能”的结构,比“把所有希望押在模型上”要稳定得多。
6.4 排查链路:路由不准时先查什么
路由出问题的时候,不要直接调 Prompt,也不要先去骂模型。按这个顺序查:
- 先看现象:是漏路由(没命中任何技能)、错路由(命中了错误技能),还是不稳定(同一句话结果不同)?
- 再看输入: query 有没有被正常清洗?上下文有没有传对?实体有没有少提取?
- 再看规则层:是不是有规则误杀?高频意图有没有匹配到?规则优先级对不对?
- 再看召回层:BM25 和向量各自有没有命中正确技能?阈值是不是太严或太松?
- 再看模型层:候选列表是否正确?Prompt 有没有歧义?输出格式是否被正确解析?
- 最后看反馈层:有没有用户点了“不是这个”?这些反馈有没有回流成样本和规则?
这个排查顺序的核心是:先定位问题发生在哪一层,再去修那一层。否则你改 Prompt 可能白改,改阈值可能影响其他意图,最后越调越乱。
回到最开始的面试问题。真正让候选人拉开差距的,不是“知道检索”还是“知道大模型”,而是你能不能把这两者放进一个可演进、可观测、可维护的工程框架里。技能路由的本质是决策分发,不是模型炫技。先想清楚哪些问题需要智能判断,哪些问题只需要高效映射,答案自然就清楚了。