
Jerry Liu • 2023-05-08
LLM 驱动的问答系统的新文档摘要索引
在这篇博文中,我们将介绍 LlamaIndex 全新的数据结构:文档摘要索引。我们将阐述与传统语义搜索相比,它如何帮助提供更好的检索性能,并演示一个示例。
背景
大型语言模型 (LLM) 的核心用例之一是在您自己的数据上进行问答。为此,我们将 LLM 与一个能够在知识语料库上执行信息检索的“检索”模型配对,并使用 LLM 对检索到的文本进行响应合成。这个整体框架称为检索增强生成(Retrieval-Augmented Generation)。
如今,大多数构建 LLM 驱动的问答系统的用户通常会采用以下形式的一些操作:
- 获取源文档,将每个文档分割成文本块
- 将文本块存储在向量数据库中
- 在查询时,通过嵌入相似度和/或关键词过滤器检索文本块。
- 执行响应合成
由于各种原因,这种方法提供的检索性能有限。
现有方法的局限性
使用文本块进行嵌入检索存在一些局限性。
- 文本块缺乏全局上下文。通常问题需要特定文本块中索引内容之外的上下文。
- 需要仔细调整 top-k / 相似度分数阈值。值设置得太小会错过上下文。值设置得太大,成本/延迟可能会随着更多不相关上下文的引入而增加。
- 嵌入并不总是能为问题选择最相关的上下文。嵌入本质上是文本和上下文之间独立确定的。
添加关键词过滤器是增强检索结果的一种方法。但这伴随着一系列挑战。我们需要充分确定每个文档的合适关键词,无论是手动还是通过 NLP 关键词提取/主题标注模型。此外,我们还需要从查询中充分推断出合适的关键词。
文档摘要索引

我们在 LlamaIndex 中提出一种新索引,该索引将为每个文档提取/索引一个非结构化文本摘要。与现有检索方法相比,此索引有助于增强检索性能。它比单个文本块索引的信息更多,并且比关键词标签包含更多的语义意义。它还允许更灵活的检索形式:我们可以进行 LLM 检索和基于嵌入的检索。
工作原理
在构建时,我们摄取每个文档,并使用 LLM 从每个文档中提取摘要。我们还将文档分割成文本块(节点)。摘要和节点都存储在我们的 文档存储 抽象层中。我们维护一个从摘要到源文档/节点的映射。
在查询时,我们使用以下方法根据文档摘要检索与查询相关的文档:
- 基于 LLM 的检索:我们将文档摘要集合呈现给 LLM,并要求 LLM 确定哪些文档是相关的以及它们的相关性分数。
- 基于嵌入的检索:我们根据摘要嵌入相似度(并进行 top-k 截断)检索相关文档。
请注意,这种文档摘要检索方法(即使是基于嵌入的方法)与基于文本块的嵌入检索不同。文档摘要索引的检索类会检索任何选定文档的所有节点,而不是在节点级别返回相关块。
存储文档摘要还可以启用基于 LLM 的检索。与其一开始就将整个文档提供给 LLM,我们可以先让 LLM 检查简洁的文档摘要,看看它是否与查询相关。这利用了 LLM 比基于嵌入查找更高级的推理能力,同时避免了将整个文档提供给 LLM 的成本/延迟。
附加洞察
使用摘要进行文档检索可以被视为语义搜索和对所有文档进行暴力摘要之间的“中间地带”。我们根据摘要与给定查询的相关性查找文档,然后返回与检索到的文档对应的所有*节点*。
为什么我们要这样做?这种检索方法通过在文档级别检索上下文,比对文本块进行 top-k 检索提供给用户更多上下文。而且,它也比主题建模更灵活/自动化;不再需要担心你的文本是否具有正确的关键词标签!
示例
让我们来看一个展示文档摘要索引的示例,它使用关于不同城市的维基百科文章。
本指南的其余部分展示了相关的代码片段。您可以在此处找到完整的演练(此处是 Notebook 链接)。
我们可以在一组文档上构建 GPTDocumentSummaryIndex
,并传入一个 ResponseSynthesizer
对象来为文档合成摘要。
from llama_index import (
SimpleDirectoryReader,
LLMPredictor,
ServiceContext,
ResponseSynthesizer
)
from llama_index.indices.document_summary import GPTDocumentSummaryIndex
from langchain.chat_models import ChatOpenAI
# load docs, define service context
...
# build the index
response_synthesizer = ResponseSynthesizer.from_args(response_mode="tree_summarize", use_async=True)
doc_summary_index = GPTDocumentSummaryIndex.from_documents(
city_docs,
service_context=service_context,
response_synthesizer=response_synthesizer
)
索引构建完成后,我们可以获取任何给定文档的摘要
summary = doc_summary_index.get_document_summary("Boston")
接下来,让我们来看一个基于 LLM 在索引上进行检索的示例。
from llama_index.indices.document_summary import DocumentSummaryIndexRetriever
retriever = DocumentSummaryIndexRetriever(
doc_summary_index,
# choice_select_prompt=choice_select_prompt,
# choice_batch_size=choice_batch_size,
# format_node_batch_fn=format_node_batch_fn,
# parse_choice_select_answer_fn=parse_choice_select_answer_fn,
# service_context=service_context
)
retrieved_nodes = retriever.retrieve("What are the sports teams in Toronto?")
print(retrieved_nodes[0].score)
print(retrieved_nodes[0].node.get_text())The retriever will retrieve a set of relevant nodes for a given index.
请注意,LLM 除了返回文档文本外,还返回相关性分数
8.0
Toronto ( (listen) tə-RON-toh; locally [təˈɹɒɾ̃ə] or [ˈtɹɒɾ̃ə]) is the capital city of the Canadian province of Ontario. With a recorded population of 2,794,356 in 2021, it is the most populous city in Canada...
我们还可以将该索引用作整个查询引擎的一部分,不仅检索相关上下文,还能合成给定问题的答案。这可以通过高级 API 和低级 API 来实现。
高级 API
query_engine = doc_summary_index.as_query_engine(
response_mode="tree_summarize", use_async=True
)
response = query_engine.query("What are the sports teams in Toronto?")
print(response)
低级 API
# use retriever as part of a query engine
from llama_index.query_engine import RetrieverQueryEngine
# configure response synthesizer
response_synthesizer = ResponseSynthesizer.from_args()
# assemble query engine
query_engine = RetrieverQueryEngine(
retriever=retriever,
response_synthesizer=response_synthesizer,
)
# query
response = query_engine.query("What are the sports teams in Toronto?")
print(response)
后续步骤
对任意文本进行自动摘要的方法非常令人兴奋。我们很高兴能在两个领域开发扩展功能:
- 继续探索不同层级的自动摘要。目前是在文档级别,但将一个大的文本块摘要成一个更小的块(例如,一句话)怎么样?
- 继续探索基于 LLM 的检索,摘要有助于解锁这种能力。
此外,我们在下面分享了示例指南/notebook,以防您在上文中错过: