LlamaIndex ResponseMode 深度解析

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 为什么能真正流式?
  1. 单一 prompt,单一 LLM 调用:不涉及多轮 refine
  2. 直接透传 TokenAsyncGenllm.astream() 返回的异步生成器直接作为返回值
  3. 无中间累积层 :DashScope token → llm.astream() yield → async for 接收,全链路无阻塞
  4. _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=Trueasync 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)。

解决

  1. 控制 top_k(建议 ≤ 5)
  2. 使用更小的切片策略(如句子级切片)
  3. 或改用 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 核心结论

  1. 流式输出 :唯一推荐 SIMPLE_SUMMARIZE
  2. 高质量非流式REFINE(少量片段)或 COMPACT(大量片段)
  3. 超长文档TREE_SUMMARIZE(但首字延迟高)
  4. 仅返回来源NO_TEXTCONTEXT_ONLY
  5. 纯 LLM 问答GENERATION(不是 RAG)

7.2 一句话选型

复制代码
流式 → SIMPLE_SUMMARIZE
非流式高质量 → REFINE / COMPACT
长文档 → TREE_SUMMARIZE
低成本 → NO_TEXT / CONTEXT_ONLY
相关推荐
AINative软件工程1 小时前
LLM API 成本失控怎么办?工程师的实时异常检测指南
python
阿pin1 小时前
Java随笔-红黑树
java·python·算法·红黑树
2603_965148112 小时前
eBay商品数据API:寻找海外仓与价格洼地
大数据·人工智能·windows·python·microsoft
2601_956319882 小时前
TqSdk Tick 数据适合做什么?获取方法与使用边界
人工智能·python
苏灿烤鱼2 小时前
把 Agent 做成一家公司,真比通用提示词好用吗?
python·agent·shell
北斗落凡尘12 小时前
LangGraph 入门实战(2)
python·langchain
ttod_qzstudio12 小时前
Java 常用语法极简通关(五):类与对象——字段、方法、构造器、this 与 static
java·开发语言·python
jufeng130714 小时前
【系列:手搓自主 AI Agent:Hermes 架构原理剖析 · 第 1 篇】
人工智能·python·架构·agent