RAG 路由笔记:不是所有问题都该走同一条路

这篇主要讲什么

这一份 PDF 对应 Part 10 ~ Part 11,主题是 Routing,也就是路由。

通俗来讲,路由就是:

先判断这个问题属于哪一类,再决定把它交给哪一套资料、哪一个提示词、或者哪条处理链。

这背后的想法很朴素:

不同问题,适合走不同路径。硬让所有问题共用一套流程,往往会让系统又慢又不准。

1. 逻辑路由:先分类,再分流

原文里举了一个很典型的例子:

把用户问题分到 python_docsjs_docsgolang_docs 这样的不同资料源。

做法是让模型先输出一个结构化分类结果,然后系统根据分类结果,选择对应的数据源。

这件事的重点不在“分类”本身,而在“减少无效检索”。

如果一个 Python 问题被拿去 JavaScript 文档里搜,那后面再怎么生成也没意义。

我个人认为,逻辑路由特别适合下面这些场景:

  • 一个系统接了多个知识库
  • 不同业务线共用一个问答入口
  • 一部分问题该查文档,一部分问题该走工具调用

它本质上是在做第一层筛选。

2. 语义路由:看意思像谁,就走谁

除了显式分类,原文还讲了语义路由。

这个方法不是直接让模型判断标签,而是把不同提示词或者不同路线的“说明文本”也做成向量,再和用户问题做相似度比较。

谁更像,就选谁。

文中举的例子是:

  • 如果问题更像物理问题,就走 physics prompt
  • 如果问题更像数学问题,就走 math prompt

这和逻辑路由的差别在于:

逻辑路由更像“先给标签,再走分支”;
语义路由更像“看气质更接近哪条路”。

我觉得语义路由很适合边界没那么清晰的场景。因为有些问题很难硬分类,但可以比较“更接近哪种处理方式”。

这份内容的核心思想

我觉得核心思想可以概括成一句话:

RAG 不一定是一个总流程,很多时候更像一个交通枢纽。

用户的问题先进来,然后系统先判断它该去哪,再开始后面的检索和生成。

如果没有路由,系统就容易出现两个问题:

  • 什么都搜,成本高
  • 到处都搜,结果乱

我自己的理解

路由其实是在解决“规模变大以后怎么办”的问题。

一个很小的 RAG 系统,可能只有一个知识库、一套提示词、一个回答方式,那确实不需要路由。

但只要系统开始变复杂,比如:

  • 多个知识来源
  • 多种任务类型
  • 不同回答风格
  • 不同工具链

那路由几乎就变成必需品了。

我自己的看法是:

路由不是高级装饰,而是系统开始变大时的基本组织能力。

我会怎么理解“逻辑路由”和“语义路由”

如果用很通俗的话区分:

  • 逻辑路由:像前台分诊,先看你挂哪个科
  • 语义路由:像系统自己听你描述后,判断你更像哪个科

前者更稳定、可控、可解释;
后者更灵活,对模糊问题更友好。

真正上线时,我倾向于:

能规则判断的,尽量先规则判断;
规则不够用时,再引入语义判断。

因为这样更容易调试,也更容易查错。

实践里要注意什么

  • 路由标签不要设计得太细,不然分类容易抖
  • 不同路线之间职责要清楚,不然路由了也没意义
  • 最好保留路由结果,方便排查为什么答错
  • 对“模糊问题”要考虑兜底策略,别强行分错类

一句总结

这一篇让我最认可的地方是:它提醒我们,RAG 不只是“搜和答”,还要先决定“去哪里搜、按什么方式答”。系统一旦复杂起来,路由就是把混乱变成有组织流程的关键一步。

核心代码实现

方法 1:逻辑路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def route_by_llm(question):
prompt = """
你要判断这个问题属于哪类数据源:
- python_docs
- js_docs
- golang_docs
只返回类别名
"""
datasource = llm.invoke(prompt + question).strip()
return datasource

def answer_with_logical_route(question):
datasource = route_by_llm(question)

if datasource == "python_docs":
docs = python_retriever.invoke(question)
elif datasource == "js_docs":
docs = js_retriever.invoke(question)
else:
docs = golang_retriever.invoke(question)

return llm.invoke(build_prompt(question, docs))

方法 2:语义路由

1
2
3
4
5
6
7
8
9
10
11
12
route_templates = {
"physics": "你是一个善于回答物理问题的助手",
"math": "你是一个善于回答数学问题的助手",
}

route_vectors = embed_documents(route_templates.values())

def semantic_route(question):
q_vec = embed_query(question)
scores = cosine_similarity(q_vec, route_vectors)
best_route = argmax(scores)
return list(route_templates.keys())[best_route]

方法 3:路由后再执行对应链路

1
2
3
4
5
6
7
8
9
def answer_with_semantic_route(question):
route = semantic_route(question)

if route == "physics":
prompt = physics_prompt
else:
prompt = math_prompt

return llm.invoke(prompt.format(query=question))