
Jerry Liu • 2023-07-04
LlamaIndex 0.7.0:更好地支持自底向上构建大型语言模型应用
几个月前,我们发布了 LlamaIndex 0.6.0,其中对代码库进行了大量重写,使我们的库更加模块化、可定制,并且对初学者和高级用户都更易于使用。
- 我们创建了模块化的存储抽象(数据、索引)和计算抽象(检索器、查询引擎)。
- 我们创建了一个低层 API,用户可以独立使用我们的模块(检索器、查询引擎),并将其作为大型系统的一部分进行定制。
今天,我们很高兴推出 LlamaIndex 0.7.0。我们的最新版本延续了在低层改进模块化/可定制性的主题,以支持自底向上构建针对您数据的 LLM 应用。您现在对使用关键抽象有了更多控制:LLM、我们的响应合成器以及我们的 Document 和 Node 对象。
- 我们创建了独立的 LLM 抽象(OpenAI、HuggingFace、PaLM)。
- 我们将响应合成模块做成了一个独立模块,您可以完全独立于我们其他抽象来使用它——摆脱了试图将上下文放入上下文窗口的提示样板代码。
- 我们为 Document/Node 对象添加了丰富的元数据管理功能——现在您可以完全控制要注入文档中的上下文。
下面,我们将更详细地描述每个部分。我们还在底部列出了完整的破坏性变更列表。
独立 LLM 抽象
我们为 OpenAI、HuggingFace 和 PaLM 创建了独立的 LLM 抽象。这些抽象可以单独使用,也可以作为现有 LlamaIndex 系统(查询引擎、检索器)的一部分使用。
高层动机
我们这样做的原因有多个
- 代码库中更清晰的抽象。在此之前,我们的
LLMPredictor
类与底层的 LangChain LLM 类存在大量泄漏的抽象。这使得我们的 LLM 抽象难以理解,也难以定制。 - 稍微更清晰的开发用户体验。之前,如果您想自定义默认 LLM(例如,使用“text-davinci-003”),您必须导入正确的 LangChain 类,将其包装在我们的 LLMPredictor 中,然后将其传递给 ServiceContext。现在,只需导入我们的 LLM 抽象(它已在我们的文档中原生记录)并将其插入 ServiceContext 即可。当然,如果您愿意,仍然可以使用 LangChain 的 LLM。
- 有利于自底向上开发:在将这些 LLM 模块插入到更大的系统之前,独立地试用它们是有意义的。这反映了我们在 0.7.0 版本中更大的推动力,即让用户能够组合自己的工作流程。
独立使用
我们的 LLM 抽象支持 complete
和 chat
端点。主要区别在于 complete
设计用于接收简单的字符串输入,并输出一个 CompletionResponse
(包含文本输出 + 附加字段)。chat
接收一个 ChatMessage
并输出一个 ChatResponse
(包含聊天消息 + 附加字段)。
这些 LLM 端点还通过 stream_complete
和 stream_chat
原生支持流式传输。
以下是关于如何独立使用 LLM 抽象的内容
from llama_index.llms import OpenAI
# using complete endpoint
resp = OpenAI().complete('Paul Graham is ')
print(resp)
# get raw object
resp_raw = resp.raw
# using chat endpoint
from llama_index.llms import ChatMessage, OpenAI
messages = [
ChatMessage(role='system', content='You are a pirate with a colorful personality'),
ChatMessage(role='user', content='What is your name')
]
resp = OpenAI().chat(messages)
print(resp)
# get raw object
resp_raw = resp.raw
# using streaming endpoint
from llama_index.llms import OpenAI
llm = OpenAI()
resp = llm.stream_complete('Paul Graham is ')
for delta in resp:
print(delta, end='')
以下是如何将 LLM 抽象作为整个 LlamaIndex 系统的一部分使用的方法。
from llama_index.llms import OpenAI
from llama_index.indices.service_context import ServiceContext
from llama_index import VectorStoreIndex
llm = OpenAI(model='gpt-3.5-turbo', temperature=0)
service_context = ServiceContext.from_defaults(llm=llm)
index = VectorStoreIndex.from_documents(docs, service_context=service_context)
response = index.as_query_engine().query("<question>")
注意:我们的顶层 LLMPredictor
仍然存在,但用户可见性较低(未来可能会弃用)。此外,您仍然可以通过我们的 LangChainLLM
类使用 LangChain LLM。
资源
我们的所有 Notebook 默认已更新为使用我们原生的 OpenAI LLM 集成。以下是一些资源,展示了如何独立使用 LLM 抽象以及如何在整个系统中使用它
独立响应合成模块
背景
在任何 RAG 系统中,都包含检索和合成。合成组件的职责是接收输入的上下文,并使用 LLM 合成响应。
从根本上说,合成模块需要针对任何上下文列表合成响应,无论上下文列表有多长。这本质上是 LLM 开发者 / “AI 工程师” 必须编写的“样板”代码。
我们之前在 LlamaIndex 内部有一个抽象(作为 ResponseSynthesizer
),但面向外部的用户体验对用户不友好。实际收集响应的部分(ResponseBuilder
)很难定制,而 ResponseSynthesizer
本身也增加了一个额外的非必要层。
现在我们有一组可以轻松导入的独立模块。以前,当您在查询引擎中设置 response_mode
时,这些模块会自动为您设置。现在它们更加直接可用,并面向用户。
以下是 llama_index.response_synthesizer
中所有新的 Response Synthesiszer
模块列表
Refine
- 查询 LLM,逐个发送文本块。第一次 LLM 调用后,现有答案也会发送给 LLM,以便使用下一个文本块进行更新和细化。Accumulate
- 使用相同的提示查询 LLM 针对多个文本块,并返回格式化的响应列表。Compact
- 与Refine
相同,但在每次 LLM 调用中尽可能多地放入文本。CompactAndAccumulate
- 与Accumulate
相同,但尽可能多地放入文本。TreeSummarize
- 从提供的文本块创建自底向上的摘要,并返回根摘要。SimpleSummarize
- 合并并截断所有文本块,并在一次 LLM 调用中进行摘要。
用法
如上所述,您可以直接在查询引擎中设置响应合成器,或者让 response_mode
获取相关的响应合成器。
此外,您还可以直接调用这些合成器并将其用作低层模块。这里有一个小例子
from llama_index import ServiceContext
from llama_index.response_synthesizers import CompactAndRefine
# you can also configure the text_qa_template, refine_template,
# and streaming toggle from here
response_synthesizer = CompactAndRefine(
service_context=service_context.from_defaults()
)
response = response_synthesizer.get_response(
"What skills does Bob have?",
text_chunks=[" ..."] # here would be text, hopefully about Bob's skills
)
资源
以下是一些额外的 Notebook,展示了如何使用 get_response_synthesizer
元数据管理功能
如果您希望在任何针对您数据的 LLM 应用(包括 RAG 管道)中获得良好性能,您需要确保您的文档实际包含与查询相关的上下文。实现这一点的一种方法是添加适当的元数据,既可以在文档级别添加,也可以在文档被解析成文本块(Node)之后添加。
我们允许您在 Document 中定义元数据字段,自定义 ID,并自定义用于 LLM 和嵌入的元数据文本/格式。
定义元数据字段
document = Document(
text='text',
metadata={
'filename': '<doc_file_name>',
'category': '<category>'
}
)
自定义 ID
每个文档的 ID 可以通过多种方式设置
- 在构造函数中设置:
document = Document(text="text", doc_id_="id")
- 构造对象后设置:
document.doc_id = "id"
- 自动使用
SimpleDirectoryReader
:SimpleDirectoryReader(filename_as_id=True).load_data()
自定义用于 LLM 和嵌入的元数据文本
如上所示,您可以设置包含有用信息的元数据。默认情况下,所有元数据都将对嵌入模型和 LLM 可见。但是,有时您可能只想包含数据来偏置嵌入,或者只将数据作为额外信息包含给 LLM!
使用新的 Document
对象,您可以配置每个元数据字段的用途
document = Document(
text='text',
metadata={
'filename': '<doc_file_name>',
'category': '<category>'
},
excluded_llm_metadata_keys=['filename', 'category'],
excluded_embed_metadata_keys=['filename']
)
自定义元数据格式模板
当元数据插入到文本中时,它遵循非常特定的格式。此格式可在多个级别配置
from llama_index.schema import MetadataMode
document = Document(
text='text',
metadata={"key": "val"},
metadata_seperator="::",
metadata_template="{key}=>{value}",
text_template="Metadata: {metadata_str}\\n-----\\nContent: {content}"
)
# available modes are ALL, NONE, LLM, and EMBED
print(document.get_content(metadata_mode=MetadataMode.ALL))
# output:
# Metadata: key=>val
# -----
# text
请查阅此指南了解更多详情!
完整的破坏性变更列表
响应合成 + 节点后处理器
ResponseSynthesizer
对象类已被移除,并替换为 get_response_synthesizer
。此外,节点后处理器现在由查询引擎直接处理,旧的 SentenceEmbeddingOptimizer
已转换为节点后处理器实例本身。
以下是使用所有已迁移功能所需的迁移示例。
旧
from llama_index import (
VectorStoreIndex,
ResponseSynthesizer,
)
from llama_index.indices.postprocessor import SimilarityPostprocessor
from llama_index.optimizers import SentenceEmbeddingOptimizer
from llama_index.query_engine import RetrieverQueryEngine
documents = ...
# build index
index = VectorStoreIndex.from_documents(documents)
# configure retriever
retriever = index.as_retriever(
similarity_top_k=3
)
# configure response synthesizer
response_synthesizer = ResponseSynthesizer.from_args(
response_mode="tree_summarize",
node_postprocessors=[
SimilarityPostprocessor(similarity_cutoff=0.7),
SentenceEmbeddingOptimizer(percentile_cutoff=0.5)
]
)
# assemble query engine
query_engine = RetrieverQueryEngine(
retriever=retriever,
response_synthesizer=response_synthesizer,
)
新
from llama_index import (
VectorStoreIndex,
get_response_synthesizer,
)
from llama_index.indices.postprocessor import (
SimilarityPostprocessor,
SentenceEmbeddingOptimizer
)
documents = ...
# build index
index = VectorStoreIndex.from_documents(documents)
# configure response synthesizer
response_synthesizer = get_response_synthesizer(
response_mode="tree_summarize",
)
# assemble query engine
query_engine = index.as_query_engine(
similarity_top_k=3,
response_synthesizer=response_synthesizer,
node_postprocessors=[
SimilarityPostprocessor(similarity_cutoff=0.7),
SentenceEmbeddingOptimizer(percentile_cutoff=0.5)
]
)
LLM Predictor
在引入新的 LLM 抽象的同时,我们清理了 LLM Predictor 并移除了几个已弃用的功能
- 移除
ChatGPTLLMPredictor
和HuggingFaceLLMPredictor
(请改用OpenAI
和HuggingFaceLLM
,参见迁移指南) - 移除通过
LLMPredictor
构造函数设置cache
的支持。 - 移除
llama_index.token_counter.token_counter
模块(参见迁移指南)。
现在,LLM Predictor 类主要是一个轻量级包装器,封装了 LLM
抽象,用于处理
- 将提示转换为 LLM 期望的字符串或聊天消息输入格式
- 将提示和响应记录到回调管理器
我们建议用户直接在 ServiceContext
中配置 llm
参数(而不是创建 LLM Predictor)。
聊天引擎
我们将 BaseChatEngine
接口更新为 chat_history
接收 List[ChatMessage]]
,而不是字符串元组。这使得数据模型与 LLM
的输入/输出一致,并且更灵活地指定具有相同角色的连续消息。
旧
engine = SimpleChatEngine.from_defaults(
chat_history=[("human message", "assistant message")],
)
response = engine.chat("new human message")
新
engine = SimpleChatEngine.from_defaults(
service_context=mock_service_context,
chat_history=[
ChatMessage(role=MessageRole.USER, content="human message"),
ChatMessage(role=MessageRole.ASSISTANT, content="assistant message"),
],
)
response = engine.chat("new human message")
我们还将 chat_history
状态作为属性公开,并支持在 chat
和 achat
端点中覆盖 chat_history
。
提示助手
我们移除了一些之前已弃用的参数:max_input_size
、embedding_limit
、max_chunk_overlap
结论
从宏观层面来看,我们希望这些改变能够继续支持针对您数据的 LLM 应用的自底向上开发。我们首先鼓励您独立地试用我们的新模块,了解它们的功能以及如何使用它们。一旦您准备好在更高级的工作流程中使用它们,就可以弄清楚如何使用我们的外部组件来搭建复杂的 RAG 管道。