Semantic Router 语义路由实战:精准分发用户请求

语义路由(Semantic Router)是一种用向量相似度做意图识别的请求分发技术:它不依赖关键词规则,而是把”用户这句话更像哪一类问题”变成向量距离问题,从而把请求精准路由到对应的 Agent、提示词或处理函数。本文用可运行的代码,带你从原理到生产落地掌握它。

一、什么是语义路由

传统路由靠”规则”:正则匹配关键词、if-else 判断意图、或者把整个问题丢给大模型让它自己决定调哪个工具。前两种 brittle(换个说法就失效),第三种又慢又贵。语义路由换了一个思路——把每一类意图,用几条”代表这句话”的样本(utterance)描述出来,再把用户的新句子和这些样本做向量相似度比较,谁最像就走哪条路。

它本质上是一个极轻量的”分类器”,但不需要训练:分类边界由你提供的样本和阈值决定,部署成本几乎为零,单次路由耗时通常在毫秒级(只做一次 embedding + 一次向量比对),比每次都调大模型便宜一两个数量级。

二、为什么需要语义路由

想象一个客服入口,用户可能问”怎么退款””这个接口报 500″”你们老板是谁”。如果只用关键词,用户写”钱怎么退回来”就匹配不到”退款”;如果用大模型实时判断,每天几百万次调用会很烧钱。语义路由在这两者之间取平衡:

  • 比规则稳:同义、口语、错别字都能靠向量语义兜住;
  • 比大模型快且省:不调用生成模型,只做一次 embedding;
  • 可解释、可收敛:路由命中和兜底都看得见,方便迭代样本。

它和 Function Calling 工具调用 是不同层的事:路由负责”这句话该交给谁”,工具调用负责”这个人拿到问题后怎么调 API”。两者常常配合,但职责清晰,不要混为一谈。

三、核心原理:把”路由”变成向量相似度问题

3.1 路由层与话语样本

每个”路由层”(Route)由两样东西定义:一个名字(如 faqtech_supportchitchat),以及若干条话语样本(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 私有知识库 串起来做端到端落地。

上一篇 Nginx 413 文件上传失败排查与根治
下一篇 代码可读性实战:让人读懂比让机器跑通更重要