Agent技能路由:检索还是大模型?混合架构才是正解
2026/8/31 2:25:49 网站建设 项目流程

朋友们,我最近在看 Agent 相关架构时发现一个高频面试题:Agent技能路由用检索还是大模型?如果你去翻网上的答案,大概率翻不到几篇真正讲懂这个决策的文章,更多的是一句“让模型自己选”就带过了。

但技术方案从来不是二选一,高并发在线系统也不会允许你无脑交一个 Token 给模型去“顿悟”。这个问题的本质是:在一个多技能、多工具的 Agent 系统里,如何又快又准地把用户意图分发到正确的技能处理器上。想通了这一点,面试时你不但能给出完整方案,还能讲清楚权衡依据、混合策略和降级方案,后面我会逐步展开。

今天这篇文章,我会从概念拆起,给你一套可运转的技能路由实践方案,包括:

  • 检索式路由(向量检索 + 规则召回)的实现思路;
  • 大模型路由(LLM Router + 结构化输出)的配置方法;
  • 混合路由器架构与回退策略;
  • 以及线上性能观测和避坑建议。

无论你是正在做 Agent 应用开发,还是准备面试,这篇文章都值得收藏。

1. 背景与核心概念

1.1 什么是 Agent 技能路由

翻译成大白话就是:系统里挂了很多个技能模块,比如查天气、写代码、搜专利、办工单、调数据库。用户来了一句请求后,系统先判断这句话该调用哪个技能模块,再把请求转发过去,这个过程就是技能路由。

举个例子:

  • 用户说“今天北京天气怎么样?” → 路由到天气查询技能;
  • 用户说“帮我把这段 Python 代码改成 Java” → 路由到代码转换技能;
  • 用户说“查一下当前订单审批卡在哪个节点” → 路由到工单/流程查询技能。

如果只有两三个技能,硬编码规则也能扛;但到了几十个技能规模时,路由就必须可扩展、可维护、可观测。于是就有了检索式路由和大模型路由这两个流派。

1.2 路由问题为什么面试高频

面试官问这个问题,通常不是考你会不会调接口,而是看你能不能把意图识别、检索系统、大模型调度这三件事组合起来。一个合格的 Agent 架构师必须回答清楚:

  • 为什么不能无脑“让模型自己选”?
  • 为什么不能全量规则写死?
  • 如何让路由准确率、延迟、成本可量化?

如果你只回答“让模型自己选”,表面上是把决策权交给了大模型,实际上是放弃了:可控性、可观测性和成本管理。面试官一听就知道你没做过高并发或生产级 Agent。

1.3 检索与大模型路由的本质区别

维度检索式路由大模型路由
核心机制通过向量相似度、关键词、规则找到最匹配技能利用模型语义理解直接输出技能 ID
延迟低,毫秒级较高,几百毫秒到秒级
成本低,近乎固定高,按 Token 计费
可解释性高,能看到匹配分数和候选排名较低,需要额外做溯源
扩展性新技能只需注册+向量化新技能需维护提示词和候选集
适合场景技能数量多、请求量大的线上系统意图复杂、规则难以覆盖的冷启动场景

你会发现,二者不是替代关系。很多生产级 Agent 系统采用检索为主 + 大模型兜底的混合路线,让低延迟需求和冷门长尾请求各得其所。

2. 检索式路由:先看匹配度再谈智能

2.1 为什么先说检索

结构化系统的第一原则是确定性优先。技能路由本质上是一个分类问题,分类问题最稳妥的做法是先把答案范围收敛出来,再决定要不要加智能处理。检索式路由的核心假设是:只要技能描述足够完整、向量空间足够好,大部分请求可以直接命中正确技能。

它的另一个天然优势是可审计。每条请求路由时,系统可以记录命中的技能、匹配分数、Top-K 候选,出现问题能快速归因。

2.2 最小实现步骤

假设你有以下技能:

skills = [ { "id": "weather_query", "name": "天气查询", "description": "查询任意城市的当前天气、未来 7 天预报、空气质量、紫外线指数等气象数据。", "keywords": ["天气", "温度", "降雨", "空气质量", "天气预报"], }, { "id": "code_convert", "name": "代码转换", "description": "将一段源代码从一种编程语言转换为另一种语言,例如 Python 转 Java、JavaScript 转 TypeScript。", "keywords": ["代码", "python", "java", "转换", "转"], }, { "id": "patent_search", "name": "专利检索", "description": "检索专利数据库,查找专利标题、摘要、申请人、公开号、法律状态等专利信息。", "keywords": ["专利", "申请号", "公开号", "patent", "发明"], }, ]

