Rerank与NodePostprocessor

向量 top-10 里常常混着「列表推导式」这种高相似、低相关块。生成模型分不清,就会把噪声炼进答案。NodePostprocessor 接在 retrieve 之后、合成之前:过滤、重排、截断到 top_n。索引不用重建;改的是这一次送给 LLM 的名单。

段末注释交叉编码器(cross-encoder,CE)把 (query, 块) 拼成一条输入打相关分,比双塔检索更准、也更贵,所以只对 top-k 短名单用。

召回 8 张乱卡,cutoff / CE / 关键词三道关,只放行 top_n 进合成锅(科普示意)


1. 一句话定位

维度 内容
角色 知识层的检索后处理:改名单,不改向量库
输入 → 输出 list[NodeWithScore] + query → 更短/重排后的 list[NodeWithScore]
典型调用入口 postprocess_nodes(nodes, query_str=...)as_query_engine(node_postprocessors=[...])
与 LangChain / LangGraph 近邻是 compressor / CohereRerank;LangGraph 节点里调 postprocess_nodes 即可

出现背景:双塔 embedding 让 query 与文档分别编码,召回快但排序粗。CE 要成对前向,只能打短名单。LlamaIndex 把这一档收成 Postprocessor,插在 QueryEngine 流水线里。

重排方法的评测口径见 RAG-03;本篇只讲 API 怎么挂。


2. 前置依赖与环境

1
2
3
pip install -U llama-index-core llama-index-llms-ollama llama-index-embeddings-ollama sentence-transformers
ollama pull qwen3.5:9b
ollama pull nomic-embed-text
  • Python 3.10+;llama-index-core 0.14.x
  • SentenceTransformerRerank 首次运行会下载 CE 权重(需联网)
  • postprocess_nodes 同步即可;QueryEngine 侧生产用 aquery

3. 实现逻辑

1
2
3
4
5
6
1. retriever.retrieve(q, similarity_top_k=较大)
2. (可选)SimilarityPostprocessor:丢掉 score < cutoff 的块
3. SentenceTransformerRerank:对 (q, node.text) 打 CE 分,截 top_n
4. (可选)KeywordNodePostprocessor:必含/必不含词
5. 把结果交给 synthesizer / QueryEngine
6. source_nodes 应等于后处理之后的名单

字段级变形

1
2
3
4
retrieve k=5
[methods 0.55, lang 0.54, methods 0.40, ...]
→ SentenceTransformerRerank(top_n=2, query_str=q)
[methods 0.91, methods 0.77] # score 被 CE 覆盖;lang 被挤出

RRF 融合发生在 Retriever 内;CE 发生在 Postprocessor。先融合再 CE,不要反过来(CE 吃不起全库)。


4. 原理说明

主轴是:Postprocessor 读的是已召回的 NodeWithScore,写回的还是 NodeWithScore;QueryEngine 按列表顺序执行它们。

1
2
3
4
5
6
7
8
9
10
11
1. QueryEngine 拿到 retrieve 结果
2. for p in node_postprocessors: nodes = p.postprocess_nodes(nodes, query_bundle)
3. SimilarityPostprocessor 只看 hit.score,与 query 文本无关
Simple 店分数量纲不稳定,cutoff=0.7 可能滤空
4. SentenceTransformerRerank 把 query 与每块 text 拼成对,CE 前向得 logit/相关分
写回 hit.score,按分排序,截 top_n
5. KeywordNodePostprocessor 对 text 做词面包含检查(不是语义)
6. PrevNextNodePostprocessor 按 PREVIOUS/NEXT 补邻块(切分时需 include_prev_next_rel)
7. 合成器只看见第 2 步之后的名单
少了第 4 步却把 k 收到 2 → 噪声与证据一起被截,CE 没机会救
cutoff 抄 0.75 且 score 是距离 → 全灭

SimilarityPostprocessor(similarity_cutoff=...) 出现在步骤 3。无 query 也能跑。默认不改相对顺序。

SentenceTransformerRerank(model=..., top_n=...) 出现在步骤 4。必须传 query_strquery_bundle。默认模型多为 MiniLM 英文 CE;中英混合方法学可换成 BAAI/bge-reranker-base

