引言

在构建复杂的 AI 应用时,我们常常需要根据用户查询的主题或意图,将请求分发到不同的处理模块。例如,一个客服机器人可能需要将技术问题转给技术专家、将账单问题转给财务系统、将投诉转给投诉处理部门。传统做法是使用关键词匹配或规则分类,但这些方法维护困难、泛化能力弱。

语义路由(Semantic Routing) 提供了一种更智能的方式:它将每个可能的目标(称为“路由”,Route)用一组代表性的参考语句(references) 来刻画,当用户查询到来时,系统计算查询与所有参考语句的语义相似度,并选择最匹配的一个或多个路由进行分发。

RedisVL 的 SemanticRouter 类基于 Redis 的向量搜索能力,实现了高效、可扩展的语义路由。它将每个参考语句转换为向量并存储在 Redis 中,查询时通过 KNN 搜索找到最相似的参考,再聚合到路由层面做出决策。

本指南将带你从零开始,掌握如何定义路由、初始化路由器、执行查询路由、动态管理路由引用,以及序列化配置。


前置条件

  • 已安装 RedisVL:pip install redisvl
  • 一个运行中的 Redis 实例(推荐 Redis 8+ 或 Redis Cloud)

概要

  • 定义包含参考语句和距离阈值的路由
  • 初始化和配置 SemanticRouter
  • 将查询路由到单个或多个匹配路由
  • 序列化和恢复路由器配置(JSON / YAML)
  • 动态添加、获取和删除路由引用

语义路由的工作原理

语义路由的核心是向量相似度搜索。其流程如下:

  1. 定义路由:每个路由有一个名称、一组参考语句(例如典型问题或关键词),以及一个距离阈值(决定匹配的宽松程度)。
  2. 向量化:所有参考语句通过嵌入模型(如 sentence-transformers/all-mpnet-base-v2)转换为高维向量,并存入 Redis 的向量索引中。
  3. 查询:当用户输入查询时,同样将其向量化,并在 Redis 中执行 KNN 搜索,找出距离最近的若干参考语句。
  4. 聚合与决策:根据找到的参考语句所属的路由,采用平均距离或最小距离等聚合方法,计算每个候选路由的最终得分(距离)。只有那些距离小于各自路由阈值的路由才会被采纳。

下图直观展示了这一流程:

是

否

用户查询

将查询向量化

在 Redis 中执行 KNN 搜索

得到 N 个最近的参考语句及其距离

按路由分组,聚合距离(平均/最小)

距离 < 路由阈值?

将该路由加入候选列表

丢弃

返回排序后的路由列表

这种方式的优势在于:

  • 语义理解:不依赖关键词,能处理同义表达。
  • 灵活阈值:每个路由可单独控制匹配严格程度。
  • 多路由支持:一个查询可以同时匹配多个路由(例如“AI 在篮球中的应用”既匹配技术也匹配体育)。

定义路由

每个路由由 Route 对象定义,关键属性包括:

  • name:路由的唯一标识。
  • references:字符串列表,代表该路由的语义覆盖范围。可以是问题、关键词或短语。
  • metadata:可选字典,用于存储额外信息(如优先级、分类标签)。
  • distance_threshold:余弦距离阈值(范围 0~2,越小越严格)。只有查询与路由中某个参考的距离小于该阈值时,才视为匹配。

下面定义三个路由:技术、体育、娱乐。

from redisvl.extensions.router import Route

technology = Route(
    name="technology",
    references=[
        "人工智能有哪些最新进展?",
        "给我讲讲最新的科技产品",
        "科技领域有什么新趋势?"
    ],
    metadata={"category": "tech", "priority": 1},
    distance_threshold=0.71
)

sports = Route(
    name="sports",
    references=[
        "昨晚谁赢了比赛?",
        "告诉我即将到来的体育赛事",
        "体育界最近有什么新闻?",
        "体育",
        "篮球和足球"
    ],
    metadata={"category": "sports", "priority": 2},
    distance_threshold=0.72
)

entertainment = Route(
    name="entertainment",
    references=[
        "现在最火的电影是什么?",
        "谁获得了最佳男演员奖?",
        "娱乐圈有什么新鲜事?"
    ],
    metadata={"category": "entertainment", "priority": 3},
    distance_threshold=0.7
)

说明:距离阈值需要根据实际应用调整。较高的阈值(如 0.72)会匹配更多查询,但可能误召;较低的阈值(如 0.7)更严格,召回率降低。


初始化 SemanticRouter

SemanticRouter 在初始化时会自动在 Redis 中创建索引(如果不存在)。你可以指定嵌入模型(默认使用 HFTextVectorizer,模型为 sentence-transformers/all-mpnet-base-v2)。

import os
from redisvl.extensions.router import SemanticRouter
from redisvl.utils.vectorize import HFTextVectorizer

