ResponseSynthesizer

检索已经给出 8 条 Node。下一段失败通常不是「没召回」,而是怎么把块喂给 LLM:一次塞爆窗口、或每块单独问却把无关块炼进答案。ResponseSynthesizerquery + list[NodeWithScore],吐 Responseresponse_mode 决定打包策略和调用次数,不决定召回质量。

段末注释ResponseSynthesizer 在 Retriever 与(可选)Postprocessor 之后运行;compact 是默认模式,先拼接再按需 refine。

四种 mode:拼接一次、逐块滚雪球、树汇总、只留 source_nodes(科普示意)


1. 一句话定位

维度 内容
角色 知识层的生成打包:多 Node → 一次或多次 LLM 调用 → 答案字符串
输入 → 输出 (query: str, nodes: list[NodeWithScore])Response
典型调用入口 get_response_synthesizer(response_mode=...)synthesize()as_query_engine(response_mode=...)
与 LangChain / LangGraph 近邻是 Stuff / Refine / Map-reduce 链;LangGraph 不替代 mode,只决定何时调用本对象

出现背景:窗口有限而块数不定。LlamaIndex 用若干已实现 mode 覆盖「塞得下就塞、塞不下就拆、要综述就递归汇总」,避免每个项目手写 for 循环调 LLM。


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
  • 合成器只认 LLM;本篇示例仍建一个小索引以便取出真实 Node(禁止 Fake LLM)
  • streaming=Truesynthesize 走生成器;aget_response / 异步 QueryEngine 与同步 mode 语义相同

3. 实现逻辑

1
2
3
4
5
6
1. retriever.retrieve(q) → nodes
2. synth = get_response_synthesizer(response_mode=..., llm=Settings.llm)
3. resp = synth.synthesize(q, nodes=nodes)
4. 内部按 mode 把 node.get_content(LLM) 填进模板
5. 一次或多次 llm.complete / chat
6. 返回 Response;source_nodes 带回输入 nodes(no_text 也带回)

字段级变形(compact)

1
2
3
4
5
nodes = [methods块, lang块]
→ 拼接 context_str = metadata+text + ...
→ prompt(text_qa_template, context_str, query_str)
→ LLM 一次(塞得下时)
Response.response = "小鼠……"

refine 则是:第 1 块 QA → (答案, 第 2 块)refine_template → 滚到第 n 块。


4. 原理说明

主轴是:mode 只改变「同一批 Node 被切成几次 LLM 调用」;不回头改检索名单。

1
2
3
4
5
6
7
8
9
10
1. synthesize 读取每个 Node 的 get_content(MetadataMode.LLM)
2. compact:能塞进窗口的块先 concatenate;超窗则 TokenTextSplitter 切开,再按 refine 流程消化这些「超级块」
3. refine:每块(或超级块)一次 LLM;相关与否都可能改写当前答案
4. tree_summarize:用 summary_template 分批问,答案再当块递归,直到 1 个答案;中间没有 refine 问句
5. simple_summarize:截断到单 prompt,快但丢尾部块
6. accumulate / compact_accumulate:每块独立作答再拼接,不合成单一叙事
7. no_text:不调 LLM,Response 文本空或占位,source_nodes 仍在
8. context_only:返回拼接后的上下文字符串,不生成
少了第 2 步直接 refine 8 块 → 8 次本地推理
compact 塞进了无关 lang 块 → 一次调用里注意力被稀释,看起来像幻觉

get_response_synthesizer(response_mode=..., **kwargs) 出现在步骤 2。response_mode 可传字符串或 ResponseMode 枚举。

synthesize(query, nodes) 出现在步骤 3。底层调 get_response(query_str, text_chunks)

structured_answer_filtering=True 挂在 refine/compact:让模型结构化声明「本块无关则跳过」。对 OpenAI function calling 较稳;本地小模型容易格式失败,默认别开。

常用 mode 对照:

mode LLM 次数(直觉) 适合
compact 少(能塞下则 1) 单事实、默认
refine ≈ 块数 要强迫读完每一块
tree_summarize 对数级~线性 多段综述
no_text 0 只评检索、只展示引用
context_only 0 自己拼 prompt
accumulate = 块数 每块独立抽取字段

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
from llama_index.core import Document, Settings, VectorStoreIndex, get_response_synthesizer
from llama_index.core.node_parser import SentenceSplitter
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)],
)
nodes = index.as_retriever(similarity_top_k=3).retrieve("实验对象是什么物种?")
print("retrieved", [n.node.doc_id for n in nodes])

q = "实验对象是什么物种?"
for mode in ("no_text", "compact", "refine"):
synth = get_response_synthesizer(response_mode=mode, llm=Settings.llm)
resp = synth.synthesize(q, nodes=nodes)
print("mode", mode, "text=", str(resp)[:80].replace("\n", " "))
print(" source", [n.node.doc_id for n in resp.source_nodes])
# 预期:no_text 几乎无生成句;compact/refine 含「小鼠」;source_nodes 与 retrieved 同批

接到引擎:

1
2
qe = index.as_query_engine(response_mode="tree_summarize", similarity_top_k=3)
print(qe.query("用一句话概括检索到的资料主题。"))

6. 重要配置参数

参数(API 名) 类型 / 默认值 功能说明 作用与影响 参考起点 / 常用范围 配置指导
response_mode str / enum,默认 compact 选择打包与调用策略 直接决定延迟与费用 见上表 先 compact,评测不过再 refine
llm LLM,默认 Settings.llm 合成用模型 与 embed 无关;未设打 OpenAI 与检索共用本地模型即可 事实题 temperature=0
streaming bool,默认 False 是否流式 decode 不改变 mode 的块调度 对前端 True compact 流式最直观
structured_answer_filtering bool,默认 False refine/compact 时丢掉无关块 本地模型易坏 JSON;OpenAI tool 较稳 默认关 先用 Postprocessor 滤,再考虑它
text_qa_template PromptTemplate 首块/compact 的 QA 模板 不约束「仅资料」则抄参数记忆 加拒答句 与 refine_template 成对改
refine_template PromptTemplate 后续块如何改写已有答案 写得弱则无关块覆盖正确答案 强调「无关则保持原答案」 refine 模式必看
summary_template PromptTemplate tree_summarize 每层问句 不适合单槽事实(剂量/物种) 综述任务 不要拿来答 yes/no 事实
verbose bool 打印每跳调用 只影响日志 调试 refine/tree 时 True 生产关

7. 适用 / 不适用

维度 适用 不适用
任务形态 已有固定名单,只需改变「怎么读这些块」 名单本身是错的——改 Retriever/rerank,换 mode 无效
集成约束 本地 LLM 可多次调用 严格 1 次 LLM 预算——用 compact 并减小 k,或 context_only 自己拼
工程阶段 调生成侧忠实度 要工具/再检索——合成器没有这个钩子

8. 易踩坑

  1. 用 tree_summarize 答「剂量是多少」:递归摘要会丢掉数字。事实槽用 compact。
  2. refine + 噪声块:后块可以把正确物种改错;先 Similarity/rerank 再 refine。
  3. 把 no_text 当坏了:它本来就不生成;看 source_nodes 才是目的。
  4. structured_answer_filtering + 小模型:解析失败表现为乱答案或异常,不是「过滤很强」。

小结

  • 合成器不检索,只决定 Node 如何变成几次 LLM 调用
  • 默认 compact;逐块强迫阅读用 refine;综述用 tree_summarize;只评检索用 no_text
  • 模板与 filtering 改的是生成契约,召回名单不变。
  • 换 mode 救不了空召回——那是 Retriever 的事。

参考链接

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