KeywordNodePostprocessor(required_keywords=..., exclude_keywords=...) 出现在步骤 5。英文 token 有效;中文短词容易误杀。

as_query_engine(node_postprocessors=[a, b]) 出现在步骤 2。列表顺序 = 流水线顺序:先滤再 CE,或先 CE 再滤,结果不同。

不要把 CE 当检索器:它对未召回的块无能为力。


5. 最小可运行示例

1
pip install -U llama-index-core llama-index-llms-ollama llama-index-embeddings-ollama sentence-transformers
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
from llama_index.core import Document, Settings, VectorStoreIndex
from llama_index.core.node_parser import SentenceSplitter
from llama_index.core.postprocessor import (
KeywordNodePostprocessor,
SentenceTransformerRerank,
SimilarityPostprocessor,
)
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",
)

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

q = "GAPDH 实验对象是什么物种?"
raw = index.as_retriever(similarity_top_k=5).retrieve(q)
print("raw", [(n.node.doc_id, round(n.score or 0, 3)) for n in raw])

# 关键参数:先放大 k,再用 CE 收到 top_n
rerank = SentenceTransformerRerank(
model="cross-encoder/ms-marco-MiniLM-L-6-v2",
top_n=2,
)
ranked = rerank.postprocess_nodes(raw, query_str=q)
print("ce", [(n.node.doc_id, round(n.score or 0, 3)) for n in ranked])
# 预期:ce 更偏向 paper_001;score 与 raw 不可比

qe = index.as_query_engine(
similarity_top_k=5,
node_postprocessors=[
SimilarityPostprocessor(similarity_cutoff=0.0),
rerank,
KeywordNodePostprocessor(exclude_keywords=["列表推导式"]),
],
)
resp = qe.query(q)
print(resp)
print([n.node.doc_id for n in resp.source_nodes])
# 预期:答案含小鼠;source_nodes 不含 py_001(若关键词过滤生效)

6. 重要配置参数

参数(API 名) 类型 / 默认值 功能说明 作用与影响 参考起点 / 常用范围 配置指导
similarity_top_k int,引擎/检索器 CE 之前的召回宽度 过小 CE 没候选;过大 CE 延迟线性升 10~20 再 rerank 与 top_n 成对:先宽后窄
top_n int,CE 默认常 2~3 重排后保留条数 过小漏第二条证据;过大白做 CE 3~5 ≤ 合成窗口能消化的块数
model str CE 权重 ID 英文 MiniLM 对中文方法节偏弱 BAAI/bge-reranker-base 中英 与语料语言对齐
similarity_cutoff float 当前 score 硬截 量纲随后端变;过严空上下文 先打印 raw.score 再设 Simple 店建议 0 或关掉
required_keywords list[str] 块文本必须命中这些词 中文单字易误杀 基因名、货号 只挡明确噪声
exclude_keywords list[str] 命中则丢块 过宽会丢掉方法节里的对比句 明确跑题词 与 CE 叠用时放 CE 后
node_postprocessors 顺序 list 顺序 流水线先后 先 CE 再 cutoff 用的是 CE 分 滤噪声 → CE → 关键词 写进注释,避免调换无感

7. 适用 / 不适用

维度 适用 不适用
任务形态 召回有了但排序吵、生成被噪声带偏 根本没召回到金标准块——先改切分/混合检索
集成约束 可下载 CE;或只用 cutoff/关键词 禁止本地模型文件——改云端 rerank API(Cohere 等)
工程阶段 QueryEngine 已跑通 要按用户反馈在线学习排序——不是 Postprocessor 的职责

8. 易踩坑

  1. k=2 再 rerank top_n=2:CE 没有重排空间。
  2. 比较 CE.score 与向量 score:量纲不同。
  3. cutoff 抄教程 0.7:Simple 店可能全滤空。
  4. Keyword 当语义过滤:「小鼠」写在无关块里也会留下。

小结

  • Postprocessor 不写索引,只改送给 LLM 的名单。
  • 模式:放大 retrieve k → CE top_n → 合成
  • source_nodes 应是后处理之后的列表。
  • 救不了漏召回;漏召回回 Retriever。

参考链接

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