Router与融合

方法学库和语言教程库已经分开建索引。用户有时问「GAPDH 内参」,有时问「列表推导式」。把两个库的块混在一次 top-k 里,生成会被无关段落带偏。两条路:

  • 先选路:LLM 读各路 description,只打一条(或几条)Retriever——RouterRetriever
  • 条条跑再合并:多路都召回,用 RRF 合成名单——跨索引的 QueryFusionRetriever

前者省嵌入与噪声,后者不怕选错路、但费用和噪声都更高。

段末注释RouterRetriever 按选择器挑 RetrieverTool跨索引融合 与「同一语料上向量+BM25」都叫 Fusion,但选路条件不同。

左:站长扳道只放行一条;右:两路进漏斗做 RRF(科普示意)


1. 一句话定位

维度 内容
角色 知识层的多路调度:分流或融合,输出仍是 list[NodeWithScore]
输入 → 输出 query →(选路或全跑)→ 合并后的 NodeWithScore
典型调用入口 RouterRetriever(selector, retriever_tools)QueryFusionRetriever([r1, r2], num_queries=1)
与 LangChain / LangGraph 近邻是路由链 / EnsembleRetriever;有环的重试选路仍用 LangGraph 条件边

出现背景:单索引 as_retriever 假设「全世界只有一个抽屉」。多集合、多模态、事实问 vs 综述问,需要显式调度,而不是把 description 写进生成 prompt 碰运气。


2. 前置依赖与环境

1
2
3
pip install -U llama-index-core llama-index-llms-ollama llama-index-embeddings-ollama
ollama pull qwen3.5:9b
ollama pull nomic-embed-text
  • Router 的 selector 要 LLM;Fusion 在 num_queries=1不要 LLM
  • 本地模型用 LLMSingleSelector 比强依赖 function calling 的 Pydantic selector 更稳
  • await router.aretrieve(q) 与同步参数一致

3. 实现逻辑

Router(选路)

1
2
3
4
5
1. 每个子索引 as_retriever → RetrieverTool.from_defaults(description=...)
2. RouterRetriever(selector=LLMSingleSelector, retriever_tools=[...])
3. retrieve(q):selector 输出工具名
4. 只调用选中的 Retriever.retrieve
5. 返回该路的 NodeWithScore

Fusion(全跑)

1
2
3
4
1. 准备多路 Retriever(不同 index 或不同算法)
2. QueryFusionRetriever(retrievers, num_queries=1, mode="reciprocal_rerank")
3. 每路 retrieve 同一 query
4. RRF 合并、截断 similarity_top_k

字段级变形(Router)

1
2
3
4
5
query = "GAPDH 内参是什么"
→ selector 选择 name/description 含「分子生物学方法」的 tool
→ methods_retriever.retrieve(...)
[NodeWithScore(..., metadata={collection:"methods"}), ...]
不会出现 collection=lang 的块(选对的前提下)

4. 原理说明

主轴是:Router 把「选哪路」交给 LLM;Fusion 把「选哪路」变成「全都要,事后按名次加权」。

1
2
3
4
5
6
7
8
9
10
1. RetrieverTool 把 retriever + name + description 编成选择器可见的选项
2. LLMSingleSelector 调 Settings.llm(或传入的 llm),输出选项下标/名称
3. RouterRetriever 只执行命中 tool 的 retrieve;MultiSelector 可执行多路再拼接
4. 选择失败或 description 互相抄 → 走错库,分数看起来仍很高
5. Fusion 跳过第 2~3 步,对每个 retriever 调 retrieve
6. 若 num_queries>1:先用 LLM 生成额外 query,笛卡尔式放大调用次数
7. reciprocal_rerank:同一 node_id 在多路出现则 RRF 累加
8. 两路索引内容正交时 Fusion 会把「第二路的头名」抬进最终名单,即使与问题无关
少了 description 差异 → Router 几乎随机
Fusion 用于正交库且不问选路 → 噪声块稳定进 top-k

RetrieverTool.from_defaults(retriever, name=..., description=...) 出现在步骤 1。description 是给选择器的,不是给最终用户的。必须写清何时用 / 何时不用

LLMSingleSelector.from_defaults(llm=...) 出现在步骤 2。一次选一个。多路并行选 LLMMultiSelector

RouterRetriever 出现在步骤 3。输入输出与普通 Retriever 相同,可再塞进 QueryEngine。

QueryFusionRetriever 出现在步骤 5。跨不同语料库时要自问:是否真的该全跑。同一语料向量+BM25 的 Fusion 是混合召回,不是分流。

不要用 LangGraph 再画一个「选 Retriever」子图,除非选路失败要重试、要 HITL、要 checkpoint。


5. 最小可运行示例

