Retriever

索引建好之后,生成模型还看不到任何私有段落。缺的是一步:query → 带分数的块。LlamaIndex 把这一步收成 Retrieverretrieve(query) 返回 list[NodeWithScore]。向量路擅长同义改写(「实验对象」→「小鼠」);货号、基因名走 BM25 更稳。两路再用倒数排名融合(reciprocal rank fusion,RRF)合成一份名单——此时还没有调用生成模型。

段末注释Retriever = str → list[NodeWithScore]RRF 按各路排名加 $\sum_i 1/(k+\mathrm{rank}_i)$,不要求两路分数同量纲。

向量磁铁与 BM25 扫码汇成 NodeWithScore,MetadataFilters 拦住无关块(科普示意)


1. 一句话定位

维度 内容
角色 知识层的召回:按 query 取出候选 Node,附相似度/词项分数
输入 → 输出 strQueryBundlelist[NodeWithScore]
典型调用入口 index.as_retriever().retrieve()BM25Retriever.from_defaults()QueryFusionRetriever
与 LangChain / LangGraph 近邻是 BaseRetriever.invoke;LangGraph 节点里只 retrieve,不要在图里 from_documents

出现背景as_query_engine() 把检索和生成焊死。要调 top-k、加 metadata 过滤、或把检索当 Agent 工具,必须先把 Retriever 拆出来。

混合检索的评测口径(Recall@k / nDCG)见 RAG 目录;本篇只讲 LlamaIndex 怎么把两路拼上。


2. 前置依赖与环境

1
2
pip install -U llama-index-core llama-index-llms-ollama llama-index-embeddings-ollama llama-index-retrievers-bm25
ollama pull nomic-embed-text
  • Python 3.10+;llama-index-core 0.14.x
  • 向量路必须设 Settings.embed_modelretrieve 不必设 LLMQueryFusionRetriever(num_queries>1) 才会为改写 query 调 LLM)
  • 异步:await retriever.aretrieve(q),参数与 retrieve 一致

3. 实现逻辑

1
2
3
4
5
6
7
1. 建好 VectorStoreIndex(Node 已有 embedding)
2. vector = index.as_retriever(similarity_top_k=k, filters=...)
3. bm25 = BM25Retriever.from_defaults(nodes=..., similarity_top_k=k)
4. (可选)QueryFusionRetriever([vector, bm25], num_queries=1, mode="reciprocal_rerank")
5. hits = retriever.retrieve(query)
6. 每条 NodeWithScore:.node.text / .node.metadata / .score
7. 把 hits 交给 QueryEngine 或 Agent 工具——本篇停在第 6 步

字段级变形

1
2
3
4
"实验对象是什么物种?"
→ VectorIndexRetriever._retrieve
[NodeWithScore(node=TextNode(text="……C57BL/6 小鼠……", metadata={section:"methods"}), score=0.81), ...]
→ 若 filters={section=methods}:section=lang 的块根本不进向量查询

4. 原理说明

主轴是:Retriever 只负责候选名单;分数不可跨后端比较;混合靠排名不是靠把余弦和 BM25 加在一起。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1. retrieve(str) 包成 QueryBundle(query_str=...)
2. 向量路:embed_model.get_query_embedding → vector_store.query(top_k, filters)
3. 向量库返回 id + 距离/相似度;docstore 取出 TextNode,包成 NodeWithScore
4. filters 在向量库侧先截断候选(不是召回后再丢)
5. BM25 路:倒排打分,同样包成 NodeWithScore(score 量纲与余弦不同)
6. Fusion 且 num_queries=1:不再让 LLM 改写 query
7. mode=reciprocal_rerank 时,对每路名单按名次做 RRF 再截断 similarity_top_k
$$
\mathrm{RRF}(d)=\sum_{i}\frac{1}{k+\mathrm{rank}_i(d)}
$$
常用 $k=60$(实现内置,不必手写)
8. 返回融合后的 list[NodeWithScore]
少了第 4 步 → 「只要 methods」仍可能混进 python 教程块
少了第 6 步(num_queries=4)→ 本地模型被拉去生成 3 条改写,延迟和费用上去
把第 5 步的 score 与第 3 步的 score 直接相加 → 量纲乱,RRF 就是为避免这件事

index.as_retriever(**kwargs) 出现在步骤 2。对向量索引默认构造 VectorIndexRetrieversimilarity_top_k 默认常为 2(过小)。

NodeWithScore(数据类)出现在步骤 3。字段:node: BaseNodescore: float | None。引用读 node.metadata,不要 str(hit)

MetadataFilters 出现在步骤 4。MetadataFilter(key, value, operator=FilterOperator.EQ);多条件用 FilterCondition.AND/OR

BM25Retriever.from_defaults 出现在步骤 5。三者只传一个:index / nodes / docstore。默认 language="english" 的停用词/词干对中文帮助有限,拉丁符号(GAPDH、C57BL/6)仍有效。

