语义路由(Semantic Router)是一种用向量相似度做意图识别的请求分发技术:它不依赖关键词规则,而是把”用户这句话更像哪一类问题”变成向量距离问题,从而把请求精准路由到对应的 Agent、提示词或处理函数。本文用可运行的代码,带你从原理到生产落地掌握它。
一、什么是语义路由
传统路由靠”规则”:正则匹配关键词、if-else 判断意图、或者把整个问题丢给大模型让它自己决定调哪个工具。前两种 brittle(换个说法就失效),第三种又慢又贵。语义路由换了一个思路——把每一类意图,用几条”代表这句话”的样本(utterance)描述出来,再把用户的新句子和这些样本做向量相似度比较,谁最像就走哪条路。
它本质上是一个极轻量的”分类器”,但不需要训练:分类边界由你提供的样本和阈值决定,部署成本几乎为零,单次路由耗时通常在毫秒级(只做一次 embedding + 一次向量比对),比每次都调大模型便宜一两个数量级。
二、为什么需要语义路由
想象一个客服入口,用户可能问”怎么退款””这个接口报 500″”你们老板是谁”。如果只用关键词,用户写”钱怎么退回来”就匹配不到”退款”;如果用大模型实时判断,每天几百万次调用会很烧钱。语义路由在这两者之间取平衡:
- 比规则稳:同义、口语、错别字都能靠向量语义兜住;
- 比大模型快且省:不调用生成模型,只做一次 embedding;
- 可解释、可收敛:路由命中和兜底都看得见,方便迭代样本。
它和 Function Calling 工具调用 是不同层的事:路由负责”这句话该交给谁”,工具调用负责”这个人拿到问题后怎么调 API”。两者常常配合,但职责清晰,不要混为一谈。
三、核心原理:把”路由”变成向量相似度问题
3.1 路由层与话语样本
每个”路由层”(Route)由两样东西定义:一个名字(如 faq、tech_support、chitchat),以及若干条话语样本(utterance)——也就是”属于这一类的话通常怎么讲”。样本越多越典型,边界越准。
3.2 相似度计算与阈值
新句子进来后,系统把它和每个路由层的样本分别算余弦相似度,取最高分;只有超过设定阈值(如 0.78)才认为”命中”,否则走兜底路由。阈值调高更保守(少误判但易兜底),调低更激进(多命中但易错分),需要用真实流量校准。
四、快速上手:用 semantic-router 库实现意图分发
Python 生态里 semantic-router 把上面这套逻辑封装得很干净。下面用 OpenAI 兼容的 embedding 接口(你也可以换成任意本地或云端 embedding,参考 Embedding 模型选型)来跑:
from semantic_router import Route
from semantic_router.encoders import OpenAIEncoder
from semantic_router import SemanticRouter
# 1) 定义路由层与话语样本
faq = Route(
name="faq",
utterances=[
"怎么退款", "钱怎么退回来", "订单能取消吗",
"发票怎么开", "会员怎么退订",
],
)
tech = Route(
name="tech_support",
utterances=[
"接口返回 500 怎么办", "这个报错 Connection refused",
"SDK 初始化失败", "token 过期了怎么刷新",
],
)
chit = Route(name="chitchat", utterances=["你是谁", "今天天气真好", "哈哈"])
# 2) 选择编码器(这里用 OpenAI 兼容 embedding)
encoder = OpenAIEncoder(
api_key="sk-xxx",
model="text-embedding-3-small",
base_url="https://your-endpoint/v1",
)
# 3) 组装路由器
router = SemanticRouter(encoder=encoder, routes=[faq, tech, chit])
# 4) 路由一条新消息
res = router("我的订单想退款怎么操作")
print(res.name) # -> 'faq'
注意 router() 返回的是命中路由的名字;如果都不像,会返回 None,这正是我们接兜底逻辑的地方。
五、实战:把请求分发到不同处理器
真实系统里,路由结果要驱动不同的下游:FAQ 走知识库检索,技术支持走 Agent,闲聊直接回一句。下面是个最小可运行的分发骨架:
def handle(route_name: str, message: str) -> str:
if route_name == "faq":
# 走向量检索 + 答案生成,可复用 pgvector 知识库(见 pgvector 实战)
return faq_bot(message)
if route_name == "tech_support":
# 交给具备工具调用能力的 Agent(见 Function Calling 实战)
return tech_agent(message)
if route_name == "chitchat":
return "我是你的智能助手~有什么可以帮你?"
# 兜底:都不像,转人工或通用大模型
return fallback_llm(message)
def dispatch(message: str) -> str:
route = router(message)
name = route.name if route else "fallback"
print(f"[route] {name} <- {message}")
return handle(name, message)
print(dispatch("SDK 一直报初始化失败")) # tech_support
print(dispatch("天气不错啊")) # chitchat
print(dispatch("额额额随便说说")) # fallback
这样入口层就变成了一个可观测、可灰度的”交换机”:哪类流量高、哪类老兜底,日志里一目了然,也方便你后续给某个路由单独扩容或接 Agent 记忆 做多轮上下文。
六、进阶:多层路由与兜底策略
6.1 阈值与兜底路由
生产环境一定要保留兜底。两层经验值得记:第一,给一个显式的 fallback 路由(或 score_threshold),别让低置信度请求硬塞进某类;第二,兜底不要直接甩给最贵的大模型,先尝试”澄清提问”,实在不行再升级。这样能显著压住成本。
6.2 语义路由与 Function Calling 的边界
一个常见误区是把语义路由当成工具选择器。路由的粒度应该是”意图域”(客服/技术/闲聊),而不是”具体工具”(查订单/查天气)。工具选择留给下游 Agent 的 Function Calling 去解决——分层之后,每一层都更简单、更好测。
七、生产落地清单
三种常见路由方案各有取舍,按团队阶段选:
| 方案 | 实现成本 | 泛化能力 | 单次成本/延迟 | 适用场景 |
|---|---|---|---|---|
| 关键词/正则 | 低 | 差(同义失效) | 几乎为 0 | 意图极固定、流量小 |
| 语义路由(本文) | 中(写样本) | 好 | 一次 embedding,毫秒级 | 中高频、需降本 |
| 大模型直接判断 | 低(prompt) | 最好 | 高,数百毫秒 | 低频、复杂意图 |
落地时还有几条硬建议:① embedding 选稳定、维度合适的模型,一旦选定别轻易换,否则历史向量要重算(可存进 pgvector 统一管理);② 路由样本要”脏”一点,把真实口语、错别字放进去,比干净的书面语更抗造;③ 把路由命中率、兜底率、各路由 P95 延迟接进监控,作为迭代依据。
八、小结
语义路由把”意图识别”从脆弱的规则和昂贵的模型调用里解放出来:用少量样本 + 一次向量比对,就能实现稳定、快速、可控的请求分发。它最适合做入口层的”交换机”,再与 Function Calling、Agent 记忆、向量库组合成完整的 AI 应用骨架。下一篇可以聊聊如何把它和 Dify + Ollama 私有知识库 串起来做端到端落地。