1
pip install -U llama-index-core llama-index-llms-ollama llama-index-embeddings-ollama
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
from llama_index.core import Document, Settings, VectorStoreIndex
from llama_index.core.node_parser import SentenceSplitter
from llama_index.core.retrievers import QueryFusionRetriever, RouterRetriever
from llama_index.core.selectors import LLMSingleSelector
from llama_index.core.tools import RetrieverTool
from llama_index.embeddings.ollama import OllamaEmbedding
from llama_index.llms.ollama import Ollama

Settings.llm = Ollama(model="qwen3.5:9b", request_timeout=120.0, temperature=0)
Settings.embed_model = OllamaEmbedding(
model_name="nomic-embed-text",
base_url="http://localhost:11434",
)
splitter = SentenceSplitter(chunk_size=128, chunk_overlap=20)

methods_index = VectorStoreIndex.from_documents(
[Document(text="定量 PCR 以小鼠肝脏 GAPDH 为内参。实验对象为 C57BL/6 小鼠。",
doc_id="paper_001", metadata={"collection": "methods"})],
transformations=[splitter],
)
lang_index = VectorStoreIndex.from_documents(
[Document(text="Python 的列表推导式用一行从可迭代对象生成列表。",
doc_id="py_001", metadata={"collection": "lang"})],
transformations=[splitter],
)

methods_tool = RetrieverTool.from_defaults(
retriever=methods_index.as_retriever(similarity_top_k=2),
name="methods_search",
description="检索分子生物学实验方法、剂量、物种、内参。不要用于编程语法问题。",
)
lang_tool = RetrieverTool.from_defaults(
retriever=lang_index.as_retriever(similarity_top_k=2),
name="python_search",
description="检索 Python 语法与语言特性。不要用于实验动物或 PCR。",
)

# 关键参数:description 互斥;selector 用本地 LLM
router = RouterRetriever(
selector=LLMSingleSelector.from_defaults(llm=Settings.llm),
retriever_tools=[methods_tool, lang_tool],
)
# 输入
q = "GAPDH 内参对应的实验对象是什么物种?"
hits = router.retrieve(q)
print("router", [(h.node.metadata, h.node.text[:24]) for h in hits])
# 预期:collection=methods,不含列表推导式

fusion = QueryFusionRetriever(
[methods_index.as_retriever(similarity_top_k=2), lang_index.as_retriever(similarity_top_k=2)],
similarity_top_k=2,
num_queries=1,
mode="reciprocal_rerank",
)
print("fusion", [(h.node.metadata, h.node.text[:24]) for h in fusion.retrieve(q)])
# 预期形态:可能混入 lang 块——这是「不选路、全跑」的可见代价

6. 重要配置参数

参数(API 名) 类型 / 默认值 功能说明 作用与影响 参考起点 / 常用范围 配置指导
RetrieverTool.description str,必填语义 选择器用来判断「这条路是否该走」 两路 description 雷同则乱跳 含「何时用 / 何时不用」各一句 比 name 更重要
selector LLMSingle / Multi 一次选 1 路还是多路 Multi 接近弱融合,费用更高 互斥库用 Single 本地模型先 Single
select_multi bool,部分 from_defaults 是否允许多 tool True 时噪声近似 Fusion 默认 False 与 Fusion 二选一
num_queries int,Fusion 默认 4 额外生成几条 query Router 场景通常不需要改写 1 改写另做,不要叠在选路上
similarity_top_k int Fusion 最终截断条数 路数 × 每路 k 很大时必须截 与单路 k 同量级 先看各路再定
temperature LLM,建议 0 选择器采样 >0 时同一问可能换路 0 选路必须稳

7. 适用 / 不适用

维度 适用 不适用
任务形态 库主题互斥、问句类型可分 同一库里语义+货号——用向量+BM25 Fusion,不要 Router
集成约束 能接受一次小 LLM 选路 选路必须零 LLM——规则 if/else 或 Fusion
工程阶段 2~5 个抽屉 几十个工具还要失败重试——LangGraph 条件边 + 显式 state

8. 易踩坑

  1. description 写成广告语(「非常有用」):选择器无法互斥。
  2. 用 Fusion 替代 Router 处理互斥库:第二库头名稳定污染。
  3. PydanticSingleSelector + 不支持 tool 的本地模型:解析失败,retrieve 空或乱。改 LLMSingleSelector
  4. Router 里 num_queries=4 的 Fusion 当子路:一次用户问题变成选路 LLM + 多路改写,账算不清。

小结

  • 互斥库 → Router同一问题必须多路证据 → Fusion
  • Tool description 是选路契约;temperature=0
  • 跨索引 Fusion 会抬进「另一路的头名」,这是特性不是 bug。
  • 选路失败要重试或审批时,把 Router 的结果当 LangGraph 一个节点的输出,不要在 Retriever 里造环。

参考链接

-------------本文结束感谢您的阅读-------------