QueryFusionRetriever 出现在步骤 6~7。num_queries=1 关闭 query 生成;多路同一语料才叫混合,多路不同索引见 Router 专篇。

VectorStoreQueryMode.HYBRID 要求向量库本身支持稀疏+稠密(Weaviate / OpenSearch 等)。SimpleVectorStore 没有这一档,单机用 BM25 + Fusion。


5. 最小可运行示例

1
pip install -U llama-index-core llama-index-embeddings-ollama llama-index-retrievers-bm25
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
from llama_index.core import Document, Settings, VectorStoreIndex
from llama_index.core.node_parser import SentenceSplitter
from llama_index.core.retrievers import QueryFusionRetriever
from llama_index.core.vector_stores import FilterOperator, MetadataFilter, MetadataFilters
from llama_index.embeddings.ollama import OllamaEmbedding
from llama_index.retrievers.bm25 import BM25Retriever

Settings.embed_model = OllamaEmbedding(
model_name="nomic-embed-text",
base_url="http://localhost:11434",
)

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

# 输入:自然语言 + 硬过滤
filters = MetadataFilters(
filters=[MetadataFilter(key="section", value="methods", operator=FilterOperator.EQ)]
)
vector = index.as_retriever(similarity_top_k=2, filters=filters)
print("vector", [(h.score, h.node.metadata["section"], h.node.text[:20]) for h in vector.retrieve("实验对象是什么物种?")])

bm25 = BM25Retriever.from_defaults(nodes=list(index.docstore.docs.values()), similarity_top_k=2)
print("bm25", [(h.node.metadata["section"], h.node.text[:20]) for h in bm25.retrieve("GAPDH C57BL/6")])

# 关键参数:num_queries=1 避免额外 LLM 改写
hybrid = QueryFusionRetriever(
[index.as_retriever(similarity_top_k=2), bm25],
similarity_top_k=2,
num_queries=1,
mode="reciprocal_rerank",
)
hits = hybrid.retrieve("GAPDH 的实验对象是什么物种?")
print("hybrid", [(h.node.doc_id, h.node.metadata, h.score) for h in hits])
# 预期:vector 过滤后只有 methods;bm25 命中 GAPDH 那条;hybrid 第一条来自 paper_001

生产异步用 await hybrid.aretrieve(q)


6. 重要配置参数

参数(API 名) 类型 / 默认值 功能说明 作用与影响 参考起点 / 常用范围 配置指导
similarity_top_k int,常默认 2 本路最多返回几条 过小漏召回;过大噪声进合成 向量 5~10;融合截断同量级 后面有 rerank 可先放大
filters MetadataFilters,可选 按 Node.metadata 在库侧硬过滤 键不存在或类型不匹配 → 空结果 section=methods 入库时 metadata 必须是扁平标量
num_queries int,Fusion 默认 4 除原 query 外再生成几条改写 >1 要 LLM;本地延迟倍增 混合基线用 1 改写策略另开 Query Rewrite,不要默认 4
mode str,Fusion 默认多为 simple 多路名单如何合成 reciprocal_rerank 不比绝对分;simple 易被量纲带偏 reciprocal_rerank 向量+BM25 用 RRF
BM25Retriever.language str,默认英语停用词 词干/停用词语言 中文正文收益小;不影响拉丁符号 货号/基因名场景可忽略 纯中文再换中文分词方案
vector_store_query_mode 枚举,默认 DEFAULT 后端是否走 hybrid/sparse Simple 店无 HYBRID,传了也没用 有 OS/Weaviate 再用 单机走 BM25+Fusion

7. 适用 / 不适用

维度 适用 不适用
任务形态 语义问 + 术语/货号并存 只要生成、不需要引用——不必拆 Retriever
集成约束 单机 Simple 店 + BM25 向量库已提供原生 hybrid——用后端 hybrid,少一层 Python Fusion
工程阶段 调 top-k / 过滤 / 当工具 多知识库「先选库再搜」——那是 Router,不是本篇 Fusion

环、审批仍交给 LangGraph;本对象只返回名单。


8. 易踩坑

  1. NodeWithScore 当 str 塞 prompt:模型读到对象 repr。用 hit.node.text
  2. similarity_top_k=2 不改:方法节证据经常排在第 3。
  3. Fusion num_queries=4 却没准备 LLM:改写失败或偷偷打 OpenAI。
  4. 比较 vector.score 与 bm25.score:量纲不同;要比名次或只看融合后的 RRF 分。

小结

  • retrieve 停在 NodeWithScore,不生成答案。
  • filters 在库侧截断;BM25 补拉丁符号;RRF 融合两路排名。
  • 混合基线:num_queries=1 + mode="reciprocal_rerank"
  • 要把检索接到问答,用 QueryEngine 把本对象塞进去。

参考链接

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