os.environ["TOKENIZERS_PARALLELISM"] = "false"

router = SemanticRouter(
    name="topic-router",                          # Redis 索引名称
    vectorizer=HFTextVectorizer(),                # 嵌入模型
    routes=[technology, sports, entertainment],   # 初始路由列表
    redis_url="redis://localhost:6379",
    overwrite=True                                # 如存在同名索引则覆盖
)

初始化后,可以查看索引信息:

rvl index info -i topic-router

你会看到索引包含 reference(文本)、vector(向量)、route_name(标签)等字段。num_docs 等于所有参考语句的总数(此处为 11 条)。


简单路由查询

使用 router(query) 方法,返回最佳匹配的路由(RouteMatch 对象,包含名称和距离)。若无匹配,则返回 name=None。

# 匹配技术路由
route_match = router("你能告诉我人工智能的最新进展吗?")
print(route_match)  # RouteMatch(name='technology', distance=0.419...)

# 不匹配任何路由
route_match = router("外星人真的存在吗?")
print(route_match)  # RouteMatch(name=None, distance=None)

如果需要返回多个候选路由(按距离升序),使用 route_many 方法:

# 返回最多 3 个候选路由
matches = router.route_many("AI 如何在篮球中应用?", max_k=3)
for match in matches:
    print(match.name, match.distance)
# 可能输出: technology 0.556, sports 0.671

聚合方法(Aggregation Method)

当查询匹配到一个路由的多个参考时,需要将这些距离聚合为一个最终距离。SemanticRouter 支持两种聚合方式:

  • avg(默认):计算该路由所有匹配参考的平均距离。
  • min:只取最小的距离(即最接近的参考)。

选择 min 会让路由更容易被匹配(因为只要有一个参考足够接近即可),而 avg 则更稳健,能避免因个别噪音参考导致的误匹配。

你可以通过更新路由配置来切换聚合方法:

from redisvl.extensions.router import RoutingConfig
from redisvl.extensions.router.schema import DistanceAggregationMethod

router.update_routing_config(
    RoutingConfig(
        aggregation_method=DistanceAggregationMethod.min,
        max_k=3
    )
)

matches = router.route_many("勒布朗·詹姆斯")
print(matches)  # 可能只匹配 sports

序列化与恢复

你可以将路由器配置导出为字典或 YAML 文件,便于保存或跨环境迁移。

# 导出为字典
config_dict = router.to_dict()
print(config_dict.keys())  # 包含 name, routes, vectorizer, routing_config

# 从字典恢复
router2 = SemanticRouter.from_dict(config_dict, redis_url="redis://localhost:6379")

# 导出为 YAML 文件
router.to_yaml("router.yaml", overwrite=True)

# 从 YAML 恢复
router3 = SemanticRouter.from_yaml("router.yaml", redis_url="redis://localhost:6379")

恢复时,Redis 索引会自动重建(如果不存在)。


动态管理路由引用

在实际应用中,你可能需要动态添加或删除某个路由的参考语句,而不需要重建整个路由器。SemanticRouter 提供了相关方法。

添加参考

added_ids = router.add_route_references(
    route_name="technology",
    references=["最新 AI 趋势", "新科技产品"]
)
print(added_ids)  # 返回新增参考的 Redis key 列表

获取参考

你可以按路由名称或参考 ID 获取所有参考语句。

# 按路由名称获取
refs = router.get_route_references(route_name="technology")
for ref in refs:
    print(ref["reference"])

# 按参考 ID 获取(示例取第一个)
first_id = refs[0]["reference_id"]
single_ref = router.get_route_references(reference_ids=[first_id])

删除参考

可以按路由名称删除该路由下的所有参考,或按指定 ID 删除特定参考。

# 删除整个 sports 路由的参考
deleted_count = router.delete_route_references(route_name="sports")
print(f"删除了 {deleted_count} 条参考")

# 删除特定参考
deleted_count = router.delete_route_references(reference_ids=[first_id])
print(f"删除了 {deleted_count} 条参考")

清理资源

  • clear():清空索引中的所有参考数据,但保留索引结构。
  • delete():删除整个索引(包括数据结构),彻底移除。
router.clear()   # 清空数据
router.delete()  # 删除索引

总结

  • 原理:基于向量相似度搜索和聚合,实现灵活的语义路由。
  • 定义路由:通过参考语句和距离阈值刻画每个路由的语义范围。
  • 查询路由:支持单匹配和多匹配,可调节聚合方法(平均/最小)。
  • 动态管理:可以随时增删改路由参考,无需重建索引。
  • 可移植性:配置可序列化为字典或 YAML,方便版本控制和部署。

语义路由是构建智能对话系统、RAG 应用、多任务分发系统的核心组件。RedisVL 提供的高性能向量检索能力,让这一切变得简单而高效。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