QueryEngine

Retriever 只给名单。产品要的是一句话答案,并且能指回 methods 那条 Node。QueryEngine 把三拍焊在一起:检索 → 可选 NodePostprocessor → ResponseSynthesizer 调 LLM,返回 Responseindex.as_query_engine() 是这条焊线的捷径;要换混合检索或过滤,就显式 RetrieverQueryEngine.from_args(retriever, ...)

段末注释QueryEngine 输入自然语言 query,输出 Response(文本 + source_nodes)。默认是 DAG,不是带环 Agent。

query() 经过检索、后处理、合成三站,source_nodes 贴在答案上(科普示意)


1. 一句话定位

维度 内容
角色 知识层的一次问答入口:检索结果变成可引用答案
输入 → 输出 strResponse.response / str(resp) + .source_nodes
典型调用入口 index.as_query_engine().query()RetrieverQueryEngine.from_args()aquery()
与 LangChain / LangGraph 近邻是 retriever | prompt | model;环、HITL、checkpoint 不在本对象里

出现背景:早期示例只有 query_engine.query。可定制检索之后,官方把引擎拆成可插拔的 Retriever + Synthesizer + Postprocessor,捷径 as_query_engine 仍然可用。


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
  • 必须同时设 Settings.llmSettings.embed_model
  • 示例用同步 query();FastAPI 用 await qe.aquery(...)
  • streaming=True 时迭代 response_gen,不要假定一次返回完整 str

3. 实现逻辑

1
2
3
4
5
6
7
1. 准备 Index / Retriever
2. qe = index.as_query_engine(...) 或 RetrieverQueryEngine.from_args(retriever, ...)
3. resp = qe.query(question)
4. 内部:retriever.retrieve → 每个 postprocessor.postprocess_nodes → synthesizer.synthesize
5. 打印 str(resp) 作为答案
6. 遍历 resp.source_nodes 做引用(node_id / metadata / score / text)
7. 生产:aquery;或 streaming=True 后读 response_gen

字段级变形

1
2
3
4
5
6
7
"实验对象是什么物种?"
→ retrieve → [NodeWithScore(..., metadata={section:methods}, score=0.8), ...]
→ synthesizer 把 node.get_content(LLM) 填进 QA prompt
Response(
response="小鼠(C57BL/6)……",
source_nodes=[同一批 NodeWithScore]
)

source_nodes 默认是送进合成器之前的名单(含后处理结果),不是模型「声称引用了谁」。模型胡编时,名单里可能根本没有那句话——所以必须对读。


4. 原理说明

主轴是:query() 是三条管道的同步门面;缺任何一拍,数据停在不同层。

1
2
3
4
5
6
7
8
9
10
11
1. QueryEngine.query 把 str 包成 QueryBundle
2. retriever.retrieve(bundle) → list[NodeWithScore]
3. 按列表顺序执行 node_postprocessors[i].postprocess_nodes(nodes, query_bundle)
4. 空名单仍可能调用 LLM(取决于 synthesizer),得到「不知道」或幻觉
5. response_synthesizer.synthesize(query, nodes) 调 Settings.llm(或局部 llm)
6. 合成模式(compact/refine/...)决定 LLM 调用次数,见合成器专篇
7. 组装 Response:文本 + source_nodes=(通常为)第 3 步输出
8. aquery 同链路异步;streaming 时 LLM token 经 response_gen 流出,source_nodes 仍在对象上
少了第 2 步 → 没有私有证据
少了第 3 步 → 低分噪声直接进 prompt
少了第 7 步只 print 文本 → 无法审计

VectorStoreIndex.as_query_engine(**kwargs) 出现在步骤 2。内部用该 index 的 VectorIndexRetriever + 默认 compact 合成器。kwargs 会传给 retriever / synthesizer(如 similarity_top_kresponse_modenode_postprocessorsstreaming)。

RetrieverQueryEngine.from_args(retriever, ...) 出现在步骤 2。功能:任意 Retriever(混合、Router)焊成引擎。RetrieverQueryEngine(retriever, response_synthesizer=..., node_postprocessors=...) 是更显式的构造。