先做最简单 BM25 召回:

from rank_bm25 import BM25Okapi # 把技能描述和关键词拼接成可检索文本 corpus = [ f"{s['name']} {' '.join(s['keywords'])} {s['description']}" for s in skills ] tokenized_corpus = [doc.split() for doc in corpus] bm25 = BM25Okapi(tokenized_corpus) def bm25_route(query: str, top_k: int = 1): tokenized_query = query.split() scores = bm25.get_scores(tokenized_query) ranked = sorted(zip(scores, skills), key=lambda x: x[0], reverse=True) return ranked[:top_k] print(bm25_route("北京明天会下雨吗"))

输出结果:

[(5.31, {'id': 'weather_query', ...})]

这种简单的关键词召回能解决少量技能的冷启动问题,但要想处理同义改写、模糊表达,就需要升级为向量检索,把“下雨”和“降水概率”映射到相近的向量区间。

2.3 向量数据库选型与配置

生产环境我一般建议把技能描述离线向量化,存入向量数据库,例如 LiteLLM、Milvus、Qdrant、Chroma、pgvector 等,在线查询时通过 embedding 模型把用户请求编码成向量,再检索 Top-K。

以下是使用 Qdrant 的示意:

from qdrant_client import QdrantClient from qdrant_client.models import Distance, VectorParams, PointStruct client = QdrantClient(host="localhost", port=6333) COLLECTION_NAME = "skill_router" # 创建集合:向量维度需与 embedding 模型输出维度一致 client.recreate_collection( collection_name=COLLECTION_NAME, vectors_config=VectorParams(size=1024, distance=Distance.COSINE), ) # 写入技能向量 vectors = model.encode([f"{s['name']} {s['description']}" for s in skills]) points = PointStruct( id=idx, vector=vectors[idx].tolist(), payload=s, ) client.upsert(collection_name=COLLECTION_NAME, points=points)

在线的路由查询:

query_vec = model.encode([user_query]) hits = client.search( collection_name=COLLECTION_NAME, query_vector=query_vec[0].tolist(), limit=3, )

需要注意的坑点:

  1. 维度要匹配:更换 embedding 模型后,旧集合的向量维度会失效,需要重建集合并重新入库。
  2. 阈值要调:不要只看 Top-1,要设置最低得分阈值,得分过低时说明技能库里没有合适技能,进入兜底分支。
  3. 技能描述质量决定上限:向量检索对描述编写方式敏感,建议采用“功能 + 触发词 + 示例问法”三段式描述,比一句话简介效果好很多。

2.4 混合检索 + 多路召回

搜索引擎的经验放在技能路由里同样适用。只做向量检索有时会漏掉精确关键词,只做 BM25 又扛不住口语化表达。成熟的方案是向量检索 + BM25 多路召回,最后用 RRF(Reciprocal Rank Fusion)合并排序。

def reciprocal_rank_fusion(ranked_lists, k=60): scores = {} for ranked_list in ranked_lists: for rank, doc in enumerate(ranked_list): doc_id = doc["id"] scores[doc_id] = scores.get(doc_id, 0) + 1.0 / (k + rank + 1) return sorted(scores.items(), key=lambda x: x[1], reverse=True)

把向量 Top-20 和 BM25 Top-20 同时交给 RRF 合并,最终取 Top-5,再叠加规则过滤(例如“专”字出现时强制优先专利检索),召回质量和稳定性都能显著提升。

3. 大模型路由:给 Agent 装上判断力

3.1 为什么要用大模型路由

检索式路由的天花板在于:它只能在现有的技能描述里找关联,如果用户问法非常飘逸、绕弯子,或者技能之间边界模糊,检索得分就容易失真。这时候,大模型的语义理解和推理能力能帮上忙。

大模型路由最常见的实现方式是:

  1. 把技能清单和说明写进 System Prompt;
  2. 让模型输出一个 JSON,包含技能 ID 和置信度;
  3. 程序解析 JSON,决定调用哪个技能或进入兜底。

3.2 结构化 Prompt 示例

ROUTER_SYSTEM_PROMPT = """ 你是一个 Agent 技能路由器。根据用户输入,从以下技能中选择最合适的一个。 技能清单: {skill_list} 输出要求: 1. 只输出 JSON,不要输出多余文本。 2. JSON 格式:{{"skill_id": "xxx", "confidence": 0.0-1.0, "reason": "简短判断理由"}} 3. 如果没有合适技能,输出 {"skill_id": "fallback", "confidence": 0.0, "reason": "没有匹配技能"} 4. 一个用户输入只能选一个技能。 """

调用代码:

import json from openai import OpenAI client = OpenAI() def llm_route(user_query: str, skills: list) -> dict: skill_lines = "\n".join([ f"- {s['id']}: {s['name']}。功能:{s['description']}" for s in skills ]) resp = client.chat.completions.create( model="gpt-4o-mini", # 按实际环境调整 messages=[ {"role": "system", "content": ROUTER_SYSTEM_PROMPT.format(skill_list=skill_lines)}, {"role": "user", "content": user_query}, ], temperature=0, response_format={"type": "json_object"}, ) content = resp.choices[0].message.content return json.loads(content)

这段代码里最关键的不是 model_name,而是temperature=0response_format={"type": "json_object"}

  • temperature=0 是让输出尽量确定,尽量不发散;
  • 结构化输出是让下游解析稳定,不背解析的锅。

3.3 大模型作为校验器的进阶玩法

大模型路由不一定要做“第一棒”。更稳的模式是让检索先出候选,大模型只做重排/校验:

  • 检索 Top-5 候选送到大模型;
  • 大模型在候选技能里二选一或三选一;
  • 如果候选都不合适,大模型给出“无匹配”,走兜底流程。

这个模式的收益非常大:检索负责缩小范围、控制成本和延迟,大模型负责最后决策,准确率比纯检索高,延迟和成本比纯大模型路由低。我推荐生产系统按这个思路设计。

4. 混合路由架构与降级策略

4.1 推荐的系统流程

我构建过的 Agent 技能路由系统一般分三层:

第一层:规则层 - 精确关键词匹配(如 "专利"、"天气") - 强制路由(如用户明确指定技能名) 第二层:检索层 - 向量检索 + BM25 多路召回 + RRF 合并 - 命中阈值以上直接返回技能 第三层:模型层 - 检索未命中/低置信度时,交给大模型路由 - 大模型输出技能 ID 与置信度 - 置信度过低时走兜底流程(多轮澄清、FAQ、转人工)

伪代码:

def route(user_query: str, threshold=0.75): # 第一层:规则 forced = rule_route(user_query) if forced: return {"skill_id": forced, "engine": "rule"} # 第二层:检索 retrieved = retrieval_route(user_query) # 返回 (技能id, 分数) if retrieved.score >= threshold: return {"skill_id": retrieved.id, "engine": "retrieval"} # 第三层:大模型 llm_result = llm_route(user_query) if llm_result.confidence >= threshold: return {"skill_id": llm_result.skill_id, "engine": "llm"} return {"skill_id": "fallback", "engine": "fallback"}

4.2 为什么混合架构更适合生产

单一方案在生产里往往不抗造:

  • 纯规则:技能一多,规则维护成本爆炸;
  • 纯检索:长尾和模糊场景误判率高;
  • 纯大模型:成本高、延迟高、且大模型偶发的“幻觉技能 ID”非常致命;
  • 纯“让模型自己选”:你失去了对系统行为的控制边界,出了问题很难解释。

混合架构的优势在于:用规则保证高频确定场景的稳定性,用检索吃掉大部分常规请求,把最难的、最异常的交给大模型,同时设置回退和日志观测。

4.3 降级策略:路由挂了怎么办

线上系统必须有降级预案。常见的降级策略有:

组件状态策略
向量数据库不可用降级到 BM25 纯关键词路由
大模型不可用/超时走检索结果直接返回,即使置信度不高也返回 Top-1 并提示确认
检索分数都过低直接走兜底话术,不再调用大模型,避免把脏请求放大
全部不可用返回默认技能(如 FAQ 或人工客服)并记录异常

另一条非常重要的原则:路由链路必须设置超时时间,尤其是大模型调用。例如检索层 20ms 内完成,大模型层 1s-2s 超时,整体路由时间控制在 2s 以内,防止技能分发成为整个 Agent 的瓶颈。

5. 实战:一个完整的技能路由 Demo

下面我们用 Python 写一个最小的混合路由服务,技能还是刚才那三个。你可以直接复制到本地跑通。

5.1 项目结构

skill-router-demo/ ├── main.py # 入口脚本 ├── skills.py # 技能注册表 ├── router.py # 路由逻辑 └── requirements.txt # 依赖

5.2 技能注册表 skills.py

SKILLS = [ { "id": "weather_query", "name": "天气查询", "description": "查询任意城市的当前天气、未来 7 天预报、空气质量、紫外线指数等气象数据。", "keywords": ["天气", "温度", "降雨", "空气质量", "天气预报", "下雪"], }, { "id": "code_convert", "name": "代码转换", "description": "将一段源代码从一种编程语言转换为另一种语言,例如 Python 转 Java、JavaScript 转 TypeScript。", "keywords": ["代码", "python", "java", "javascript", "typescript", "转换", "翻译代码"], }, { "id": "patent_search", "name": "专利检索", "description": "检索专利数据库,查找专利标题、摘要、申请人、公开号、法律状态等专利信息。", "keywords": ["专利", "申请号", "公开号", "patent", "发明", "知识产权"], }, ] FALLBACK_SKILL_ID = "fallback"

5.3 路由逻辑 router.py

import json from rank_bm25 import BM25Okapi from openai import OpenAI class SkillRouter: def __init__(self, skills): self.skills = skills corpus = [ f"{s['name']} {' '.join(s['keywords'])} {s['description']}" for s in skills ] tokenized_corpus = [doc.split() for doc in corpus] self.bm25 = BM25Okapi(tokenized_corpus) self.client = OpenAI() def rule_route(self, query: str): """第一层:规则强制命中""" for skill in self.skills: for kw in skill["keywords"]: if kw in query: return skill["id"] return None def retrieval_route(self, query: str, threshold: float = 1.2): """第二层:BM25 检索命中(生产上可替换为向量检索)""" tokenized_query = query.split() scores = self.bm25.get_scores(tokenized_query) ranked = sorted( zip(scores, self.skills), key=lambda x: x[0], reverse=True ) best_score, best_skill = ranked[0] if best_score >= threshold: return best_skill["id"], best_score return None, best_score def llm_route(self, query: str): """第三层:大模型路由""" skill_lines = "\n".join([ f"- {s['id']}: {s['name']}。功能:{s['description']}" for s in self.skills ]) system_prompt = f""" 你是一个 Agent 技能路由器。根据用户输入,从以下技能中选择最合适的一个。 技能清单: {skill_lines} 输出要求: 1. 只输出 JSON,不要输出多余文本。 2. JSON 格式:{{"skill_id": "xxx", "confidence": 0.0-1.0, "reason": "简短判断理由"}} 3. 如果没有合适技能,输出 {{"skill_id": "fallback", "confidence": 0.0, "reason": "没有匹配技能"}} """ resp = self.client.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": query}, ], temperature=0, response_format={"type": "json_object"}, ) return json.loads(resp.choices[0].message.content) def route(self, query: str): forced = self.rule_route(query) if forced: return {"skill_id": forced, "engine": "rule"} best_id, best_score = self.retrieval_route(query) if best_id: return {"skill_id": best_id, "engine": "retrieval", "score": best_score} llm_result = self.llm_route(query) skill_id = llm_result.get("skill_id") confidence = llm_result.get("confidence", 0.0) if skill_id and skill_id != FALLBACK_SKILL_ID and confidence >= 0.6: return {"skill_id": skill_id, "engine": "llm", "confidence": confidence} return {"skill_id": FALLBACK_SKILL_ID, "engine": "fallback"}

5.4 入口测试 main.py

from skills import SKILLS from router import SkillRouter router = SkillRouter(SKILLS) test_queries = [ "上海明天降温吗", "把下面这段 Python 代码转成 Java", "帮我查一下华为申请的专利公开号", "今天心情不错推荐一部电影", ] for q in test_queries: result = router.route(q) print(f"Query: {q}\nRoute -> {result}\n")

5.5 运行结果与解释

Query: 上海明天降温吗 Route -> {'skill_id': 'weather_query', 'engine': 'rule'} Query: 把下面这段 Python 代码转成 Java Route -> {'skill_id': 'code_convert', 'engine': 'rule'} Query: 帮我查一下华为申请的专利公开号 Route -> {'skill_id': 'patent_search', 'engine': 'retrieval', 'score': ...} Query: 今天心情不错推荐一部电影 Route -> {'skill_id': 'fallback', 'engine': 'fallback'}

前两个因为关键词命中直接走规则层;第三个通过检索分数命中;第四个既没有关键词、检索分数也不高,于是交给大模型。如果大模型也认为没有匹配技能,就回退到 fallback。这就是一个完整的闭环。

需要注意的是,上面用了 BM25 模拟检索层,真实项目建议换用向量检索,召回效果会好很多。

6. 常见问题与排查思路

6.1 路由命中错误技能

问题现象常见原因解决思路
“查下深圳天气”命中专利检索技能描述中关键词重叠或向量空间纠缠检查技能描述,尽量减少歧义词;增加测试集做回归
长句子命中多个技能多路召回时阈值过低调高检索阈值,或者让大模型做二选一
新增技能后旧请求全部误路由新技能的描述风格差异大统一技能描述格式,做好语义相似度检查

排查思路很简单:把路由的每一层日志打出来,看看是规则层、检索层还是模型层误判,再针对那一层做优化。不要把问题混在一起排查。

6.2 延迟和成本超预期

问题现象常见原因解决思路
路由平均耗时 2 秒以上大模型调用成为主要瓶颈加缓存、加超时、提升检索层命中率
大模型调用量占比高检索阈值设置偏高降低阈值,让更多请求在检索层闭环
检索结果不稳定embedding 模型频繁切换固定 embedding 模型版本

6.3 大模型输出 JSON 解析失败

可能原因:

  1. 模型未启用 JSON Mode;
  2. 模型返回了额外解释文字;
  3. 提示词里 JSON 示例格式有问题。

解决:

# 解析失败时做一次容错 try: result = json.loads(content) except json.JSONDecodeError: # 尝试提取大括号部分 start = content.find("{") end = content.rfind("}") + 1 result = json.loads(content[start:end])

但更推荐从源头解决:response_format={"type": "json_object"}或使用函数调用(Function Calling)强制结构化输出。

7. 最佳实践与工程建议

7.1 技能描述是路由质量的地基

无论你最终选择检索还是大模型,技能描述的质量直接影响路由准确率。建议每条技能描述统一为三段式:

功能定位(一句话) 具体能力(列 3-5 个细分能力点) 示例问法(提供真实用户可能说的 2-3 个问句)

示例:

技能ID: patent_search 功能定位: 查询专利信息和法律状态 具体能力: 支持按申请人、公开号、申请号、IPC分类号检索;可查询法律状态、同族专利、引证信息。 示例问法: - 帮我查一下华为最近公开的发明专利 - 查询专利公开号 CN123456789A 的法律状态

这样写的好处是:关键词维度能覆盖,语义向量空间的边界也更清晰。

7.2 建立路由评估集

很多 Agent 项目最大的问题是没有评估集,导致改一版路由,线上效果像开盲盒。我建议从第一天就维护一份路由评测集:

[ { "query": "帮我查一下华为申请的专利公开号", "expected_skill_id": "patent_search", "engine_hint": "any" }, { "query": "上海明天会下雨吗", "expected_skill_id": "weather_query", "engine_hint": "any" } ]

每次调整技能描述、更换 embedding 模型、改 Prompt 时,都跑一遍评测集,记录准确率变化。没有评估集的路由系统,优化就是玄学。

7.3 安全与权限边界

技能路由还承担着安全网关的角色,必须坚持最小权限原则:

  • 不同技能配置独立的权限校验,不要用一个 Agent 主令牌打通所有技能;
  • 路由层对用户输入做敏感词和注入检测,不能把恶意指令转发给高权限技能;
  • 生产环境变更路由策略时,先在灰度环境验证,再逐步放流量;
  • 涉及删除、写操作、生产数据的技能,路由命中后需要二次确认。

7.4 可观测性是最终底线

路由日志至少记录以下字段:

字段说明
request_id链路追踪 ID
user_id用户标识(脱敏)
query原始请求
engine命中引擎:rule / retrieval / llm / fallback
skill_id最终技能
score检索分数或模型置信度
latency_ms路由耗时
llm_reason大模型判断理由(如有)

有了这些数据,你可以定期分析:哪些请求到了 fallback?哪些技能老是抢别人的流量?哪些 query 耗时特别长?路由优化就能从感性决策变成数据决策。

8. 回到面试题:到底用检索还是大模型

最后回到开头那个问题。

面试官问“Agent技能路由该用检索还是大模型”,不是让你二选一,而是看你有没有架构思维。你可以这样回答:

技能路由不能简单二选一。我的方案是分层混合路由:第一层用规则处理确定性的显式指令;第二层用向量检索加 BM25 多路召回覆盖大多数常规请求,保证低延迟和低成本;第三层才用大模型处理长尾和模糊query,同时设置置信度阈值和 fallback。这样既控制了成本,也保住了准确性,还能通过日志观测逐步优化。

如果面试官继续追问“那如果只有一个选择呢”,你可以说:

如果资源充足、对延迟不敏感,可以先用纯大模型路由快速验证效果;如果技能量很大、请求量很高,那检索一定是主路由,大模型只做兜底。最推荐的做法是用评测集量化对比,而不是拍脑袋选型。

这样回答,既有方案又有取舍,还能体现你对成本和工程落地的理解,深度完全不一样。

希望这篇技能路由实战文章对你的 Agent 开发和面试准备有实际帮助。如果觉得内容有用,欢迎收藏备用;如果你在项目里实践了这套方案,欢迎在评论区聊聊你的路由命中率和踩坑经历。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询