LlamaIndex ResponseMode 深度解析:从源码看 8 种响应合成模式的区别、关联与最佳实践
摘要:LlamaIndex 提供了 8 种 ResponseMode(响应合成模式),但官方文档语焉不详。本文从源码级别深度解析每种模式的实现原理、适用场景、性能差异和隐藏坑点,帮助开发者在 RAG 项目中做出正确选择。涵盖 REFINE、COMPACT、SIMPLE_SUMMARIZE、TREE_SUMMARIZE、ACCUMULATE、COMPACT_ACCUMULATE、GENERATION、NO_TEXT、CONTEXT_ONLY 等全部模式。
文章目录
- [LlamaIndex ResponseMode 深度解析:从源码看 8 种响应合成模式的区别、关联与最佳实践](#LlamaIndex ResponseMode 深度解析:从源码看 8 种响应合成模式的区别、关联与最佳实践)
-
- [一、ResponseMode 概述](#一、ResponseMode 概述)
-
- [1.1 什么是 ResponseMode?](#1.1 什么是 ResponseMode?)
- [1.2 ResponseMode 枚举定义](#1.2 ResponseMode 枚举定义)
- [1.3 工厂方法创建合成器](#1.3 工厂方法创建合成器)
- [二、8 种模式深度解析](#二、8 种模式深度解析)
-
- [2.1 REFINE(迭代精炼模式)](#2.1 REFINE(迭代精炼模式))
-
- [2.1.1 核心思想](#2.1.1 核心思想)
- [2.1.2 工作流程](#2.1.2 工作流程)
- [2.1.3 源码实现](#2.1.3 源码实现)
- [2.1.4 性能特征](#2.1.4 性能特征)
- [2.1.5 致命坑点:流式失效](#2.1.5 致命坑点:流式失效)
- [2.1.6 使用场景](#2.1.6 使用场景)
- [2.2 COMPACT(压缩精炼模式)](#2.2 COMPACT(压缩精炼模式))
-
- [2.2.1 核心思想](#2.2.1 核心思想)
- [2.2.2 工作流程](#2.2.2 工作流程)
- [2.2.3 源码实现](#2.2.3 源码实现)
- [2.2.4 与 REFINE 的关系](#2.2.4 与 REFINE 的关系)
- [2.2.5 使用场景](#2.2.5 使用场景)
- [2.3 SIMPLE_SUMMARIZE(简单汇总模式)](#2.3 SIMPLE_SUMMARIZE(简单汇总模式))
-
- [2.3.1 核心思想](#2.3.1 核心思想)
- [2.3.2 工作流程](#2.3.2 工作流程)
- [2.3.3 源码实现](#2.3.3 源码实现)
- [2.3.4 为什么能真正流式?](#2.3.4 为什么能真正流式?)
- [2.3.5 性能特征](#2.3.5 性能特征)
- [2.3.6 使用场景](#2.3.6 使用场景)
- [2.4 TREE_SUMMARIZE(树形汇总模式)](#2.4 TREE_SUMMARIZE(树形汇总模式))
-
- [2.4.1 核心思想](#2.4.1 核心思想)
- [2.4.2 工作流程](#2.4.2 工作流程)
- [2.4.3 源码实现](#2.4.3 源码实现)
- [2.4.4 流式支持:条件性](#2.4.4 流式支持:条件性)
- [2.4.5 使用场景](#2.4.5 使用场景)
- [2.5 ACCUMULATE(累加模式)](#2.5 ACCUMULATE(累加模式))
-
- [2.5.1 核心思想](#2.5.1 核心思想)
- [2.5.2 工作流程](#2.5.2 工作流程)
- [2.5.3 源码实现](#2.5.3 源码实现)
- [2.5.4 使用场景](#2.5.4 使用场景)
- [2.6 COMPACT_ACCUMULATE(压缩累加模式)](#2.6 COMPACT_ACCUMULATE(压缩累加模式))
-
- [2.6.1 核心思想](#2.6.1 核心思想)
- [2.6.2 源码实现](#2.6.2 源码实现)
- [2.6.3 与 ACCUMULATE 的关系](#2.6.3 与 ACCUMULATE 的关系)
- [2.7 GENERATION(纯生成模式)](#2.7 GENERATION(纯生成模式))
-
- [2.7.1 核心思想](#2.7.1 核心思想)
- [2.7.2 源码实现](#2.7.2 源码实现)
- [2.7.3 使用场景](#2.7.3 使用场景)
- [2.8 NO_TEXT / CONTEXT_ONLY(无文本/仅上下文模式)](#2.8 NO_TEXT / CONTEXT_ONLY(无文本/仅上下文模式))
-
- [2.8.1 NO_TEXT](#2.8.1 NO_TEXT)
- [2.8.2 CONTEXT_ONLY](#2.8.2 CONTEXT_ONLY)
- [2.8.3 使用场景](#2.8.3 使用场景)
- [三、8 种模式对比总览](#三、8 种模式对比总览)
-
- [3.1 功能对比表](#3.1 功能对比表)
- [3.2 继承关系图](#3.2 继承关系图)
- [3.3 流式支持矩阵](#3.3 流式支持矩阵)
- 四、最佳实践与选型建议
-
- [4.1 流式接口(强烈推荐)](#4.1 流式接口(强烈推荐))
- [4.2 非流式接口 - 高质量优先](#4.2 非流式接口 - 高质量优先)
- [4.3 非流式接口 - 长文档总结](#4.3 非流式接口 - 长文档总结)
- [4.4 仅返回参考来源](#4.4 仅返回参考来源)
- [4.5 决策树](#4.5 决策树)
- 五、隐藏坑点与避坑指南
-
- [5.1 REFINE/COMPACT 流式失效](#5.1 REFINE/COMPACT 流式失效)
- [5.2 TREE_SUMMARIZE 伪流式](#5.2 TREE_SUMMARIZE 伪流式)
- [5.3 ACCUMULATE 显式禁止流式](#5.3 ACCUMULATE 显式禁止流式)
- [5.4 SIMPLE_SUMMARIZE 截断问题](#5.4 SIMPLE_SUMMARIZE 截断问题)
- [5.5 GENERATION 不是 RAG](#5.5 GENERATION 不是 RAG)
- 六、自定义响应合成器(进阶)
- 七、总结
-
- [7.1 核心结论](#7.1 核心结论)
- [7.2 一句话选型](#7.2 一句话选型)
一、ResponseMode 概述
1.1 什么是 ResponseMode?
在 LlamaIndex 的 RAG 流程中,检索器(Retriever)从向量数据库获取相关文档片段(Node)后,需要将这些片段"合成"为对用户的最终回答。这个"合成"策略就是 ResponseMode。
用户问题 → 检索器 → top-k 个 NodeWithScore → 响应合成器 → 最终回答
↑
ResponseMode 决定如何合成
1.2 ResponseMode 枚举定义
源码位置 :llama_index/core/response_synthesizers/type.py
python
class ResponseMode(str, enum.Enum):
"""Response modes of the response builder."""
REFINE = "refine"
COMPACT = "compact"
SIMPLE_SUMMARIZE = "simple_summarize"
TREE_SUMMARIZE = "tree_summarize"
GENERATION = "generation"
ACCUMULATE = "accumulate"
COMPACT_ACCUMULATE = "compact_accumulate"
NO_TEXT = "no_text"
CONTEXT_ONLY = "context_only"
1.3 工厂方法创建合成器
源码位置 :llama_index/core/response_synthesizers/factory.py
python
def get_response_synthesizer(
response_mode: ResponseMode = ResponseMode.COMPACT,
streaming: bool = False,
# ... 其他参数
) -> BaseSynthesizer:
"""Get a response synthesizer."""
if response_mode == ResponseMode.REFINE:
return Refine(...)
elif response_mode == ResponseMode.COMPACT:
return CompactAndRefine(...)
elif response_mode == ResponseMode.TREE_SUMMARIZE:
return TreeSummarize(...)
elif response_mode == ResponseMode.SIMPLE_SUMMARIZE:
return SimpleSummarize(...)
elif response_mode == ResponseMode.GENERATION:
return Generation(...)
elif response_mode == ResponseMode.ACCUMULATE:
return Accumulate(...)
elif response_mode == ResponseMode.COMPACT_ACCUMULATE:
return CompactAndAccumulate(...)
elif response_mode == ResponseMode.NO_TEXT:
return NoText(...)
elif response_mode == ResponseMode.CONTEXT_ONLY:
return ContextOnly(...)
二、8 种模式深度解析
2.1 REFINE(迭代精炼模式)
2.1.1 核心思想
逐个处理检索到的文档片段,每次用当前片段"精炼"之前的回答。
2.1.2 工作流程
问题:"阿里巴巴的创始人是谁?"
节点 1:"马云 1999 年在杭州创立了阿里巴巴..."
→ LLM:"阿里巴巴的创始人是马云。"
节点 2:"蔡崇信是阿里巴巴的联合创始人,负责财务..."
→ LLM:"阿里巴巴的创始人是马云,蔡崇信是联合创始人,负责财务..."
节点3: "彭蕾也是早期团队成员,曾任CEO..."
→ LLM: "阿里巴巴的创始人是马云,联合创始人包括蔡崇信(财务)和彭蕾(曾任CEO)..."
2.1.3 源码实现
源码位置 :llama_index/core/response_synthesizers/refine.py
python
class Refine(BaseSynthesizer):
"""Refine a response to a query across text chunks."""
async def aget_response(
self,
query_str: str,
text_chunks: Sequence[str],
prev_response: Optional[RESPONSE_TEXT_TYPE] = None,
**response_kwargs: Any,
) -> RESPONSE_TEXT_TYPE:
return await self._arun_refine_loop(
query_str=query_str,
text_chunks=text_chunks,
prev_response=prev_response,
**response_kwargs,
)
async def _arun_refine_loop(self, query_str, text_chunks, prev_response, ...):
response = prev_response
chunks_deque = deque(text_chunks)
while chunks_deque:
text_chunk = chunks_deque.popleft()
# 构建 refine prompt
if response is None:
# 第一个 chunk:使用 `text_qa_template`
prompt = self._text_qa_template.partial_format(
query_str=query_str,
context_str=text_chunk,
)
else:
# 后续 chunk:使用 `refine_template`
prompt = self._refine_template.partial_format(
query_str=query_str,
existing_answer=response,
context_msg=text_chunk,
)
# 调用 LLM
program = self._default_program_factory(prompt, ...)
response = await self._aget_new_response(
program, program_kwargs, response_kwargs
)
# 关键:如果 `response` 是异步生成器,必须完全消费
if isinstance(response, AsyncGenerator):
response = await aget_response_text(response)
return response
2.1.4 性能特征
- LLM 调用次数:N 次(N = 文档片段数)
- 首 Token 延迟:高(需等待前 N-1 次调用完成)
- 答案质量:理论上最高(逐片段深入分析)
- Token 消耗:高(每次调用都包含历史回答)
2.1.5 致命坑点:流式失效
源码问题 :DefaultRefineProgram.astream_call()(L161--200)
python
async def astream_call(self, *args, **kwds):
async def gen():
if self._output_cls is not None:
# 有 output_cls 时:可能真正流式
async for structured_answer in await self._llm.astream_structured_predict(...):
yield StructuredRefineResponse(answer=answer, query_satisfied=True)
else:
# ★ output_cls=None 时(常见场景):
answer = ""
async for token in await self._llm.astream(prompt, **kwds):
answer += token # ← 累积每一个 token!
if answer:
yield StructuredRefineResponse(
answer=answer.strip(), # ← 全部累积完才 yield 一次!
query_satisfied=True
)
return gen()
结论 :REFINE 模式在 LlamaIndex 0.14.x 中不支持真正的流式输出。
2.1.6 使用场景
- ✅ 非流式接口
- ✅ 需要深度分析多个文档片段
- ✅ 对答案质量要求极高
- ❌ 不适合流式输出
- ❌ 不适合实时性要求高的场景
2.2 COMPACT(压缩精炼模式)
2.2.1 核心思想
先使用 PromptHelper.repack() 将多个小片段合并为较大块(适配 LLM 上下文窗口),然后再执行 REFINE 流程。
2.2.2 工作流程
原始片段:[片段1, 片段2, 片段3, 片段4, 片段5]
↓ repack() 合并
压缩后:[合并块A(片段1+2+3), 合并块B(片段4+5)]
↓ REFINE 流程
LLM 调用 1:处理合并块 A → 回答 1
LLM 调用 2:用回答 1 + 合并块 B → 最终回答
2.2.3 源码实现
源码位置 :llama_index/core/response_synthesizers/compact_and_refine.py
python
class CompactAndRefine(Refine):
"""Refine responses across compact text chunks."""
async def aget_response(
self,
query_str: str,
text_chunks: Sequence[str],
prev_response: Optional[RESPONSE_TEXT_TYPE] = None,
**response_kwargs: Any,
) -> RESPONSE_TEXT_TYPE:
# 1. 压缩文本块
compact_texts = self._make_compact_text_chunks(query_str, text_chunks)
# 2. 调用父类 Refine 的 refine 循环
return await super().aget_response(
query_str=query_str,
text_chunks=compact_texts,
prev_response=prev_response,
**response_kwargs,
)
def _make_compact_text_chunks(
self, query_str: str, text_chunks: Sequence[str]
) -> List[str]:
# 获取最大的 prompt(text_qa_template 和 refine_template 中更大的那个)
text_qa_template = self._text_qa_template.partial_format(query_str=query_str)
refine_template = self._refine_template.partial_format(query_str=query_str)
max_prompt = get_biggest_prompt([text_qa_template, refine_template])
# 使用 prompt_helper.repack() 合并文本块
return self._prompt_helper.repack(
max_prompt, text_chunks, llm=self._llm, padding=self._response_padding_size
)
2.2.4 与 REFINE 的关系
COMPACT继承Refine类- 唯一区别:在调用 refine 循环前,先执行
repack()合并小片段 - 流式 bug 完全一样 (因为继承自
Refine)
2.2.5 使用场景
- ✅ 检索到大量小片段(如句子级切片)
- ✅ 需要减少 LLM 调用次数
- ✅ 非流式接口
- ❌ 不适合流式输出
2.3 SIMPLE_SUMMARIZE(简单汇总模式)
2.3.1 核心思想
将所有检索到的文档片段合并为一个字符串,截断适配 LLM 上下文窗口,然后调用 LLM 一次生成回答。
2.3.2 工作流程
问题: "阿里巴巴的创始人有哪些?"
片段1: "马云1999年在杭州创立了阿里巴巴..."
片段2: "蔡崇信是阿里巴巴的联合创始人..."
片段3: "彭蕾也是早期团队成员..."
↓ "\n".join() 合并
上下文: "马云1999年...蔡崇信是...彭蕾也是..."
↓ truncate() 截断适配窗口
截断后: "马云1999年...蔡崇信是...彭蕾也是..." (≤ 上下文窗口)
↓ llm.astream() 一次调用
回答: "阿里巴巴的创始人包括马云(主要创始人)、蔡崇信(联合创始人)和彭蕾(早期成员)..."
2.3.3 源码实现
源码位置 :llama_index/core/response_synthesizers/simple_summarize.py
python
class SimpleSummarize(BaseSynthesizer):
def __init__(
self,
llm: Optional[LLM] = None,
streaming: bool = False,
# ...
) -> None:
super().__init__(llm=llm, streaming=streaming, ...)
self._text_qa_template = text_qa_template or DEFAULT_TEXT_QA_PROMPT_SEL
async def aget_response(
self,
query_str: str,
text_chunks: Sequence[str],
**response_kwargs: Any,
) -> RESPONSE_TEXT_TYPE:
# 1. 构建 prompt 模板
text_qa_template = self._text_qa_template.partial_format(query_str=query_str)
# 2. 合并所有 chunk 为单一字符串
single_text_chunk = "\n".join(text_chunks)
# 3. 截断适配 LLM 上下文窗口
truncated_chunks = self._prompt_helper.truncate(
prompt=text_qa_template,
text_chunks=[single_text_chunk],
llm=self._llm,
)
# 4. ★ 关键:直接调用 llm.astream() 或 llm.apredict()
if self._streaming:
response = await self._llm.astream( # ← 返回 TokenAsyncGen
text_qa_template,
context_str=truncated_chunks,
**response_kwargs,
)
else:
response = await self._llm.apredict(
text_qa_template,
context_str=truncated_chunks,
**response_kwargs,
)
# 5. 返回 response(直接透传,无中间层)
if isinstance(response, str):
response = response or "Empty Response"
else:
response = cast(Generator, response)
return response
2.3.4 为什么能真正流式?
- 单一 prompt,单一 LLM 调用:不涉及多轮 refine
- 直接透传 TokenAsyncGen :
llm.astream()返回的异步生成器直接作为返回值 - 无中间累积层 :DashScope token →
llm.astream()yield →async for接收,全链路无阻塞 _prepare_response_output()正确识别 :isinstance(response_str, AsyncGenerator)→ 返回AsyncStreamingResponse
2.3.5 性能特征
- LLM 调用次数:1 次
- 首 token 延迟:低(~2-3 秒)
- 答案质量:受上下文窗口限制
- Token 消耗:低(仅 1 次调用)
- 流式支持:✅ 完美
2.3.6 使用场景
- ✅ 流式输出接口(唯一推荐)
- ✅ top_k ≤ 5(不超过上下文窗口)
- ✅ 实时性要求高
- ✅ 需要低首字延迟
- ❌ 片段过多时会截断丢失信息
2.4 TREE_SUMMARIZE(树形汇总模式)
2.4.1 核心思想
递归地将文档片段两两合并并总结,构建一棵"总结树",最终在根节点得到完整回答。
2.4.2 工作流程
原始片段: [A, B, C, D, E, F, G, H]
↓ 第 1 层总结
[A+B总结, C+D总结, E+F总结, G+H总结]
↓ 第 2 层总结
[(A+B)+(C+D)总结, (E+F)+(G+H)总结]
↓ 第 3 层总结
[(A+B+C+D)+(E+F+G+H)总结]
↓ 最终回答
完整总结
2.4.3 源码实现
源码位置 :llama_index/core/response_synthesizers/tree_summarize.py
python
class TreeSummarize(BaseSynthesizer):
"""
Tree summarize response builder.
This response builder recursively merges text chunks and summarizes them
in a bottom-up fashion (i.e. building a tree from leaves to root).
"""
async def aget_response(
self,
query_str: str,
text_chunks: Sequence[str],
**response_kwargs: Any,
) -> RESPONSE_TEXT_TYPE:
summary_template = self._summary_template.partial_format(query_str=query_str)
# 1. repack 文本块
text_chunks = self._prompt_helper.repack(
summary_template, text_chunks=text_chunks, llm=self._llm
)
if self._verbose:
print(f"{len(text_chunks)} text chunks after repacking")
# 2. 如果只剩一个 chunk,直接返回最终回答
if len(text_chunks) == 1:
response: RESPONSE_TEXT_TYPE
if self._streaming:
response = await self._llm.astream( # ← 只有这里支持流式
summary_template, context_str=text_chunks[0], **response_kwargs
)
else:
response = await self._llm.apredict(
summary_template,
context_str=text_chunks[0],
**response_kwargs,
)
return response
# 3. 否则,递归总结每个 chunk
else:
str_tasks = [
self._llm.apredict(
summary_template,
context_str=text_chunk,
**response_kwargs,
)
for text_chunk in text_chunks
]
summaries = await asyncio.gather(*str_tasks)
# 4. 递归调用
return await self.aget_response(
query_str=query_str,
text_chunks=summaries,
**response_kwargs,
)
2.4.4 流式支持:条件性
关键代码:
python
if len(text_chunks) == 1:
if self._streaming:
response = await self._llm.astream(...) # ✅ 流式
else:
response = await self._llm.apredict(...)
else:
# ❌ 非流式:并行调用 llm.apredict() 总结每个 chunk
str_tasks = [self._llm.apredict(...) for text_chunk in text_chunks]
summaries = await asyncio.gather(*str_tasks)
return await self.aget_response(...) # 递归
结论:
- 只有递归到最后一层(只剩 1 个 chunk)时才支持流式
- 前面的层都是非流式并行调用
- 首 token 延迟 = 所有中间层调用时间 + 最后一层首 token 时间
2.4.5 使用场景
- ✅ 超长文档总结(如整本书)
- ✅ 多层级信息汇总
- ✅ 非流式接口
- ❌ 不适合流式输出(前面层不流式)
- ❌ 不适合实时性要求高的场景
2.5 ACCUMULATE(累加模式)
2.5.1 核心思想
对每个文档片段独立调用 LLM 生成回答,然后将所有回答拼接起来。
2.5.2 工作流程
问题: "阿里巴巴的创始人有哪些?"
片段1: "马云1999年在杭州创立了阿里巴巴..."
→ LLM1: "创始人是马云。"
片段2: "蔡崇信是阿里巴巴的联合创始人..."
→ LLM2: "联合创始人是蔡崇信。"
片段3: "彭蕾也是早期团队成员..."
→ LLM3: "彭蕾是早期成员。"
最终回答: "Response 1: 创始人是马云。\nResponse 2: 联合创始人是蔡崇信。\nResponse 3: 彭蕾是早期成员。"
2.5.3 源码实现
源码位置 :llama_index/core/response_synthesizers/accumulate.py
python
class Accumulate(BaseSynthesizer):
"""Accumulate responses from multiple text chunks."""
async def aget_response(
self,
query_str: str,
text_chunks: Sequence[str],
separator: str = "\n---------------------\n",
**response_kwargs: Any,
) -> RESPONSE_TEXT_TYPE:
# ★ 显式禁止流式
if self._streaming:
raise ValueError("Unable to stream in Accumulate response mode")
# 并行调用 LLM 处理每个 chunk
tasks = [
self._give_responses(
query_str, text_chunk, use_async=True, **response_kwargs
)
for text_chunk in text_chunks
]
flattened_tasks = self.flatten_list(tasks)
outputs = await asyncio.gather(*flattened_tasks)
# 格式化输出
return self._format_response(outputs, separator)
def _format_response(self, outputs: List[Any], separator: str) -> str:
responses: List[str] = []
for response in outputs:
responses.append(response or "Empty Response")
return separator.join(
[f"Response {index + 1}: {item}" for index, item in enumerate(responses)]
)
2.5.4 使用场景
- ✅ 需要保留每个片段的独立观点
- ✅ 对比分析多个来源
- ✅ 非流式接口
- ❌ 显式禁止流式 (
raise ValueError)
2.6 COMPACT_ACCUMULATE(压缩累加模式)
2.6.1 核心思想
先合并小片段,再执行 ACCUMULATE 流程。
2.6.2 源码实现
源码位置 :llama_index/core/response_synthesizers/compact_and_accumulate.py
python
class CompactAndAccumulate(Accumulate):
"""Accumulate responses across compact text chunks."""
async def aget_response(
self,
query_str: str,
text_chunks: Sequence[str],
separator: str = "\n---------------------\n",
**response_kwargs: Any,
) -> RESPONSE_TEXT_TYPE:
text_qa_template = self._text_qa_template.partial_format(query_str=query_str)
with temp_set_attrs(self._prompt_helper):
# 先合并小片段
new_texts = self._prompt_helper.repack(
text_qa_template, text_chunks, llm=self._llm
)
# 再调用父类 Accumulate
return await super().aget_response(
query_str=query_str,
text_chunks=new_texts,
separator=separator,
**response_kwargs,
)
2.6.3 与 ACCUMULATE 的关系
- 继承
Accumulate - 唯一区别:调用前先
repack()合并 - 流式限制一样 (继承自
Accumulate)
2.7 GENERATION(纯生成模式)
2.7.1 核心思想
完全忽略检索到的文档片段,直接让 LLM 根据问题生成回答。
2.7.2 源码实现
源码位置 :llama_index/core/response_synthesizers/generation.py
python
class Generation(BaseSynthesizer):
def __init__(
self,
llm: Optional[LLM] = None,
streaming: bool = False,
# ...
) -> None:
super().__init__(llm=llm, streaming=streaming, ...)
self._input_prompt = simple_template or DEFAULT_SIMPLE_INPUT_PROMPT
def get_response(
self,
query_str: str,
text_chunks: Sequence[str],
**response_kwargs: Any,
) -> RESPONSE_TEXT_TYPE:
del text_chunks # ★ 忽略所有检索内容
if not self._streaming:
return self._llm.predict(
self._input_prompt,
query_str=query_str,
**response_kwargs,
)
else:
return self._llm.stream(
self._input_prompt,
query_str=query_str,
**response_kwargs,
)
2.7.3 使用场景
- ✅ 纯 LLM 问答(不需要检索)
- ✅ 支持流式输出
- ❌ 不是 RAG(不使用检索内容)
- ❌ 可能产生幻觉
2.8 NO_TEXT / CONTEXT_ONLY(无文本/仅上下文模式)
2.8.1 NO_TEXT
不生成任何文本回答,仅返回检索到的来源节点。
python
class NoText(BaseSynthesizer):
async def aget_response(self, query_str, text_chunks, **kwargs):
return "" # 空回答
2.8.2 CONTEXT_ONLY
仅返回拼接的上下文,不调用 LLM。
python
class ContextOnly(BaseSynthesizer):
async def aget_response(self, query_str, text_chunks, **kwargs):
return "\n".join(text_chunks) # 仅返回原始文本
2.8.3 使用场景
- ✅ 仅需要参考来源,不需要 LLM 总结
- ✅ 节省 LLM 调用成本
- ✅ 支持流式(直接返回文本)
三、8 种模式对比总览
3.1 功能对比表
| 模式 | LLM 调用次数 | 流式支持 | 首字延迟 | 答案质量 | 适合场景 |
|---|---|---|---|---|---|
| REFINE | N 次 | ❌ 坏了 | 高 | 高 | 非流式深度分析 |
| COMPACT | N/2~N/3 次 | ❌ 坏了 | 高 | 高 | 大量小片段非流式 |
| SIMPLE_SUMMARIZE | 1 次 | ✅ 完美 | 低 | 中 | 流式输出 |
| TREE_SUMMARIZE | log₂N 层 | ⚠️ 条件性 | 很高 | 高 | 超长文档总结 |
| ACCUMULATE | N 次 | ❌ 禁止 | 中 | 中 | 独立观点对比 |
| COMPACT_ACCUMULATE | N/2~N/3 次 | ❌ 禁止 | 中 | 中 | 大量小片段对比 |
| GENERATION | 1 次 | ✅ 可以 | 低 | 低 | 纯 LLM 问答 |
| NO_TEXT | 0 次 | ✅ 可以 | 无 | 无 | 仅返回来源 |
| CONTEXT_ONLY | 0 次 | ✅ 可以 | 无 | 无 | 仅返回上下文 |
3.2 继承关系图
BaseSynthesizer
├── Refine
│ └── CompactAndRefine (COMPACT)
├── SimpleSummarize (SIMPLE_SUMMARIZE)
├── TreeSummarize (TREE_SUMMARIZE)
├── Accumulate (ACCUMULATE)
│ └── CompactAndAccumulate (COMPACT_ACCUMULATE)
├── Generation (GENERATION)
├── NoText (NO_TEXT)
└── ContextOnly (CONTEXT_ONLY)
3.3 流式支持矩阵
| 模式 | 是否支持流式 | 原因 |
|---|---|---|
| REFINE | ❌ | DefaultRefineProgram.astream_call() 累积 token |
| COMPACT | ❌ | 继承 Refine,同样 bug |
| SIMPLE_SUMMARIZE | ✅ | 直接调用 llm.astream() |
| TREE_SUMMARIZE | ⚠️ | 只有最后一层流式 |
| ACCUMULATE | ❌ | 显式 raise ValueError |
| COMPACT_ACCUMULATE | ❌ | 继承 Accumulate |
| GENERATION | ✅ | 直接调用 llm.stream() |
| NO_TEXT | ✅ | 无 LLM 调用 |
| CONTEXT_ONLY | ✅ | 无 LLM 调用 |
四、最佳实践与选型建议
4.1 流式接口(强烈推荐)
python
# ✅ 唯一推荐:SIMPLE_SUMMARIZE
query_engine = RetrieverQueryEngine.from_args(
retriever=retriever,
response_mode=ResponseMode.SIMPLE_SUMMARIZE,
streaming=True,
)
response = await query_engine.aquery(query)
async for text in response.response_gen:
yield text
理由:
- 唯一真正支持逐 token 流式的模式
- 首字延迟低(~2-3 秒)
- Token 消耗少(仅 1 次 LLM 调用)
限制:
- top_k 不宜过大(建议 ≤ 5)
- 片段过多时会截断
4.2 非流式接口 - 高质量优先
python
# ✅ REFINE(适合少量片段)
query_engine = RetrieverQueryEngine.from_args(
retriever=retriever,
response_mode=ResponseMode.REFINE,
)
# ✅ COMPACT(适合大量小片段)
query_engine = RetrieverQueryEngine.from_args(
retriever=retriever,
response_mode=ResponseMode.COMPACT,
)
4.3 非流式接口 - 长文档总结
python
# ✅ TREE_SUMMARIZE(适合整本书/长报告)
query_engine = RetrieverQueryEngine.from_args(
retriever=retriever,
response_mode=ResponseMode.TREE_SUMMARIZE,
)
4.4 仅返回参考来源
python
# ✅ NO_TEXT(不需要 LLM 总结)
query_engine = RetrieverQueryEngine.from_args(
retriever=retriever,
response_mode=ResponseMode.NO_TEXT,
)
# 或 CONTEXT_ONLY(返回拼接的上下文)
query_engine = RetrieverQueryEngine.from_args(
retriever=retriever,
response_mode=ResponseMode.CONTEXT_ONLY,
)
4.5 决策树
需要流式输出?
├── 是 → SIMPLE_SUMMARIZE ✅
└── 否 → 片段数量?
├── ≤ 5 → REFINE ✅
├── 6~20 → COMPACT ✅
└── > 20 → TREE_SUMMARIZE ✅
需要保留独立观点?
├── 是 → ACCUMULATE ✅
└── 否 → 继续判断
不需要 LLM 总结?
├── 是 → NO_TEXT 或 CONTEXT_ONLY ✅
└── 否 → 继续判断
纯 LLM 问答(不用检索)?
├── 是 → GENERATION ✅
└── 否 → 回到顶部重新判断
五、隐藏坑点与避坑指南
5.1 REFINE/COMPACT 流式失效
坑点 :即使设置 streaming=True,async for 仍然一次性返回完整回答。
根因 :DefaultRefineProgram.astream_call() 累积所有 token(详见本文 2.1.5 节)。
解决 :改用 SIMPLE_SUMMARIZE。
5.2 TREE_SUMMARIZE 伪流式
坑点 :虽然支持 streaming=True,但只有最后一层流式,前面层都是非流式。
根因 :中间层使用 asyncio.gather(*str_tasks) 并行调用 apredict()。
解决 :如果对首字延迟敏感,不要用 TREE_SUMMARIZE。
5.3 ACCUMULATE 显式禁止流式
坑点 :设置 streaming=True 会直接抛异常。
源码:
python
if self._streaming:
raise ValueError("Unable to stream in Accumulate response mode")
解决 :不要对 ACCUMULATE 设置 streaming=True。
5.4 SIMPLE_SUMMARIZE 截断问题
坑点 :片段过多时,truncate() 会截断丢失信息。
根因:LLM 上下文窗口有限(如 8K/32K token)。
解决:
- 控制 top_k(建议 ≤ 5)
- 使用更小的切片策略(如句子级切片)
- 或改用
TREE_SUMMARIZE(非流式场景)
5.5 GENERATION 不是 RAG
坑点 :GENERATION 模式完全忽略检索内容,可能产生幻觉。
源码:
python
def get_response(self, query_str, text_chunks, **kwargs):
del text_chunks # ← 忽略所有检索内容!
return self._llm.predict(self._input_prompt, query_str=query_str)
解决 :除非你明确知道不需要检索,否则不要用 GENERATION。
六、自定义响应合成器(进阶)
如果你需要特殊的合成策略,可以继承 BaseSynthesizer 自定义:
python
from llama_index.core.response_synthesizers.base import BaseSynthesizer
from llama_index.core.types import RESPONSE_TEXT_TYPE
from llama_index.core import Settings
class CustomStreamingSynthesizer(BaseSynthesizer):
"""自定义流式合成器"""
def __init__(self, text_qa_template=None):
super().__init__(llm=Settings.llm, streaming=True)
self._text_qa_template = text_qa_template or self._default_template
async def aget_response(
self,
query_str: str,
text_chunks: Sequence[str],
**response_kwargs: Any,
) -> RESPONSE_TEXT_TYPE:
# 自定义逻辑
context_str = self._custom_merge(text_chunks)
prompt = self._text_qa_template.partial_format(
query_str=query_str,
context_str=context_str,
)
return await self._llm.astream(prompt, **response_kwargs)
七、总结
7.1 核心结论
- 流式输出 :唯一推荐
SIMPLE_SUMMARIZE - 高质量非流式 :
REFINE(少量片段)或COMPACT(大量片段) - 超长文档 :
TREE_SUMMARIZE(但首字延迟高) - 仅返回来源 :
NO_TEXT或CONTEXT_ONLY - 纯 LLM 问答 :
GENERATION(不是 RAG)
7.2 一句话选型
流式 → SIMPLE_SUMMARIZE
非流式高质量 → REFINE / COMPACT
长文档 → TREE_SUMMARIZE
低成本 → NO_TEXT / CONTEXT_ONLY