主题
数据已加载、索引已建好、索引也存下来备用了,现在可以进入 LLM 应用最关键的一环:查询。
最简单的情形下,查询就是向 LLM 发一次提示词调用:可以是有问有答,可以是请求摘要,也可以是复杂得多的指令。
更复杂的查询会涉及反复/链式的提示词 + LLM 调用,甚至在多个组件之间走一轮推理循环。
入门
一切查询的基础是 QueryEngine。获得 QueryEngine 最省事的办法是让索引替你创建一个,就像这样:
query_engine = index.as_query_engine()
response = query_engine.query(
"Write an email to the user given their background information."
)
print(response)1
2
3
4
5
2
3
4
5
查询的阶段
不过,查询并不像看上去那么直白,它由三个各不相同的阶段组成:
- 检索(Retrieval):从你的
Index中找到并返回与查询最相关的文档。如前文索引所讨论的,最常见的检索方式是“top-k”语义检索,但检索策略远不止这一种。 - 后处理(Postprocessing):对检索到的
Node可选地做重排、变换或过滤,例如要求它们带有特定的元数据(如关键词)。 - 响应合成(Response synthesis):把查询、最相关的数据和提示词组合起来交给 LLM,由它给出回答。
定制查询的各个阶段
LlamaIndex 提供了一套低层组合 API,让你对查询拥有细粒度的控制。
这个例子里,我们把检索器的 top_k 改成另一个数值,并加一道后处理:只有达到最低相似度分数的节点才会被保留。这样一来,有相关结果时你会拿到很多数据,而没有任何相关内容时可能一条都拿不到。
from llama_index.core import VectorStoreIndex, get_response_synthesizer
from llama_index.core.retrievers import VectorIndexRetriever
from llama_index.core.query_engine import RetrieverQueryEngine
from llama_index.core.postprocessor import SimilarityPostprocessor
# build index
index = VectorStoreIndex.from_documents(documents)
# configure retriever
retriever = VectorIndexRetriever(
index=index,
similarity_top_k=10,
)
# configure response synthesizer
response_synthesizer = get_response_synthesizer()
# assemble query engine
query_engine = RetrieverQueryEngine(
retriever=retriever,
response_synthesizer=response_synthesizer,
node_postprocessors=[SimilarityPostprocessor(similarity_cutoff=0.7)],
)
# query
response = query_engine.query("What did the author do growing up?")
print(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
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
你也可以通过实现相应的接口,加入自己的检索、响应合成以及整体的查询逻辑。
已实现的组件清单与支持的配置项,请看我们的参考文档。
下面逐个步骤细说怎么定制:
配置检索器
retriever = VectorIndexRetriever(
index=index,
similarity_top_k=10,
)1
2
3
4
2
3
4
检索器的种类非常多,可以在我们的检索器模块指南里逐一了解。
配置节点后处理器
我们支持进阶的 Node 过滤与增强,可以进一步提升检索到的 Node 对象的相关性。这有助于减少 LLM 调用的次数与耗时(也就是成本),或者提升回答质量。
例如:
KeywordNodePostprocessor:按required_keywords与exclude_keywords过滤节点。SimilarityPostprocessor:通过设定相似度分数阈值来过滤节点(因此仅适用于基于嵌入的检索器)PrevNextNodePostprocessor:依据Node之间的关联关系,为检索到的Node补充额外的相关上下文。
节点后处理器的完整清单记录在节点后处理器参考里。
按需要配置节点后处理器:
node_postprocessors = [
KeywordNodePostprocessor(
required_keywords=["Combinator"], exclude_keywords=["Italy"]
)
]
query_engine = RetrieverQueryEngine.from_args(
retriever, node_postprocessors=node_postprocessors
)
response = query_engine.query("What did the author do growing up?")1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
配置响应合成
检索器取回相关节点之后,由 BaseSynthesizer 汇总这些信息,合成最终回答。
可以通过以下方式配置它:
query_engine = RetrieverQueryEngine.from_args(
retriever, response_mode=response_mode
)1
2
3
2
3
目前我们支持以下选项:
default:依次遍历每个检索到的Node,以“先生成再修正”的方式作答;每个 Node 都会单独调用一次 LLM。适合更详细的回答。compact:在每次 LLM 调用时“压缩”提示词,把尽可能多的Node文本块塞进提示词长度上限之内。如果块太多塞不进一个提示词,就用多个提示词走“先生成再修正”。tree_summarize:给定一组Node对象和查询,递归构造一棵树,并把根节点作为最终回答。适合做摘要。no_text:只运行检索器,取回本来要发给 LLM 的节点,但并不真的发送。之后可以通过response.source_nodes查看。响应对象的更多细节见第 5 节。accumulate:给定一组Node对象和查询,把该查询分别应用到每个Node文本块上,并把回答累积到一个数组里。返回所有回答拼接后的字符串。适合需要把同一个查询逐块分别执行的情形。
结构化输出
你可能希望输出是结构化的。如何从查询引擎类中抽取一个 Pydantic 对象,见我们的 Query Engines + Pydantic 输出。
也别忘了看我们完整的结构化输出指南。
创建你自己的查询工作流
想要设计复杂的查询流程,可以在众多模块之间自由组合出自己的查询工作流——从提示词/LLM/输出解析器,到检索器、响应合成器,再到你自己的自定义组件。
更多细节见我们的工作流指南。