主题
概念
响应合成器(Response Synthesizer)负责基于用户查询和给定的一组文本块,让 LLM 生成回答。它的输出是一个 Response 对象。
具体做法形态各异,简单到只是遍历文本块,复杂到构建一棵树。核心思想是简化「用 LLM 跨你的数据生成回答」这一过程。
在查询引擎中使用时,响应合成器运行在检索器取回节点、以及各节点后处理器执行之后。
提示
不确定响应合成器在 RAG 流程中的位置?读一读高层概念
使用方式
单独使用响应合成器:
from llama_index.core.data_structs import Node
from llama_index.core.response_synthesizers import ResponseMode
from llama_index.core import get_response_synthesizer
response_synthesizer = get_response_synthesizer(
response_mode=ResponseMode.COMPACT
)
response = response_synthesizer.synthesize(
"query text", nodes=[Node(text="text"), ...]
)1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
或者在建好索引之后放进查询引擎里使用:
query_engine = index.as_query_engine(response_synthesizer=response_synthesizer)
response = query_engine.query("query_text")1
2
2
所有可用的响应合成器、模式,以及如何自建合成器,细节见下文。
使用方式
开始使用
通过 response_mode 为查询引擎配置响应合成器:
from llama_index.core.data_structs import Node
from llama_index.core.schema import NodeWithScore
from llama_index.core import get_response_synthesizer
response_synthesizer = get_response_synthesizer(response_mode="compact")
response = response_synthesizer.synthesize(
"query text", nodes=[NodeWithScore(node=Node(text="text"), score=1.0), ...]
)1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
或者更常见的,在建好索引之后放进查询引擎:
query_engine = index.as_query_engine(response_synthesizer=response_synthesizer)
response = query_engine.query("query_text")1
2
2
提示
如何构建索引,见索引(Indexing)
配置响应模式
响应合成器通常通过 response_mode 这个关键字参数来指定。
LlamaIndex 已经实现了若干种响应合成器:
refine:依次遍历每个检索到的文本块来先生成再精修回答。每个 Node/检索块都会单独调用一次 LLM。**细节:**先用
text_qa_template提示词对第一个块发起查询。然后把已得回答连同下一个块(以及原始问题)一起,用refine_template提示词再发起一次查询。如此往复,直到所有块都处理完毕。如果某个块(考虑提示词长度)大到装不进上下文窗口,就会用
TokenTextSplitter把它切开(块之间允许一定的文本重叠),切出的新块会作为原块集合的一部分被纳入处理(因而同样用refine_template查询)。适合需要更详细回答的场合。
compact(默认):与refine类似,但会事先压缩(拼接)文本块,从而减少 LLM 调用次数。**细节:**在上下文窗口之内(考虑
text_qa_template与refine_template之间的最大提示词长度)尽可能多地塞入文本(由检索块拼接/打包而来)。如果文本长到装不进一个提示词,就按需要拆成尽可能多的部分(用TokenTextSplitter拆分,因而文本块之间会有一些重叠)。每个文本部分都被视作一个“块”,依次交给
refine合成器处理。简言之,它和
refine类似,但 LLM 调用更少。tree_summarize:用summary_template提示词按需多次查询 LLM,直到全部拼接后的块都被查询完,得到若干回答;这些回答本身又作为块,递归地继续tree_summarize的 LLM 调用,直到只剩最后一块、也就只剩一个最终回答。**细节:**先用
summary_template提示词把块尽可能多地拼接以填满上下文窗口,需要时再切开(同样用TokenTextSplitter,保留一些文本重叠)。然后把切出的每个块/切片按summary_template查询(这里没有 精修 查询!),得到相应数量的回答。如果只有一个回答(也就是只有一个块),那它就是最终回答。
如果有多个回答,它们会作为块被递归送回
tree_summarize流程(拼接/切分到填满窗口后再查询)。适合做摘要。
simple_summarize:把所有文本块截断以塞进一个 LLM 提示词。适合快速摘要,但因截断而可能丢失细节。no_text:只运行检索器,取回本来要发给 LLM 的节点,但并不真的发送。之后可以通过response.source_nodes查看。context_only:返回所有文本块拼接后的字符串。accumulate:给定一组文本块和查询,把该查询分别应用到每个文本块上,并把回答累积到一个数组里。返回所有回答拼接后的字符串。适合需要把同一个查询逐块分别执行的情形。compact_accumulate:与accumulate相同,但会像compact那样“压缩”每个 LLM 提示词,并对每个文本块运行同一个查询。
自定义响应合成器
每个响应合成器都继承自 llama_index.response_synthesizers.base.BaseSynthesizer。基类 API 极其简单,因此自己写一个响应合成器也很容易。
也许你想定制 tree_summarize 每一步使用哪个模板,也许某篇新论文提出了一种生成回答的新方法——你都可以创建自己的响应合成器,插进任意查询引擎,或者单独使用。
下面展示 __init__() 函数,以及每个响应合成器都必须实现的两个抽象方法。基本要求就是:处理一个查询和若干文本块,并返回一个字符串(或字符串生成器)形式的回答。
from llama_index.core import Settings
class BaseSynthesizer(ABC):
"""Response builder class."""
def __init__(
self,
llm: Optional[LLM] = None,
streaming: bool = False,
) -> None:
"""Init params."""
self._llm = llm or Settings.llm
self._callback_manager = Settings.callback_manager
self._streaming = streaming
@abstractmethod
def get_response(
self,
query_str: str,
text_chunks: Sequence[str],
**response_kwargs: Any,
) -> RESPONSE_TEXT_TYPE:
"""Get response."""
...
@abstractmethod
async def aget_response(
self,
query_str: str,
text_chunks: Sequence[str],
**response_kwargs: Any,
) -> RESPONSE_TEXT_TYPE:
"""Get response."""
...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
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
使用结构化回答过滤
在使用 "refine" 或 "compact" 响应合成模块时,试试 structured_answer_filtering 选项可能对你有益。
from llama_index.core import get_response_synthesizer
response_synthesizer = get_response_synthesizer(structured_answer_filtering=True)1
2
3
2
3
把 structured_answer_filtering 设为 True 后,我们的 refine 模块就能过滤掉与当前问题无关的输入节点。对于「针对用户查询从外部向量库检索文本块」的 RAG 问答系统,这一点尤其有用。
如果你用的是支持函数调用的 OpenAI 模型,这个选项特别好用。其他没有原生函数调用支持的 LLM 服务商或模型,在产出这项功能所依赖的结构化回答时可能没那么可靠。
使用自定义提示词模板(并附加变量)
你可能想定制响应合成器所用的提示词,并在查询时额外传入一些变量。
这些额外的变量可以通过 get_response 的 **kwargs 指定。
例如:
from llama_index.core import PromptTemplate
from llama_index.core.response_synthesizers import TreeSummarize
# NOTE: we add an extra tone_name variable here
qa_prompt_tmpl = (
"Context information is below.\n"
"---------------------\n"
"{context_str}\n"
"---------------------\n"
"Given the context information and not prior knowledge, "
"answer the query.\n"
"Please also write the answer in the tone of {tone_name}.\n"
"Query: {query_str}\n"
"Answer: "
)
qa_prompt = PromptTemplate(qa_prompt_tmpl)
# initialize response synthesizer
summarizer = TreeSummarize(verbose=True, summary_template=qa_prompt)
# get response
response = summarizer.get_response(
"who is Paul Graham?", [text], tone_name="a Shakespeare play"
)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
模块
更多细节见完整的模块指南。