Response 出现在步骤 7。常用:str(resp)resp.source_nodesresp.metadata。不要用 resp.response is None 判断流式对象。

SimilarityPostprocessor(similarity_cutoff=...) 可作为第 3 步的最小后处理;完整 rerank 见后处理专篇。

QueryEngine 不会在合成失败后自动再检索——那是 Agent / LangGraph。


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
from llama_index.core import Document, Settings, VectorStoreIndex, get_response_synthesizer
from llama_index.core.node_parser import SentenceSplitter
from llama_index.core.postprocessor import SimilarityPostprocessor
from llama_index.core.query_engine import RetrieverQueryEngine
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 为内参。实验对象为 SPF 级 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)],
)

retriever = index.as_retriever(similarity_top_k=3)
synth = get_response_synthesizer(response_mode="compact", llm=Settings.llm)

# 关键参数:显式焊上 retriever / synthesizer / postprocessor
qe = RetrieverQueryEngine.from_args(
retriever,
response_synthesizer=synth,
node_postprocessors=[SimilarityPostprocessor(similarity_cutoff=0.0)],
)

question = "实验对象是什么物种?" # 输入
resp = qe.query(question) # 输出 Response
print(resp)
for n in resp.source_nodes:
print("cite", n.node.doc_id, n.node.metadata, round(n.score or 0, 3), n.node.text[:32])
# 预期:答案含「小鼠」;source_nodes 含 paper_001 / section=methods

流式(token 随迭代到达):

1
2
3
4
5
qe_s = index.as_query_engine(similarity_top_k=3, streaming=True)
sresp = qe_s.query(question)
for t in sresp.response_gen:
print(t, end="")
print("\n", [n.node.doc_id for n in sresp.source_nodes])

6. 重要配置参数

参数(API 名) 类型 / 默认值 功能说明 作用与影响 参考起点 / 常用范围 配置指导
similarity_top_k int,常默认 2 传给底层向量 Retriever 的 k 过小漏证据;过大挤合成窗口 5~10 与 cutoff / rerank 联调
response_mode str,默认 compact 选择合成器策略 决定 LLM 次数与细节保留 compact / refine / tree_summarize / no_text 单事实 compact;只审计用 no_text
node_postprocessors list,默认 [] retrieve 之后、合成之前的过滤/重排 空列表 = 原样送 LLM 至少可加 SimilarityPostprocessor 顺序即流水线顺序
streaming bool,默认 False 是否流式解码 True 时不要当普通 str 一次用完 SSE 接口 True 与 aquery 独立
llm LLM,可选 覆盖 Settings.llm 只用于合成 检索仍用 embed_model 与 Settings 成对 不要只设 llm
text_qa_template PromptTemplate,可选 compact/refine 的主问答模板 不写「只根据资料」易引入参数记忆 加「无证据则不知道」 引用约束写在这里

7. 适用 / 不适用

维度 适用 不适用
任务形态 单轮文档问答、要 source_nodes 工具失败再检索、多步规划——Agent / LangGraph
集成约束 已有 Retriever 无私有库——直接 ChatModel
工程阶段 同步脚本到 FastAPI aquery 需要审批后再生成——interrupt 放在图上,QE 只当节点

8. 易踩坑

  1. print(resp):丢掉引用,无法证明不是幻觉。
  2. as_query_engine 默认 k=2:混合语料时方法块排不到前两名。
  3. cutoff 按 0.7 抄向量库教程:Simple 店分数量纲不同,可能滤空。先打印 source_nodes.score 再设。
  4. 在 FastAPI 里同步 query():阻塞事件循环,改 aquery

小结

  • QueryEngine = retrieve → postprocess → synthesize。
  • 捷径 as_query_engine;可插拔用 RetrieverQueryEngine.from_args
  • 审计读 source_nodes,并与答案对读。
  • 要再检索、要环,把本对象当工具,不要在 query() 里 while True。

参考链接

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