宣布 LlamaCloud 全面上线(以及我们完成 1900 万美元 A 轮融资)!
LlamaIndex

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 抽象支持 completechat 端点。主要区别在于 complete 设计用于接收简单的字符串输入,并输出一个 CompletionResponse(包含文本输出 + 附加字段)。chat 接收一个 ChatMessage 并输出一个 ChatResponse(包含聊天消息 + 附加字段)。

这些 LLM 端点还通过 stream_completestream_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"
  • 自动使用 SimpleDirectoryReaderSimpleDirectoryReader(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 并移除了几个已弃用的功能

  1. 移除 ChatGPTLLMPredictorHuggingFaceLLMPredictor(请改用 OpenAIHuggingFaceLLM,参见迁移指南
  2. 移除通过 LLMPredictor 构造函数设置 cache 的支持。
  3. 移除 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 状态作为属性公开,并支持在 chatachat 端点中覆盖 chat_history

提示助手

我们移除了一些之前已弃用的参数:max_input_sizeembedding_limitmax_chunk_overlap

结论

从宏观层面来看,我们希望这些改变能够继续支持针对您数据的 LLM 应用的自底向上开发。我们首先鼓励您独立地试用我们的新模块,了解它们的功能以及如何使用它们。一旦您准备好在更高级的工作流程中使用它们,就可以弄清楚如何使用我们的外部组件来搭建复杂的 RAG 管道。

一如既往,我们的代码库在此,我们的文档在此。如果您有想法/评论,请随时加入我们的Discord