一、三层结构:Retriever / Postprocessor / Synthesizer
LlamaIndex 的 QueryEngine 就是这三步的容器。搞清楚每一步都能独立替换,RAG 调优才有章法:
from llama_index.core.query_engine import RetrieverQueryEngine
from llama_index.core import get_response_synthesizer
retriever = index.as_retriever(similarity_top_k=10)
postprocessors = [...] # 马上讲
synthesizer = get_response_synthesizer(response_mode="compact")
qe = RetrieverQueryEngine(
retriever=retriever,
node_postprocessors=postprocessors,
response_synthesizer=synthesizer,
)
ans = qe.query("2025 Q3 营收")
二、Retriever 参数与模式
retriever = index.as_retriever(
similarity_top_k=10, # 召回多少 ↑多一些给 postprocessor 挑
filters=metadata_filters, # Ch6 讲的 MetadataFilters
vector_store_query_mode="default", # default/hybrid/sparse/text_search
alpha=0.5, # hybrid 模式下稀疏/稠密权重(0=全稀疏,1=全稠密)
sparse_top_k=20, # hybrid 模式下稀疏部分召回数
)
top_k 的选择哲学
"召回多 + 后处理筛"几乎总是比"精准少召回"好:
- 不用重排 → top_k=3-5
- 上重排(强烈建议)→ top_k=20-50 召回,重排后留 3-5
- hybrid 检索 → 稠密 top_k=15 + 稀疏 top_k=15,融合后再重排
- 多跳/复杂问题 → 每跳独立 top_k,总量别爆 context
三、AutoRetriever:LLM 自己写过滤条件
用户问"Acme 在 2024 年的 NDA"——这里有两个 filter:party=Acme、year=2024。AutoRetriever 让 LLM 看 schema 描述,自己构造 MetadataFilters:
from llama_index.core.retrievers import VectorIndexAutoRetriever
from llama_index.core.vector_stores import MetadataInfo, VectorStoreInfo
info = VectorStoreInfo(
content_info="合同条款全文",
metadata_info=[
MetadataInfo(name="party", type="str", description="合同对方公司名"),
MetadataInfo(name="year", type="int", description="签约年份"),
MetadataInfo(name="type", type="str", description="合同类型:NDA/SOW/MSA"),
],
)
retriever = VectorIndexAutoRetriever(
index=index,
vector_store_info=info,
similarity_top_k=5,
verbose=True, # 打印 LLM 生成的 filter,调试必开
)
nodes = retriever.retrieve("Acme 在 2024 年的 NDA")
# LLM 会自动生成 filter: party=="Acme" AND year==2024 AND type=="NDA"
四、RecursiveRetriever:多跳下钻
Ch2 讲过 IndexNode 可以作为"桥"。RecursiveRetriever 就是遇到 IndexNode 时自动下钻到对应子索引继续检索:
from llama_index.core.retrievers import RecursiveRetriever
from llama_index.core.schema import IndexNode
# 上层索引:产品级
top_nodes = [
IndexNode(text="产品 A 介绍", index_id="prod_a_index"),
IndexNode(text="产品 B 介绍", index_id="prod_b_index"),
]
top_index = VectorStoreIndex(top_nodes)
# 子索引:每个产品的详细文档
sub_indices = {
"prod_a_index": VectorStoreIndex.from_documents(prod_a_docs).as_retriever(),
"prod_b_index": VectorStoreIndex.from_documents(prod_b_docs).as_retriever(),
}
recursive = RecursiveRetriever(
"vector",
retriever_dict={"vector": top_index.as_retriever(), **sub_indices},
verbose=True,
)
nodes = recursive.retrieve("产品 A 的定价")
# ①上层命中 IndexNode(prod_a_index) ②自动跳到 prod_a_index 继续检索 ③返回细节 chunk
五、AutoMergingRetriever:小块命中、大块回答
理想的 RAG:embedding 用小块(512)命中准,但回答时给 LLM 更大的上下文(2048)。AutoMergingRetriever + HierarchicalNodeParser 就是这个组合:
from llama_index.core.node_parser import HierarchicalNodeParser
from llama_index.core.retrievers import AutoMergingRetriever
from llama_index.core.storage.docstore import SimpleDocumentStore
parser = HierarchicalNodeParser.from_defaults(chunk_sizes=[2048, 512, 128])
nodes = parser.get_nodes_from_documents(docs)
docstore = SimpleDocumentStore()
docstore.add_documents(nodes) # 大中小三级都存
# 只把最小块(128)建向量索引
from llama_index.core.node_parser import get_leaf_nodes
leaf_nodes = get_leaf_nodes(nodes)
vector_index = VectorStoreIndex(leaf_nodes, storage_context=StorageContext.from_defaults(docstore=docstore))
base = vector_index.as_retriever(similarity_top_k=12)
retriever = AutoMergingRetriever(base, storage_context=vector_index.storage_context, verbose=True)
nodes = retriever.retrieve("怎么配置双因素认证")
# 如果 12 个小块里有 5 个属于同一个父块,自动合并返回父块
思路:如果召回的小块密集聚集在同一个父块里,说明答案就在那附近,与其给 LLM 几个碎片不如给它完整父块。
六、Postprocessor:对召回再加工
| Postprocessor | 作用 | 场景 |
|---|---|---|
SimilarityPostprocessor | 按分数阈值过滤 | 低于 0.7 的垃圾召回 |
LLMRerank | LLM 重排 top_k | 最后一公里精度 |
SentenceTransformerRerank | BGE/Jina 本地重排模型 | 生产主力 🔥 |
CohereRerank | Cohere 重排 API | 效果顶级但要钱 |
MetadataReplacementPostProcessor | 替换 text 为 metadata 中的字段 | SentenceWindow 还原上下文 |
PrevNextNodePostprocessor | 拉回相邻节点 | 扩展上下文 |
TimeWeightedPostprocessor | 按时间衰减加权 | 新闻/资讯场景 |
LongContextReorder | 把重要的放头尾(中间遗忘) | 长 context LLM 都有 |
重排(Rerank):RAG 的最强 ROI 工具
先说结论:加 rerank 几乎总是值得的。重排用的是 cross-encoder——把 query 和每个候选 chunk 一起送进模型,比双塔 embedding 精准得多。
# 本地 BGE rerank(推荐)
from llama_index.postprocessor.flag_embedding_reranker import FlagEmbeddingReranker
rerank = FlagEmbeddingReranker(
model="BAAI/bge-reranker-v2-m3", # 多语主力
top_n=5,
use_fp16=True,
)
# Cohere rerank(最强)
from llama_index.postprocessor.cohere_rerank import CohereRerank
rerank = CohereRerank(api_key="...", top_n=5, model="rerank-v3.5")
qe = index.as_query_engine(
similarity_top_k=20, # 多召回
node_postprocessors=[rerank], # 重排后留 5
)
LongContextReorder:中间遗忘问题
LLM 对 context 里中间位置的信息记忆最差("lost in the middle" 论文)。LongContextReorder 把最重要的放头尾:
from llama_index.core.postprocessor import LongContextReorder
qe = index.as_query_engine(
similarity_top_k=10,
node_postprocessors=[rerank, LongContextReorder()],
)
七、Response Synthesizer:拼 prompt 的艺术
Ch5 介绍过 response_mode,这里更全:
| mode | 逻辑 | 适合 |
|---|---|---|
compact(默认) | 能装一起就一起,一次 LLM | 短回答、一般场景 |
refine | 逐节点精炼答案 | 要非常严谨、慢、贵 |
tree_summarize | 二分归并 | 总结、多节点合并 |
simple_summarize | 粗暴拼一起 | 节点数少、上下文够 |
accumulate | 每节点独立回答,最后拼 | "列举每个节点提到的..." |
compact_accumulate | accumulate + compact 合批 | 列举型 + 成本意识 |
no_text | 只返回 nodes,不合成 | 要自己处理答案 |
自定义 prompt 模板
from llama_index.core import PromptTemplate
qa_tmpl = PromptTemplate(
"""请根据下面的上下文回答问题。回答要求:
1. 只使用提供的上下文,不要臆造
2. 答案末尾列出引用的来源文件名
3. 找不到答案就回答"上下文中没有这个信息"
<context>
{context_str}
</context>
问题:{query_str}
答案:"""
)
qe = index.as_query_engine(text_qa_template=qa_tmpl)
八、RouterQueryEngine:多库分发
公司有 5 个不同领域的 index(产品/合同/HR/财务/技术)——问题来了要路由到对的 index。RouterQueryEngine 让 LLM 选:
from llama_index.core.query_engine import RouterQueryEngine
from llama_index.core.tools import QueryEngineTool
from llama_index.core.selectors import LLMSingleSelector, PydanticSingleSelector
tools = [
QueryEngineTool.from_defaults(
query_engine=qe_product,
name="product_docs",
description="产品使用手册、API 文档、版本发布说明",
),
QueryEngineTool.from_defaults(
query_engine=qe_contract,
name="contracts",
description="公司对外签订的所有合同、NDA、MSA",
),
QueryEngineTool.from_defaults(
query_engine=qe_hr,
name="hr",
description="员工手册、薪酬规则、休假政策",
),
]
router = RouterQueryEngine(
selector=PydanticSingleSelector.from_defaults(), # LLM 选 1 个
query_engine_tools=tools,
verbose=True,
)
ans = router.query("年假有多少天?")
# LLM 会选 hr,然后在 hr 索引上跑
要 LLM 选多个 tool(比如问题跨领域)换 PydanticMultiSelector。注意 description 越清晰越准——像给实习生写的任务说明,别写得像 DB 字段名。
九、QueryFusionRetriever:多查询融合
用户的问题可能模糊。让 LLM改写成 N 个变体一起检索,结果融合:
from llama_index.core.retrievers import QueryFusionRetriever
fusion = QueryFusionRetriever(
[vector_retriever, bm25_retriever],
similarity_top_k=5,
num_queries=4, # 原问题 + 3 个改写
mode="reciprocal_rerank",
use_async=True,
verbose=True,
query_gen_prompt="""原问题: {query}
请生成 {num_queries} 个语义相近但表达不同的检索查询,每行一个:""",
)
nodes = fusion.retrieve("怎么退款?")
# LLM 改写成:"退款流程" "如何申请退款" "退款需要的材料",四条各自检索,RRF 融合
十、Streaming 流式返回
qe = index.as_query_engine(streaming=True)
streaming_response = qe.query("解释双因素认证")
for token in streaming_response.response_gen:
print(token, end="", flush=True)
# 或 FastAPI SSE
async def stream_chat(q):
resp = await qe.aquery(q)
async for token in resp.async_response_gen():
yield f"data: {token}\n\n"
十一、一个生产级组合
from llama_index.core.query_engine import RetrieverQueryEngine
from llama_index.core.retrievers import QueryFusionRetriever
from llama_index.core.postprocessor import SimilarityPostprocessor, LongContextReorder
from llama_index.postprocessor.flag_embedding_reranker import FlagEmbeddingReranker
from llama_index.retrievers.bm25 import BM25Retriever
# 1. 召回层(hybrid)
v = index.as_retriever(similarity_top_k=15)
b = BM25Retriever.from_defaults(nodes=nodes, similarity_top_k=15)
retriever = QueryFusionRetriever(
[v, b], similarity_top_k=20, num_queries=1, mode="reciprocal_rerank",
)
# 2. 后处理层
postprocessors = [
SimilarityPostprocessor(similarity_cutoff=0.25),
FlagEmbeddingReranker(model="BAAI/bge-reranker-v2-m3", top_n=5),
LongContextReorder(),
]
# 3. 合成层
qe = RetrieverQueryEngine.from_args(
retriever=retriever,
node_postprocessors=postprocessors,
response_mode="compact",
streaming=True,
text_qa_template=qa_tmpl,
)
十二、常见坑
- top_k 太小又没重排:精度天花板就在那了——至少 10 召回 + rerank。
- AutoRetriever 的 metadata description 写得太简:LLM 生成错误 filter,全部查不到。
- rerank 模型没跟 embedding 匹配:BGE embed + BGE rerank 是一个家族,混用其他可能退化。
- Cohere rerank 忘了开 cutoff:top_n 截断后返回的都是假分数,要配合
SimilarityPostprocessor(cutoff=0.3)。 - RouterQueryEngine 的 description 只写字段名:写成自然描述,LLM 才能匹配。
- QueryFusion 的 num_queries 设大(>5):收益递减,成本翻倍。
- refine 模式在长文档上慢到 30 秒:换 compact 或 tree_summarize。
- streaming 模式忘了异步 await:SSE 端点直接阻塞。
- SentenceWindow 忘了 MetadataReplacementPostProcessor:拿到的是小窗口不是完整上下文,白设计了。
十三、本章小结
① 标配组合:top_k=20 召回 + BGE rerank top_n=5 + LongContextReorder——这一组就能击败 90% 的默认 pipeline。
② AutoRetriever 把 metadata 过滤从手写变成 LLM 自动——注意 description 要写清楚。
③ RouterQueryEngine 处理多领域分发,QueryFusionRetriever 处理模糊问题改写——生产项目通常两者都要。
④ 小块命中、大块回答 → SentenceWindow 或 AutoMerging,既准又完整。