概念指南
本节包含对 LangChain 关键部分的介绍。
架构
LangChain 作为一个框架,由多个包组成。
langchain-core
此包包含不同组件的基本抽象和组合它们的方法。此处定义了核心组件(如 LLM、向量存储、检索器等)的接口。此处未定义任何第三方集成。依赖项有意保持非常轻量级。
langchain
主要的 langchain
包包含构成应用程序认知架构的链、代理和检索策略。这些**不是**第三方集成。此处的所有链、代理和检索策略**不**特定于任何一个集成,而是跨所有集成通用。
langchain-community
此包包含由 LangChain 社区维护的第三方集成。关键合作伙伴包被分离出来(见下文)。它包含各种组件(LLM、向量存储、检索器)的所有集成。此包中的所有依赖项都是可选的,以使包尽可能轻量级。
合作伙伴包
虽然许多集成(long tail)位于 langchain-community
中,但我们将一些常用的集成拆分到各自的包中(例如 langchain-openai
、langchain-anthropic
等)。这样做是为了更好地支持这些重要的集成。
langgraph
langgraph
是 langchain
的扩展,旨在通过将步骤建模为图中的边和节点,构建健壮且有状态的、基于大型语言模型的多参与者应用程序。
LangGraph 提供了创建常见类型代理的高级接口,以及用于组合自定义流程的低级 API。
langserve
一个用于将 LangChain 链部署为 REST API 的包。可以轻松地启动并运行一个生产就绪的 API。
LangSmith
一个开发者平台,允许你调试、测试、评估和监控大型语言模型应用程序。
LangChain 表达式语言 (LCEL)
LangChain 表达式语言
,或 LCEL
,是一种声明式的方式来连接 LangChain 组件。LCEL 从一开始就设计为**支持将原型直接投入生产,无需代码更改**,从最简单的“提示 + 大型语言模型”链到最复杂的链(我们看到一些用户成功地在生产环境中运行了包含数百个步骤的 LCEL 链)。以下是一些你可能想要使用 LCEL 的原因:
**一流的流式支持:**当你使用 LCEL 构建链时,你会获得尽可能快的首个标记时间(第一个输出片段出现之前的时间)。对于某些链来说,这意味着例如,我们将标记直接从大型语言模型流式传输到流式输出解析器,并且你会以与大型语言模型提供程序输出原始标记相同的速率获得解析后的增量输出片段。
**异步支持:**任何使用 LCEL 构建的链都可以使用同步 API(例如,在原型设计时使用 Jupyter Notebook)和异步 API(例如,在 LangServe 服务器中)调用。这使得能够使用相同的代码进行原型设计和生产,并获得出色的性能,以及在同一服务器上处理许多并发请求的能力。
**优化的并行执行:**无论何时你的 LCEL 链包含可以并行执行的步骤(例如,如果从多个检索器中获取文档),我们都会自动执行它,无论是在同步还是异步接口中,以获得尽可能低的延迟。
**重试和回退:**为 LCEL 链的任何部分配置重试和回退。这是一种在规模化时提高链可靠性的好方法。我们目前正在努力为重试/回退添加流式支持,以便你可以在没有任何延迟成本的情况下获得额外的可靠性。
**访问中间结果:**对于更复杂的链,在最终输出产生之前访问中间步骤的结果通常非常有用。这可以用来让最终用户知道某些事情正在发生,或者仅仅是为了调试你的链。你可以流式传输中间结果,并且它在每个 LangServe 服务器上都可用。
**输入和输出模式** 输入和输出模式为每个 LCEL 链提供了从链结构推断出的 Pydantic 和 JSONSchema 模式。这可以用于验证输入和输出,并且是 LangServe 不可或缺的一部分。
无缝 LangSmith 追踪 随着你的链变得越来越复杂,理解每个步骤中究竟发生了什么变得越来越重要。使用 LCEL,**所有**步骤都会自动记录到 LangSmith 中,以实现最大的可观察性和可调试性。
LCEL 旨在围绕行为和自定义提供一致性,而不是像 LLMChain
和 ConversationalRetrievalChain
这样的旧版子类链。许多这些旧版链隐藏了重要的细节,例如提示,并且随着各种可行的模型出现,自定义变得越来越重要。
如果你目前正在使用其中一个旧版链,请参阅 此指南了解如何迁移。
有关如何使用 LCEL 执行特定任务的指南,请查看 相关的操作指南。
可运行接口
为了尽可能轻松地创建自定义链,我们实现了一个 "可运行" 协议。许多 LangChain 组件都实现了 Runnable
协议,包括聊天模型、大型语言模型、输出解析器、检索器、提示模板等。还有一些用于处理可运行对象的实用原语,你可以在下面阅读相关内容。
这是一个标准接口,可以轻松地定义自定义链以及以标准方式调用它们。标准接口包括:
stream
:流式返回响应的片段invoke
:在输入上调用链batch
:在输入列表上调用链
这些还具有相应的异步方法,应与 asyncio await
语法一起使用以实现并发性
astream
:异步流式返回响应的片段ainvoke
:异步在输入上调用链abatch
:异步在输入列表上调用链astream_log
:除了最终响应之外,还流式返回发生的中间步骤。astream_events
:**测试版** 流式传输链中发生的事件(在langchain-core
0.1.14 中引入)
**输入类型** 和 **输出类型** 随组件而异
组件 | 输入类型 | 输出类型 |
---|---|---|
提示 | 字典 | PromptValue |
聊天模型 | 单个字符串、聊天消息列表或 PromptValue | ChatMessage |
大型语言模型 | 单个字符串、聊天消息列表或 PromptValue | 字符串 |
输出解析器 | 大型语言模型或聊天模型的输出 | 取决于解析器 |
检索器 | 单个字符串 | 文档列表 |
工具 | 单个字符串或字典,取决于工具 | 取决于工具 |
所有可运行对象都公开了输入和输出**模式**以检查输入和输出
input_schema
:从可运行对象的结构自动生成的输入 Pydantic 模型output_schema
:从可运行对象的结构自动生成的输出 Pydantic 模型
组件
LangChain 提供了标准的可扩展接口和外部集成,用于构建大型语言模型时有用的各种组件。LangChain 实现了一些组件,我们依赖第三方集成来实现一些组件,而另一些组件则两者兼而有之。
聊天模型
使用消息序列作为输入并返回聊天消息作为输出(而不是使用纯文本)的语言模型(与使用纯文本相反)。这些通常是较新的模型(较旧的模型通常是 大型语言模型
,见下文)。聊天模型支持为对话消息分配不同的角色,有助于区分来自 AI、用户和指令(例如系统消息)的消息。
尽管底层模型是消息输入、消息输出,但 LangChain 包装器也允许这些模型以字符串作为输入。这意味着你可以轻松地将聊天模型用作大型语言模型的替代。
当字符串作为输入传入时,它会被转换为 HumanMessage
,然后传递给底层模型。
LangChain 不托管任何聊天模型,而是依赖于第三方集成。
我们在构建聊天模型时有一些标准化的参数:
model
:模型的名称temperature
:采样温度timeout
:请求超时max_tokens
:要生成的最多标记数stop
:默认停止序列max_retries
:请求重试的最大次数api_key
:模型提供程序的 API 密钥base_url
:发送请求的端点
需要注意一些重要事项:
- 标准参数仅适用于公开具有预期功能的参数的模型提供程序。例如,某些提供程序不公开最大输出标记的配置,因此在这些提供程序上不支持 max_tokens。
- 标准参数目前仅在有其自身集成包的集成上执行(例如
langchain-openai
、langchain-anthropic
等),在langchain-community
中的模型上不执行。
聊天模型还接受特定于该集成的其他参数。要查找聊天模型支持的所有参数,请查看该模型的 API 参考。
一些聊天模型已针对**工具调用**进行了微调,并为此提供了一个专用的 API。通常,此类模型在工具调用方面比未经微调的模型更好,建议用于需要工具调用的用例。请参阅 工具调用部分 以获取更多信息。
有关如何使用聊天模型的具体信息,请参阅 此处相关的操作指南。
多模态
一些聊天模型是多模态的,可以接受图像、音频甚至视频作为输入。这些仍然不太常见,这意味着模型提供商尚未标准化“最佳”的 API 定义方式。多模态**输出**甚至更不常见。因此,我们保持我们的多模态抽象相当轻量级,并计划在该领域成熟时进一步巩固多模态 API 和交互模式。
在 LangChain 中,大多数支持多模态输入的聊天模型也以 OpenAI 的内容块格式接受这些值。到目前为止,这仅限于图像输入。对于像 Gemini 这样支持视频和其他字节输入的模型,API 也支持本机、特定于模型的表示。
有关如何使用多模态模型的具体信息,请参阅 此处相关的操作指南。
有关具有多模态模型的 LangChain 模型提供程序的完整列表,请查看此表。
大型语言模型
将字符串作为输入并返回字符串的语言模型。这些传统上是较旧的模型(较新的模型通常是聊天模型,请参见上文)。
虽然底层模型是字符串输入,字符串输出,但 LangChain 包装器也允许这些模型将消息作为输入。这使它们与聊天模型具有相同的接口。当消息作为输入传递时,它们将在后台格式化为字符串,然后再传递给底层模型。
LangChain 不托管任何大型语言模型(LLM),而是依赖于第三方集成。
有关如何使用大型语言模型(LLM)的详细信息,请参阅操作指南。
消息
一些语言模型将消息列表作为输入并返回一条消息。有几种不同类型的消息。所有消息都具有role
、content
和response_metadata
属性。
role
描述了谁在发送消息。标准角色包括“用户”、“助手”、“系统”和“工具”。LangChain 为不同的角色提供了不同的消息类。
content
属性描述了消息的内容。这可以是以下几种情况
- 字符串(大多数模型处理此类型的内容)
- 字典列表(这用于多模态输入,其中字典包含有关该输入类型和该输入位置的信息)
可选地,消息可以具有name
属性,该属性允许区分具有相同角色的多个说话者。例如,如果聊天历史记录中有两个用户,则区分它们很有用。并非所有模型都支持此功能。
HumanMessage
这表示具有角色“用户”的消息。
AIMessage
这表示具有角色“助手”的消息。除了content
属性外,这些消息还具有
response_metadata
response_metadata
属性包含有关响应的其他元数据。此处的數據通常特定于每个模型提供商。这是存储诸如日志概率和令牌使用情况等信息的位置。
tool_calls
这些表示语言模型调用工具的决定。它们作为AIMessage
输出的一部分包含在内。可以通过.tool_calls
属性访问它们。
此属性返回一个ToolCall
列表。ToolCall
是一个包含以下参数的字典
name
:应调用的工具的名称。args
:该工具的参数。id
:该工具调用的 ID。
SystemMessage
这表示具有角色“系统”的消息,它告诉模型如何表现。并非每个模型提供商都支持此功能。
ToolMessage
这表示具有角色“工具”的消息,其中包含调用工具的结果。除了role
和content
之外,此消息还具有
tool_call_id
字段,用于传达调用以产生此结果的工具的 ID。artifact
字段,可用于传递工具执行的任意工件,这些工件对于跟踪很有用,但不要发送给模型。
(旧版)FunctionMessage
这是一种旧版消息类型,对应于 OpenAI 的旧版函数调用 API。应改用ToolMessage
来对应更新的工具调用 API。
这表示函数调用的结果。除了role
和content
之外,此消息还具有name
参数,用于传达调用以产生此结果的函数的名称。
提示模板
提示模板有助于将用户输入和参数转换为语言模型的指令。这可以用来指导模型的响应,帮助它理解上下文并生成相关且连贯的基于语言的输出。
提示模板将字典作为输入,其中每个键都表示提示模板中要填充的变量。
提示模板输出PromptValue。此PromptValue可以传递给LLM或ChatModel,也可以转换为字符串或消息列表。PromptValue存在的原因是为了简化字符串和消息之间的切换。
有几种不同类型的提示模板
字符串PromptTemplates
这些提示模板用于格式化单个字符串,通常用于更简单的输入。例如,构造和使用PromptTemplate的常用方法如下
from langchain_core.prompts import PromptTemplate
prompt_template = PromptTemplate.from_template("Tell me a joke about {topic}")
prompt_template.invoke({"topic": "cats"})
ChatPromptTemplates
这些提示模板用于格式化消息列表。这些“模板”本身就由模板列表组成。例如,构造和使用ChatPromptTemplate的常用方法如下
from langchain_core.prompts import ChatPromptTemplate
prompt_template = ChatPromptTemplate.from_messages([
("system", "You are a helpful assistant"),
("user", "Tell me a joke about {topic}")
])
prompt_template.invoke({"topic": "cats"})
在上面的示例中,此ChatPromptTemplate将在调用时构造两条消息。第一条是系统消息,没有要格式化的变量。第二个是HumanMessage,将由用户传入的topic
变量进行格式化。
MessagesPlaceholder
此提示模板负责在特定位置添加消息列表。在上面的ChatPromptTemplate中,我们看到了如何格式化两条消息,每条消息都是一个字符串。但是,如果我们希望用户传入一个消息列表,我们将把它放到特定位置呢?这就是使用MessagesPlaceholder的方式。
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.messages import HumanMessage
prompt_template = ChatPromptTemplate.from_messages([
("system", "You are a helpful assistant"),
MessagesPlaceholder("msgs")
])
prompt_template.invoke({"msgs": [HumanMessage(content="hi!")]})
这将生成两条消息的列表,第一条是系统消息,第二条是我们传入的HumanMessage。如果我们传入5条消息,那么它将总共生成6条消息(系统消息加上传入的5条)。这对于让消息列表插入特定位置很有用。
无需显式使用MessagesPlaceholder
类即可实现相同效果的另一种方法是
prompt_template = ChatPromptTemplate.from_messages([
("system", "You are a helpful assistant"),
("placeholder", "{msgs}") # <-- This is the changed part
])
有关如何使用提示模板的详细信息,请参阅此处相关的操作指南。
示例选择器
实现更好性能的一种常见的提示技术是在提示中包含示例。这被称为少样本提示。这为语言模型提供了关于如何表现的具体示例。有时这些示例硬编码在提示中,但在更高级的情况下,动态选择它们可能很有用。示例选择器是负责选择并将示例格式化为提示的类。
有关如何使用示例选择器的详细信息,请参阅此处相关的操作指南。
输出解析器
此处的資訊指的是解析模型的文本輸出並嘗試将其解析為更結構化表示的解析器。越來越多的模型支持函數(或工具)調用,它會自動處理此問題。建議使用函數/工具調用而不是輸出解析。請參閱此處的相关文档。
输出解析器
负责获取模型的输出并将其转换为更适合下游任务的格式。当您使用大型语言模型(LLM)生成结构化数据或规范化聊天模型和大型语言模型(LLM)的输出时很有用。
LangChain 有许多不同类型的输出解析器。以下是 LangChain 支持的输出解析器列表。下表包含各种信息
- **名称**:输出解析器的名称
- **支持流式传输**:输出解析器是否支持流式传输。
- **具有格式说明**:输出解析器是否具有格式说明。这通常可用,除非(a)所需的模式未在提示中指定,而是在其他参数中指定(如 OpenAI 函数调用),或者(b)当 OutputParser 包装另一个 OutputParser 时。
- **调用 LLM**:此输出解析器本身是否调用 LLM。这通常仅由尝试更正格式错误输出的输出解析器执行。
- **输入类型**:预期的输入类型。大多数输出解析器都适用于字符串和消息,但有些(如 OpenAI 函数)需要具有特定关键字参数的消息。
- **输出类型**:解析器返回的对象的输出类型。
- **描述**:我们对这个输出解析器的评论以及何时使用它。
名称 | 支持流式传输 | 具有格式说明 | 调用 LLM | 输入类型 | 输出类型 | 描述 |
---|---|---|---|---|---|---|
JSON | ✅ | ✅ | str | 消息 | JSON 对象 | 返回指定的 JSON 对象。您可以指定一个 Pydantic 模型,它将返回该模型的 JSON。对于获取不使用函数调用的结构化数据,这可能是最可靠的输出解析器。 | |
XML | ✅ | ✅ | str | 消息 | dict | 返回标签字典。当需要 XML 输出时使用。与擅长编写 XML 的模型(如 Anthropic 的模型)一起使用。 | |
CSV | ✅ | ✅ | str | 消息 | List[str] | 返回逗号分隔值列表。 | |
OutputFixing | ✅ | str | 消息 | 包装另一个输出解析器。如果该输出解析器出错,则将错误消息和错误输出传递给 LLM 并要求它修复输出。 | |||
RetryWithError | ✅ | str | 消息 | 包装另一个输出解析器。如果该输出解析器出错,则将原始输入、错误输出和错误消息传递给 LLM 并要求它修复它。与 OutputFixingParser 相比,此解析器还会发送原始指令。 | |||
Pydantic | ✅ | str | 消息 | pydantic.BaseModel | 采用用户定义的 Pydantic 模型并以该格式返回数据。 | ||
YAML | ✅ | str | 消息 | pydantic.BaseModel | 接收用户定义的 Pydantic 模型并以该格式返回数据。使用 YAML 进行编码。 | ||
PandasDataFrame | ✅ | str | 消息 | dict | 用于执行 Pandas DataFrame 操作。 | ||
枚举 | ✅ | str | 消息 | 枚举 | 将响应解析为提供的枚举值之一。 | ||
日期时间 | ✅ | str | 消息 | datetime.datetime | 将响应解析为日期时间字符串。 | ||
结构化 | ✅ | str | 消息 | Dict[str, str] | 一个返回结构化信息的输出解析器。它不如其他输出解析器强大,因为它只允许字段为字符串。当您使用较小的 LLM 时,这很有用。 |
有关如何使用输出解析器的详细信息,请参阅此处的相关操作指南。
聊天历史
大多数 LLM 应用程序都具有对话界面。对话的一个重要组成部分是能够参考对话早期引入的信息。至少,对话系统应该能够直接访问某些过去消息的窗口。
ChatHistory
的概念指的是 LangChain 中的一个类,可用于包装任意链。此ChatHistory
将跟踪底层链的输入和输出,并将它们作为消息附加到消息数据库。未来的交互将加载这些消息,并将它们作为输入的一部分传递到链中。
文档
LangChain 中的 Document 对象包含有关某些数据的信息。它有两个属性
page_content: str
:此文档的内容。目前仅为字符串。metadata: dict
:与此文档关联的任意元数据。可以跟踪文档 ID、文件名等。
文档加载器
这些类加载 Document 对象。LangChain 与各种数据源(如 Slack、Notion、Google Drive 等)有数百个集成,可用于加载数据。
每个 DocumentLoader 都有其自己的特定参数,但它们都可以使用.load
方法以相同的方式调用。一个示例用例如下所示
from langchain_community.document_loaders.csv_loader import CSVLoader
loader = CSVLoader(
... # <-- Integration specific parameters here
)
data = loader.load()
有关如何使用文档加载器的详细信息,请参阅此处的相关操作指南。
文本分割器
加载文档后,您通常希望对其进行转换以更好地满足您的应用程序需求。最简单的示例是,您可能希望将长文档拆分为更小的块,以便适合模型的上下文窗口。LangChain 有许多内置的文档转换器,可以轻松地拆分、组合、过滤和以其他方式操作文档。
当您要处理长文本时,需要将该文本拆分为块。虽然听起来很简单,但这里有很多潜在的复杂性。理想情况下,您希望将语义相关的文本片段放在一起。“语义相关”的含义可能取决于文本类型。此笔记本展示了几种执行此操作的方法。
在高级别上,文本分割器的工作原理如下
- 将文本拆分为小的、语义有意义的块(通常是句子)。
- 开始将这些小块组合成更大的块,直到达到一定大小(以某个函数衡量)。
- 达到该大小后,将该块设为其自己的文本片段,然后开始创建包含一些重叠的新文本块(以保持块之间的上下文)。
这意味着您可以沿着两个不同的轴自定义文本分割器
- 文本的分割方式
- 块大小的衡量方式
有关如何使用文本分割器的详细信息,请参阅此处的相关操作指南。
嵌入模型
嵌入模型创建文本片段的向量表示。您可以将向量视为一个数字数组,它捕获文本的语义含义。通过以这种方式表示文本,您可以执行数学运算,从而执行诸如搜索含义最相似的其他文本片段等操作。这些自然语言搜索功能是许多类型的上下文检索的基础,在这些检索中,我们为 LLM 提供了有效响应查询所需的相关数据。
Embeddings
类是用于与文本嵌入模型交互的类。有许多不同的嵌入模型提供商(OpenAI、Cohere、Hugging Face 等)和本地模型,此类旨在为所有这些模型提供标准接口。
LangChain 中的基类 Embeddings 提供两种方法:一种用于嵌入文档,另一种用于嵌入查询。前者将多个文本作为输入,而后者将单个文本作为输入。将它们作为两种单独方法的原因是,某些嵌入提供商对文档(要搜索的文档)和查询(搜索查询本身)具有不同的嵌入方法。
有关如何使用嵌入模型的详细信息,请参阅此处的相关操作指南。
向量存储
存储和搜索非结构化数据最常见的方法之一是将其嵌入并存储生成的嵌入向量,然后在查询时嵌入非结构化查询并检索与嵌入查询“最相似”的嵌入向量。向量存储负责为您存储嵌入数据并执行向量搜索。
大多数向量存储还可以存储有关嵌入向量的元数据,并在相似性搜索之前支持对该元数据进行过滤,从而使您可以更好地控制返回的文档。
通过执行以下操作,可以将向量存储转换为检索器接口
vectorstore = MyVectorStore()
retriever = vectorstore.as_retriever()
有关如何使用向量存储的详细信息,请参阅此处的相关操作指南。
检索器
检索器是一个接口,它根据非结构化查询返回文档。它比向量存储更通用。检索器不需要能够存储文档,只需要返回(或检索)它们。检索器可以从向量存储中创建,但也足够广泛,可以包括维基百科搜索和Amazon Kendra。
检索器接受字符串查询作为输入,并返回 Document 列表作为输出。
有关如何使用检索器的详细信息,请参阅此处的相关操作指南。
键值存储
对于某些技术,例如每个文档使用多个向量进行索引和检索或缓存嵌入,使用某种形式的键值 (KV) 存储很有帮助。
LangChain 包含一个BaseStore
接口,允许存储任意数据。但是,需要 KV 存储的 LangChain 组件接受更具体的BaseStore[str, bytes]
实例,该实例存储二进制数据(称为ByteStore
),并在内部处理编码和解码数据以满足其特定需求。
这意味着作为用户,您只需要考虑一种类型的存储,而不是不同类型的数据使用不同的存储。
接口
所有BaseStores
都支持以下接口。请注意,该接口允许一次修改**多个**键值对
mget(key: Sequence[str]) -> List[Optional[bytes]]
:获取多个键的内容,如果键不存在则返回None
mset(key_value_pairs: Sequence[Tuple[str, bytes]]) -> None
:设置多个键的内容mdelete(key: Sequence[str]) -> None
:删除多个键yield_keys(prefix: Optional[str] = None) -> Iterator[str]
:生成存储中的所有键,可以选择按前缀进行过滤
有关键值存储实现,请参阅此部分。
工具
工具是旨在由模型调用的实用程序:它们的输入旨在由模型生成,它们的输出旨在传递回模型。当您希望模型控制代码的某些部分或调用外部 API 时,需要使用工具。
工具包括
- 工具的
name
。 - 工具功能的
description
。 - 定义工具输入的
JSON schema
。 - 一个
function
(以及可选的函数异步变体)。
当工具绑定到模型时,名称、描述和 JSON 模式将作为上下文提供给模型。给定一个工具列表和一组指令,模型可以请求使用特定输入调用一个或多个工具。典型用法可能如下所示
tools = [...] # Define a list of tools
llm_with_tools = llm.bind_tools(tools)
ai_msg = llm_with_tools.invoke("do xyz...")
# -> AIMessage(tool_calls=[ToolCall(...), ...], ...)
模型返回的AIMessage
可能与tool_calls
相关联。阅读本指南,以获取有关响应类型可能是什么样子的更多信息。
一旦调用选定的工具,结果就可以传递回模型,以便它可以完成正在执行的任何任务。通常有两种不同的方法来调用工具并传递回响应
仅使用参数调用
当您仅使用参数调用工具时,您将获得原始工具输出(通常是字符串)。这通常如下所示
# You will want to previously check that the LLM returned tool calls
tool_call = ai_msg.tool_calls[0]
# ToolCall(args={...}, id=..., ...)
tool_output = tool.invoke(tool_call["args"])
tool_message = ToolMessage(
content=tool_output,
tool_call_id=tool_call["id"],
name=tool_call["name"]
)
请注意,content
字段通常会传递回模型。如果您不希望将原始工具响应传递给模型,但仍希望保留它,则可以转换工具输出,但也可以将其作为工件传递(阅读有关此处ToolMessage.artifact
的更多信息)
... # Same code as above
response_for_llm = transform(response)
tool_message = ToolMessage(
content=response_for_llm,
tool_call_id=tool_call["id"],
name=tool_call["name"],
artifact=tool_output
)
使用ToolCall
调用
调用工具的另一种方法是使用模型生成的完整ToolCall
来调用它。当您这样做时,工具将返回 ToolMessage。这样做的好处是,您不必自己编写逻辑将工具输出转换为 ToolMessage。这通常如下所示
tool_call = ai_msg.tool_calls[0]
# -> ToolCall(args={...}, id=..., ...)
tool_message = tool.invoke(tool_call)
# -> ToolMessage(
content="tool result foobar...",
tool_call_id=...,
name="tool_name"
)
如果您以这种方式调用工具并希望为 ToolMessage 包含工件,则需要让工具返回两件事。阅读有关此处定义返回工件的工具的更多信息。
最佳实践
在设计供模型使用的工具时,务必牢记以下几点
- 具有显式工具调用 API 的聊天模型在工具调用方面会比未进行微调的模型表现更好。
- 如果工具具有精心选择的名称、描述和 JSON 模式,则模型将表现更好。这是一种提示工程的形式。
- 简单、范围狭窄的工具比复杂工具更容易被模型使用。
相关
有关如何使用工具的详细信息,请参阅工具使用指南。
要使用预构建的工具,请参阅工具集成文档。
工具包
工具包是一组旨在共同用于特定任务的工具的集合。它们具有便捷的加载方法。
所有工具包都公开了一个get_tools
方法,该方法返回一个工具列表。因此,您可以执行以下操作
# Initialize a toolkit
toolkit = ExampleTookit(...)
# Get list of tools
tools = toolkit.get_tools()
代理
语言模型本身无法执行操作 - 它们只会输出文本。LangChain 的一个主要用例是创建代理。代理是使用 LLM 作为推理引擎来确定要采取哪些操作以及这些操作的输入应该是什么的系统。然后,可以将这些操作的结果反馈给代理,并由其确定是否需要更多操作,或者是否可以结束。
LangGraph 是 LangChain 的一个扩展,专门用于创建高度可控和可定制的代理。请查看该文档以更深入地了解代理概念。
LangChain 中有一个我们正逐步弃用的旧版agent
概念:AgentExecutor
。AgentExecutor 本质上是代理的运行时。这是一个很好的入门点,但是,当您开始拥有更多自定义代理时,它不够灵活。为了解决这个问题,我们构建了 LangGraph 作为这个灵活且高度可控的运行时。
如果您仍在使用 AgentExecutor,请不要担心:我们仍然有关于如何使用 AgentExecutor的指南。但是,建议您开始迁移到 LangGraph。为了帮助您完成此操作,我们整理了一个迁移指南。
ReAct 代理
构建代理的一种流行架构是ReAct。ReAct 将推理和行动结合在一个迭代过程中 - 事实上,“ReAct”这个名称代表“Reason”(推理)和“Act”(行动)。
一般流程如下所示
- 模型将“思考”如何对输入和任何先前的观察做出响应。
- 然后,模型将从可用工具中选择一个操作(或选择响应用户)。
- 模型将为该工具生成参数。
- 代理运行时(执行器)将解析选定的工具并使用生成的参数调用它。
- 执行器将工具调用的结果作为观察结果返回给模型。
- 此过程重复,直到代理选择响应。
有一些基于通用提示的实现不需要任何模型特定的功能,但最可靠的实现使用诸如工具调用之类的功能来可靠地格式化输出并减少差异。
请参阅LangGraph 文档以获取更多信息,或参阅此使用指南以获取有关迁移到 LangGraph 的特定信息。
回调
LangChain 提供了一个回调系统,允许您挂接到 LLM 应用程序的各个阶段。这对于日志记录、监控、流式传输和其他任务很有用。
您可以使用遍布 API 的callbacks
参数订阅这些事件。此参数是处理程序对象列表,这些对象预计将实现下面更详细描述的一种或多种方法。
回调事件
事件 | 事件触发器 | 关联方法 |
---|---|---|
聊天模型开始 | 当聊天模型开始时 | on_chat_model_start |
LLM 开始 | 当 llm 开始时 | on_llm_start |
LLM 新令牌 | 当 llm 或聊天模型发出新令牌时 | on_llm_new_token |
LLM 结束 | 当 llm 或聊天模型结束时 | on_llm_end |
LLM 错误 | 当 llm 或聊天模型出错时 | on_llm_error |
链开始 | 当链开始运行时 | on_chain_start |
链结束 | 当链结束时 | on_chain_end |
链错误 | 当链出错时 | on_chain_error |
工具开始 | 当工具开始运行时 | on_tool_start |
工具结束 | 当工具结束时 | on_tool_end |
工具错误 | 当工具出错时 | on_tool_error |
代理操作 | 当代理采取操作时 | on_agent_action |
代理完成 | 当代理结束时 | on_agent_finish |
检索器开始 | 当检索器开始时 | on_retriever_start |
检索器结束 | 当检索器结束时 | on_retriever_end |
检索器错误 | 当检索器出错时 | on_retriever_error |
文本 | 当运行任意文本时 | on_text |
重试 | 当运行重试事件时 | on_retry |
回调处理程序
回调处理程序可以是sync
或async
- 同步回调处理程序实现BaseCallbackHandler接口。
- 异步回调处理程序实现AsyncCallbackHandler接口。
在运行时,LangChain 配置一个合适的回调管理器(例如,CallbackManager或AsyncCallbackManager),当事件触发时,该管理器将负责在每个“已注册”回调处理程序上调用适当的方法。
传递回调
callbacks
属性在 API 中大多数对象的两个不同位置可用(模型、工具、代理等)
回调在 API 中大多数对象(模型、工具、代理等)的两个不同位置可用。
- 请求时回调:除了输入数据外,还在请求时传递。在所有标准的
Runnable
对象上可用。这些回调由其定义的对象的所有子对象继承。例如,chain.invoke({"number": 25}, {"callbacks": [handler]})
。 - 构造函数回调:
chain = TheNameOfSomeChain(callbacks=[handler])
。这些回调作为参数传递给对象的构造函数。回调仅作用于其定义的对象,并且不会被该对象的任何子对象继承。
构造函数回调仅作用于其定义的对象。它们不会被该对象的子对象继承。
如果您正在创建自定义链或可运行对象,则需要记住将请求时回调传播到任何子对象。
任何RunnableLambda
、RunnableGenerator
或在 python<=3.10 中调用其他可运行对象并运行async
的Tool
都必须手动将回调传播到子对象。这是因为在这种情况下,LangChain 无法自动将回调传播到子对象。
这是您可能无法看到自定义可运行对象或工具发出的事件的一个常见原因。
有关如何使用回调的详细信息,请参阅此处的相关使用指南。
技术
流式传输
单个 LLM 调用通常比传统的资源请求运行时间长得多。当您构建需要多个推理步骤的更复杂的链或代理时,这种情况会加剧。
幸运的是,LLM 迭代地生成输出,这意味着在最终响应准备就绪之前,可以显示合理的中间结果。因此,尽快使用输出已成为构建使用 LLM 的应用程序的 UX 中至关重要的一部分,以帮助缓解延迟问题,而 LangChain 旨在对流式传输提供一流的支持。
下面,我们将讨论 LangChain 中流式传输的一些概念和注意事项。
.stream()
和.astream()
LangChain 中的大多数模块都包含.stream()
方法(以及用于异步环境的等效.astream()
方法)作为符合人体工程学的流式传输接口。.stream()
返回一个迭代器,您可以使用简单的for
循环使用它。以下是一个使用聊天模型的示例
from langchain_anthropic import ChatAnthropic
model = ChatAnthropic(model="claude-3-sonnet-20240229")
for chunk in model.stream("what color is the sky?"):
print(chunk.content, end="|", flush=True)
对于(或其他不支持流式传输的组件),此迭代器只会产生一个块,但您仍然可以在调用它们时使用相同的通用模式。使用.stream()
还将自动以流式模式调用模型,无需提供其他配置。
每个输出块的类型取决于组件的类型 - 例如,聊天模型会产生AIMessageChunks
。因为此方法是LangChain 表达式语言的一部分,所以您可以使用输出解析器处理来自不同输出的格式差异,以转换每个产生的块。
您可以查看本指南以详细了解如何使用.stream()
。
.astream_events()
虽然.stream()
方法很直观,但它只能返回链中生成的最终值。对于单个 LLM 调用来说,这很好,但当您将多个 LLM 调用构建成更复杂的链时,您可能希望将链的中间值与最终输出一起使用——例如,在构建基于文档的聊天应用程序时,返回源代码以及最终生成的内容。
可以通过一些方法实现这一点,例如使用回调,或者以某种方式构建您的链,使其通过诸如链式.assign()
调用将中间值传递到末尾,但 LangChain 还包含一个.astream_events()
方法,它将回调的灵活性与.stream()
的简易性结合在一起。当被调用时,它返回一个迭代器,该迭代器会生成各种类型的事件,您可以根据项目的需要对其进行过滤和处理。
以下是一个小例子,它只打印包含流式聊天模型输出的事件
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_anthropic import ChatAnthropic
model = ChatAnthropic(model="claude-3-sonnet-20240229")
prompt = ChatPromptTemplate.from_template("tell me a joke about {topic}")
parser = StrOutputParser()
chain = prompt | model | parser
async for event in chain.astream_events({"topic": "parrot"}, version="v2"):
kind = event["event"]
if kind == "on_chat_model_stream":
print(event, end="|", flush=True)
您可以将其粗略地理解为回调事件的迭代器(尽管格式不同)——并且您可以在几乎所有 LangChain 组件上使用它!
有关如何使用.astream_events()
的更详细信息(包括列出可用事件的表格),请参阅本指南。
回调
在 LangChain 中,通过回调系统以最低级别的方式从 LLM 流式传输输出。您可以传递一个回调处理程序,该处理程序处理on_llm_new_token
事件到 LangChain 组件中。当该组件被调用时,组件中包含的任何LLM或聊天模型都会使用生成的标记调用回调。在回调中,您可以将标记传递到其他目的地,例如 HTTP 响应。您还可以处理on_llm_end
事件以执行任何必要的清理。
有关使用回调的更多详细信息,请参阅本操作指南。
回调是在 LangChain 中引入的第一种流式传输技术。虽然功能强大且通用,但对于开发人员来说,它们可能难以操作。例如
- 您需要显式初始化和管理一些聚合器或其他流来收集结果。
- 执行顺序没有明确保证,理论上回调可能会在
.invoke()
方法完成后运行。 - 提供商通常会让您传递一个额外的参数来流式传输输出,而不是一次性返回所有输出。
- 您通常会忽略实际模型调用的结果,而有利于回调结果。
标记
大多数模型提供商用来衡量输入和输出的单位称为**标记**。标记是语言模型在处理或生成文本时读取和生成的基元。标记的确切定义可能因模型的具体训练方式而异——例如,在英语中,标记可以是一个单独的单词,如“apple”,或者单词的一部分,如“app”。
当您向模型发送提示时,提示中的单词和字符会使用**标记器**编码成标记。然后,模型会流式传输回生成的输出标记,标记器会将这些标记解码成人类可读的文本。以下示例显示了 OpenAI 模型如何标记LangChain is cool!
您可以看到它被拆分成 5 个不同的标记,并且标记之间的边界与单词边界并不完全相同。
语言模型使用标记而不是更直观的“字符”的原因与其处理和理解文本的方式有关。在高级别上,语言模型基于初始输入及其之前的生成迭代地预测其下一个生成的输出。使用标记训练模型可以使语言模型处理具有含义的语言单元(如单词或子词),而不是单个字符,这使得模型更容易学习和理解语言的结构,包括语法和上下文。此外,使用标记还可以提高效率,因为与字符级处理相比,模型处理的文本单元更少。
函数/工具调用
我们将术语工具调用
与函数调用
互换使用。虽然函数调用有时是指单个函数的调用,但我们认为所有模型都可以在每条消息中返回多个工具或函数调用。
工具调用允许聊天模型通过生成与用户定义的模式匹配的输出来响应给定的提示。
虽然名称暗示模型正在执行某些操作,但实际上并非如此!模型仅生成工具的参数,而实际运行工具(或不运行)则取决于用户。一个常见的示例是,您**不希望**使用生成的參數來调用函数,例如,如果您想从非结构化文本中提取与某些模式匹配的结构化输出。您可以为模型提供一个“提取”工具,该工具采用与所需模式匹配的参数,然后将生成的输出视为您的最终结果。
工具调用并非普遍适用,但许多流行的 LLM 提供商都支持它,包括Anthropic、Cohere、Google、Mistral、OpenAI,甚至通过Ollama用于本地运行的模型。
LangChain 提供了一个标准化的工具调用接口,该接口在不同模型之间保持一致。
标准接口包括
ChatModel.bind_tools()
:一种指定模型可调用哪些工具的方法。此方法接受LangChain 工具以及Pydantic对象。AIMessage.tool_calls
:模型返回的AIMessage
上的一个属性,用于访问模型请求的工具调用。
工具使用
模型调用工具后,您可以通过调用该工具,然后将参数传递回模型来使用该工具。LangChain 提供了Tool
抽象来帮助您处理此问题。
一般流程如下
- 使用聊天模型生成工具调用以响应查询。
- 使用生成的工具调用作为参数调用相应的工具。
- 将工具调用的结果格式化为
ToolMessages
。 - 将整个消息列表传递回模型,以便它可以生成最终答案(或调用更多工具)。
这就是工具调用代理执行任务和回答查询的方式。
查看下面一些更集中的指南
结构化输出
LLM 能够生成任意文本。这使得模型能够适当地响应各种输入,但在某些用例中,将 LLM 的输出限制为特定的格式或结构会很有用。这称为**结构化输出**。
例如,如果输出要存储在关系数据库中,那么如果模型生成的输出符合定义的模式或格式,则会容易得多。从非结构化文本中提取特定信息是另一个特别有用的案例。最常见的输出格式是 JSON,尽管其他格式(如YAML)也可能有用。下面,我们将讨论在 LangChain 中从模型获取结构化输出的几种方法。
.with_structured_output()
为了方便起见,一些 LangChain 聊天模型支持.with_structured_output()
方法。此方法只需要一个模式作为输入,并返回一个字典或 Pydantic 对象。通常,此方法仅存在于支持以下所述更高级方法之一的模型上,并且会在内部使用其中一种方法。它负责导入合适的输出解析器并以正确的格式格式化模式,以便模型使用。
这是一个例子
from typing import Optional
from langchain_core.pydantic_v1 import BaseModel, Field
class Joke(BaseModel):
"""Joke to tell user."""
setup: str = Field(description="The setup of the joke")
punchline: str = Field(description="The punchline to the joke")
rating: Optional[int] = Field(description="How funny the joke is, from 1 to 10")
structured_llm = llm.with_structured_output(Joke)
structured_llm.invoke("Tell me a joke about cats")
Joke(setup='Why was the cat sitting on the computer?', punchline='To keep an eye on the mouse!', rating=None)
我们建议将此方法作为处理结构化输出的起点
- 它在后台使用其他特定于模型的功能,而无需导入输出解析器。
- 对于使用工具调用的模型,不需要特殊的提示。
- 如果支持多种底层技术,则可以向
method
参数提供切换使用哪种技术。
如果您遇到以下情况,您可能希望或需要使用其他技术
- 您正在使用的聊天模型不支持工具调用。
- 您正在处理非常复杂的模式,并且模型难以生成符合模式的输出。
有关更多信息,请查看本操作指南。
您还可以查看此表,了解支持with_structured_output()
的模型列表。
原始提示
让模型构建输出的最直观方法是礼貌地请求。除了您的查询之外,您还可以提供描述您希望获得哪种输出的说明,然后使用输出解析器解析输出,以将原始模型消息或字符串输出转换为更容易操作的内容。
原始提示的最大好处在于其灵活性
- 原始提示不需要任何特殊的模型功能,只需要足够强的推理能力来理解传递的模式。
- 您可以提示您想要的任何格式,而不仅仅是 JSON。如果您的模型更侧重于训练某种类型的数据(例如 XML 或 YAML),这会很有用。
但是,也有一些缺点
- 大型语言模型 (LLM) 是非确定性的,提示 LLM 以完全正确的格式一致地输出数据以便于顺利解析可能出乎意料地困难且特定于模型。
- 各个模型根据其训练数据存在一些特性,优化提示可能非常困难。一些模型可能更擅长解释JSON 模式,另一些模型可能最适合 TypeScript 定义,还有一些模型可能更喜欢 XML。
虽然模型提供商提供的功能可以提高可靠性,但无论您选择哪种方法,提示技巧对于微调结果仍然很重要。
JSON 模式
一些模型,例如Mistral、OpenAI、Together AI 和Ollama,支持一项称为**JSON 模式**的功能,通常通过配置启用。
启用后,JSON 模式会将模型的输出限制为始终为某种有效的 JSON。通常它们需要一些自定义提示,但通常比完全原始提示轻松得多,更像是 "你必须始终返回 JSON"
。该输出通常也更容易解析。
它通常也更易于直接使用,并且比工具调用更常用,并且可以提供比工具调用更多的提示和塑造结果的灵活性。
这是一个例子
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain.output_parsers.json import SimpleJsonOutputParser
model = ChatOpenAI(
model="gpt-4o",
model_kwargs={ "response_format": { "type": "json_object" } },
)
prompt = ChatPromptTemplate.from_template(
"Answer the user's question to the best of your ability."
'You must always output a JSON object with an "answer" key and a "followup_question" key.'
"{question}"
)
chain = prompt | model | SimpleJsonOutputParser()
chain.invoke({ "question": "What is the powerhouse of the cell?" })
{'answer': 'The powerhouse of the cell is the mitochondrion. It is responsible for producing energy in the form of ATP through cellular respiration.',
'followup_question': 'Would you like to know more about how mitochondria produce energy?'}
有关支持 JSON 模式的模型提供商的完整列表,请参阅此表。
工具调用
对于支持它的模型,工具调用对于结构化输出非常方便。它消除了围绕如何最好地提示模式的猜测,转而使用内置的模型功能。
它的工作原理是首先将所需的模式直接或通过LangChain 工具绑定到聊天模型,使用 .bind_tools()
方法。然后,模型将生成一个包含 tool_calls
字段的 AIMessage
,该字段包含与所需形状匹配的 args
。
您可以使用多种可接受的格式将工具绑定到 LangChain 中的模型。这是一个示例
from langchain_core.pydantic_v1 import BaseModel, Field
from langchain_openai import ChatOpenAI
class ResponseFormatter(BaseModel):
"""Always use this tool to structure your response to the user."""
answer: str = Field(description="The answer to the user's question")
followup_question: str = Field(description="A followup question the user could ask")
model = ChatOpenAI(
model="gpt-4o",
temperature=0,
)
model_with_tools = model.bind_tools([ResponseFormatter])
ai_msg = model_with_tools.invoke("What is the powerhouse of the cell?")
ai_msg.tool_calls[0]["args"]
{'answer': "The powerhouse of the cell is the mitochondrion. It generates most of the cell's supply of adenosine triphosphate (ATP), which is used as a source of chemical energy.",
'followup_question': 'How do mitochondria generate ATP?'}
工具调用是让模型生成结构化输出的一种普遍一致的方式,并且是当模型支持时,用于.with_structured_output()
方法的默认技术。
以下操作指南是使用函数/工具调用进行结构化输出的良好实用资源
有关支持工具调用的模型提供商的完整列表,请参阅此表。
少样本提示
提高模型性能最有效的方法之一是向模型提供其所需执行操作的示例。在模型提示中添加示例输入和预期输出的技术称为“少样本提示”。该技术基于语言模型是少样本学习器论文。在进行少样本提示时,需要考虑一些事项
- 如何生成示例?
- 每个提示中有多少个示例?
- 如何在运行时选择示例?
- 如何在提示中格式化示例?
以下是每个方面的注意事项。
1. 生成示例
少样本提示的第一步也是最重要的一步是提出一个好的示例数据集。好的示例应该在运行时相关、清晰、信息丰富,并提供模型尚不知晓的信息。
从高层次来看,生成示例的基本方法是
- 手动:一个人或多个人生成他们认为有用的示例。
- 更好的模型:更好的(可能更昂贵/更慢)模型的响应用作较差(可能更便宜/更快)模型的示例。
- 用户反馈:用户(或标记者)对与应用程序的交互提供反馈,并根据该反馈生成示例(例如,所有具有正面反馈的交互都可以转换为示例)。
- LLM 反馈:与用户反馈相同,但该过程通过让模型自行评估来自动化。
哪种方法最佳取决于您的任务。对于需要真正理解少量核心原则的任务,手动制作几个真正好的示例可能很有价值。对于行为空间更广泛、更细致的任务,以更自动化的方式生成许多示例可能很有用,这样任何运行时输入都更有可能存在一些高度相关的示例。
单轮与多轮示例
在生成示例时需要考虑的另一个维度是示例实际上展示了什么。
最简单的示例类型仅具有用户输入和预期的模型输出。这些是单轮示例。
另一种更复杂的示例类型是整个对话的示例,通常其中模型最初响应不正确,然后用户告诉模型如何纠正其答案。这称为多轮示例。多轮示例可用于更细致的任务,在这些任务中,显示常见错误并准确说明它们错误的原因以及应该采取的措施很有用。
2. 示例数量
一旦我们拥有了一个示例数据集,我们就需要考虑每个提示中应该包含多少个示例。关键的权衡是,更多的示例通常会提高性能,但更大的提示会增加成本和延迟。并且超过某个阈值后,示例过多可能会开始混淆模型。找到合适的示例数量高度依赖于模型、任务、示例质量以及您的成本和延迟约束。根据经验,模型越好,其需要表现良好的示例就越少,并且越快达到添加更多示例的收益递减。但是,可靠地回答此问题的最佳/唯一方法是使用不同数量的示例进行一些实验。
3. 选择示例
假设我们不会将整个示例数据集添加到每个提示中,我们需要有一种方法可以根据给定的输入从我们的数据集中选择示例。我们可以这样做
- 随机地
- 通过输入的(语义或基于关键字的)相似性
- 基于其他一些约束,例如令牌大小
LangChain 有许多ExampleSelectors
,这使得轻松使用任何这些技术成为可能。
通常,通过语义相似性进行选择会导致最佳的模型性能。但这有多重要再次取决于模型和任务,值得尝试。
4. 格式化示例
如今,大多数最先进的模型都是聊天模型,因此我们将重点介绍如何为这些模型格式化示例。我们的基本选项是插入示例
- 在系统提示中作为字符串
- 作为他们自己的消息
如果我们将示例作为字符串插入系统提示中,我们需要确保模型清楚每个示例从哪里开始以及哪些部分是输入与输出。不同的模型对不同的语法有更好的响应,例如ChatML、XML、TypeScript 等。
如果我们将示例作为消息插入,其中每个示例都表示为 Human、AI 消息的序列,我们可能还想为我们的消息分配名称,例如 "example_user"
和 "example_assistant"
,以明确这些消息对应于与最新输入消息不同的参与者。
格式化工具调用示例
示例作为消息格式化可能很棘手的领域是当我们的示例输出具有工具调用时。这是因为不同的模型对生成任何工具调用时允许的消息序列类型有不同的约束。
- 一些模型要求任何带有工具调用的 AIMessage 之后都必须紧跟着每个工具调用的 ToolMessages,
- 一些模型还要求任何 ToolMessages 之后都必须紧跟着下一个 HumanMessage 之前的 AIMessage,
- 一些模型要求如果聊天历史记录中存在任何工具调用/ToolMessages,则必须将工具传递给模型。
这些要求特定于模型,应检查您正在使用的模型。如果您的模型需要工具调用后使用 ToolMessages 和/或 ToolMessages 后使用 AIMessages,并且您的示例仅包含预期的工具调用而不包含实际的工具输出,您可以尝试在每个示例的末尾添加具有通用内容的虚拟 ToolMessages/AIMessages 以满足 API 约束。在这些情况下,尤其值得尝试将示例作为字符串插入与作为消息插入,因为具有虚拟消息可能会对某些模型产生不利影响。
您可以在此处查看 Anthropic 和 OpenAI 如何在两个不同的工具调用基准上响应不同少样本提示技术的案例研究。
检索
LLM 在大型但固定的数据集上进行训练,限制了它们对私有或最新信息进行推理的能力。使用特定事实微调 LLM 是一种缓解此问题的方法,但通常不适合事实回忆,并且可能成本很高。检索
是指为 LLM 提供相关信息以改善其对给定输入的响应的过程。检索增强生成
(RAG
) 论文是使用检索到的信息为 LLM 生成(输出)提供依据的过程。
- 请参阅我们的从头开始的 RAG代码和视频系列。
- 有关检索的高级指南,请参阅此有关 RAG 的教程。
RAG 系统的好坏取决于检索到的文档的相关性和质量。幸运的是,我们可以使用一些新兴的技术来设计和改进 RAG 系统。我们重点对许多此类技术进行了分类和总结(见下图),并在接下来的部分分享一些高级策略指导。您可以并且应该尝试将不同的部分组合在一起使用。您可能还会发现这份 LangSmith 指南对展示如何评估应用程序的不同迭代很有用。
查询翻译
首先,考虑您 RAG 系统的用户输入。理想情况下,RAG 系统可以处理各种各样的输入,从措辞不佳的问题到复杂的多部分查询。使用 LLM 来审查并根据需要修改输入是查询翻译背后的核心思想。这充当了一个通用缓冲区,为您的检索系统优化原始用户输入。例如,这可以像提取关键词一样简单,也可以像为复杂查询生成多个子问题一样复杂。
名称 | 何时使用 | 描述 |
---|---|---|
多查询 | 当您需要涵盖问题的多个方面时。 | 从多个角度重写用户问题,为每个重写的问题检索文档,返回所有查询的唯一文档。 |
分解 | 当一个问题可以分解成更小的子问题时。 | 将问题分解成一系列子问题,这些子问题可以顺序解决(使用第一个问题的答案 + 检索来回答第二个问题)或并行解决(将每个答案整合到最终答案中)。 |
后退一步 | 当需要更高层次的概念理解时。 | 首先提示 LLM 提问一个关于更高层次概念或原则的通用后退问题,并检索相关的事实信息。利用这些基础知识来帮助回答用户问题。论文。 |
HyDE | 如果您在使用原始用户输入检索相关文档时遇到挑战。 | 使用 LLM 将问题转换为假设文档,这些文档可以回答问题。使用嵌入的假设文档来检索真实文档,前提是文档-文档相似性搜索可以产生更相关的匹配结果。论文。 |
路由
其次,考虑 RAG 系统可用的数据源。您希望跨多个数据库或跨结构化和非结构化数据源进行查询。使用 LLM 来审查输入并将其路由到适当的数据源是跨源查询的一种简单有效的方法。
名称 | 何时使用 | 描述 |
---|---|---|
逻辑路由 | 当您可以提示 LLM 使用规则来决定将输入路由到哪里时。 | 逻辑路由可以使用 LLM 推理查询并选择最合适的数据存储。 |
语义路由 | 当语义相似性是确定将输入路由到何处的有效方法时。 | 语义路由嵌入查询和通常是一组提示。然后根据相似性选择合适的提示。 |
请参阅我们在路由方面的“从零开始的 RAG”视频。
查询构建
第三,考虑您的任何数据源是否需要特定的查询格式。许多结构化数据库使用 SQL。向量数据库通常具有用于将关键词过滤器应用于文档元数据的特定语法。使用 LLM 将自然语言查询转换为查询语法是一种流行且强大的方法。特别是,文本到 SQL、文本到 Cypher和用于元数据过滤器的查询分析分别是与结构化、图和向量数据库交互的有用方法。
名称 | 何时使用 | 描述 |
---|---|---|
文本到 SQL | 如果用户提出的问题需要存储在关系数据库中、可以通过 SQL 访问的信息。 | 这使用 LLM 将用户输入转换为 SQL 查询。 |
文本到 Cypher | 如果用户提出的问题需要存储在图数据库中、可以通过 Cypher 访问的信息。 | 这使用 LLM 将用户输入转换为 Cypher 查询。 |
自我查询 | 如果用户提出的问题可以通过基于元数据而不是文本相似性来获取文档得到更好的解答。 | 这使用 LLM 将用户输入转换为两件事:(1) 一个用于语义查找的字符串,(2) 一个与之配套的元数据过滤器。这很有用,因为通常问题是关于文档的元数据(而不是内容本身)。 |
索引
第四,考虑文档索引的设计。一个简单而强大的想法是将您为检索而索引的文档与您传递给 LLM 以进行生成的文档分离。索引通常使用嵌入模型和向量数据库,这些模型将文档中的语义信息压缩为固定大小的向量。
许多 RAG 方法都侧重于将文档拆分为块,并根据与 LLM 输入问题的相似性检索一定数量的块。但是,块的大小和数量可能难以设置,如果它们没有为 LLM 提供回答问题的完整上下文,则会影响结果。此外,LLM 越来越能够处理数百万个标记。
两种方法可以解决这种矛盾:(1) 使用 LLM 将文档转换为任何形式(例如,通常转换为摘要)以适合索引,但将完整文档返回给 LLM 以进行生成的多向量检索器。(2) ParentDocument检索器嵌入文档块,但也返回完整文档。其想法是两全其美:使用简洁的表示(摘要或块)进行检索,但使用完整文档进行答案生成。
名称 | 索引类型 | 使用 LLM | 何时使用 | 描述 |
---|---|---|---|---|
向量存储 | 向量存储 | 否 | 如果您刚刚开始并正在寻找快速简便的方法。 | 这是最简单的方法,也是最容易上手的方法。它涉及为每段文本创建嵌入。 |
ParentDocument | 向量存储 + 文档存储 | 否 | 如果您的页面包含许多最好单独索引的较小且不同的信息片段,但最好一起检索。 | 这涉及为每个文档索引多个块。然后您找到嵌入空间中最相似的块,但您会检索整个父文档并将其返回(而不是单个块)。 |
多向量 | 向量存储 + 文档存储 | 有时在索引期间 | 如果您能够从文档中提取您认为比文本本身更相关的信息进行索引。 | 这涉及为每个文档创建多个向量。每个向量都可以通过多种方式创建 - 例如文本摘要和假设问题。 |
时间加权向量存储 | 向量存储 | 否 | 如果您有与文档关联的时间戳,并且您希望检索最新的文档 | 这根据语义相似性(如在普通向量检索中)和最新性(查看已索引文档的时间戳)的组合来获取文档。 |
第五,考虑改进相似性搜索本身的方法。嵌入模型将文本压缩为固定长度(向量)表示,这些表示捕获文档的语义内容。这种压缩对于搜索/检索很有用,但它给单个向量表示带来了沉重的负担,要求其捕获文档的语义细微差别/细节。在某些情况下,不相关或冗余的内容会稀释嵌入的语义效用。
ColBERT 是一种使用更高粒度嵌入来解决此问题的方法:(1) 为文档和查询中的每个标记生成上下文影响的嵌入,(2) 对每个查询标记和所有文档标记之间的相似性进行评分,(3) 获取最大值,(4) 对所有查询标记执行此操作,以及 (5) 获取所有查询标记的最大分数(步骤 3 中)的总和以获得查询-文档相似性分数;这种逐标记评分可以产生强大的结果。
还有一些额外的技巧可以提高检索质量。嵌入擅长捕获语义信息,但可能难以处理基于关键词的查询。许多向量存储提供内置的混合搜索来结合关键词和语义相似性,从而结合了这两种方法的优势。此外,许多向量存储具有最大边际相关性,它试图使搜索结果多样化,以避免返回相似和冗余的文档。
名称 | 何时使用 | 描述 |
---|---|---|
ColBERT | 当需要更高粒度的嵌入时。 | ColBERT 使用文档和查询中每个标记的上下文影响嵌入来获得粒度的查询-文档相似性分数。论文。 |
混合搜索 | 当结合基于关键词的搜索和语义相似性时。 | 混合搜索结合了关键词和语义相似性,结合了这两种方法的优势。论文。 |
最大边际相关性 (MMR) | 当需要使搜索结果多样化时。 | MMR 试图使搜索结果多样化,以避免返回相似和冗余的文档。 |
请参阅我们在ColBERT方面的“从零开始的 RAG”视频。
后处理
第六,考虑过滤或排序检索到的文档的方法。如果您要结合来自多个来源返回的文档,这非常有用,因为它可以降低不太相关的文档的排名和/或压缩相似的文档。
名称 | 索引类型 | 使用 LLM | 何时使用 | 描述 |
---|---|---|---|---|
上下文压缩 | 任何 | 有时 | 如果您发现检索到的文档包含过多的不相关信息并且分散了 LLM 的注意力。 | 这在另一个检索器之上添加了一个后处理步骤,并仅从检索到的文档中提取最相关的信息。这可以使用嵌入或 LLM 来完成。 |
集成 | 任何 | 否 | 如果您有多种检索方法并希望尝试将它们组合在一起。 | 这从多个检索器中获取文档,然后将它们组合在一起。 |
重新排序 | 任何 | 是 | 如果您希望根据相关性对检索到的文档进行排序,尤其是在您想将来自多个检索方法的结果结合起来时。 | 给定查询和文档列表,重新排序将文档从最语义相关到最不相关的排序。 |
请观看我们关于RAG-Fusion(论文)的从零开始的RAG视频,了解跨多个查询进行后处理的方法:从多个角度改写用户问题,为每个改写后的问题检索文档,并结合多个搜索结果列表的排名,使用互惠排序融合 (RRF)生成单个统一的排名。
生成
最后,考虑如何在你的RAG系统中构建自我校正功能。 RAG系统可能会遇到检索质量低(例如,如果用户问题超出索引范围)和/或生成幻觉的问题。一个简单的检索-生成管道无法检测或自我校正此类错误。 “流程工程”的概念已在代码生成领域中提出:使用单元测试迭代构建代码问题的答案,以检查和自我校正错误。一些研究将此应用于RAG,例如Self-RAG和Corrective-RAG。在这两种情况下,都会在RAG答案生成流程中检查文档相关性、幻觉和/或答案质量。
我们发现,图是一种可靠地表达逻辑流程的好方法,并且已经使用LangGraph实现了其中一些论文的想法,如下图所示(红色 - 路由,蓝色 - 回退,绿色 - 自我校正)
- 路由: 自适应RAG(论文)。如上所述,将问题路由到不同的检索方法。
- 回退: Corrective RAG(论文)。如果文档与查询不相关,则回退到网络搜索。
- 自我校正: Self-RAG(论文)。修复存在幻觉或未解决问题的答案。
名称 | 何时使用 | 描述 |
---|---|---|
Self-RAG | 当需要修复包含幻觉或不相关内容的答案时。 | Self-RAG 在 RAG 答案生成流程中执行文档相关性、幻觉和答案质量检查,迭代构建答案并自我校正错误。 |
Corrective-RAG | 当需要回退机制来处理相关性低的文档时。 | Corrective-RAG 包含一个回退机制(例如,到网络搜索),如果检索到的文档与查询不相关,则确保更高质量和更相关的检索。 |
查看一些展示使用 LangGraph 的 RAG 的视频和食谱
查看我们与合作伙伴合作的 LangGraph RAG 食谱
文本分割
LangChain 提供许多不同类型的文本分割器
。这些都位于langchain-text-splitters
包中。
表格列
- 名称:文本分割器的名称
- 类:实现此文本分割器的类
- 分割依据:此文本分割器如何分割文本
- 添加元数据:此文本分割器是否添加有关每个片段来源的元数据。
- 描述:分割器的描述,包括何时使用它的建议。
名称 | 类 | 分割依据 | 添加元数据 | 描述 |
---|---|---|---|---|
递归 | RecursiveCharacterTextSplitter,RecursiveJsonSplitter | 用户定义字符列表 | 递归分割文本。这种分割试图将相关的文本片段保持在一起。这是推荐的文本分割起始方法 。 | |
HTML | HTMLHeaderTextSplitter,HTMLSectionSplitter | HTML 特定字符 | ✅ | 基于 HTML 特定字符分割文本。值得注意的是,它会添加有关该片段来源的相关信息(基于 HTML) |
Markdown | MarkdownHeaderTextSplitter, | Markdown 特定字符 | ✅ | 基于 Markdown 特定字符分割文本。值得注意的是,它会添加有关该片段来源的相关信息(基于 Markdown) |
代码 | 多种语言 | 代码(Python、JS)特定字符 | 基于编码语言特定字符分割文本。可以选择 15 种不同的语言。 | |
令牌 | 多种类 | 令牌 | 基于令牌分割文本。存在几种不同的令牌测量方法。 | |
字符 | CharacterTextSplitter | 用户定义字符 | 基于用户定义字符分割文本。一种较为简单的方法。 | |
语义分块器(实验性) | SemanticChunker | 句子 | 首先基于句子进行分割。然后如果相邻的句子在语义上足够相似,则将其合并。来自Greg Kamradt | |
集成:AI21 语义 | AI21SemanticTextSplitter | ✅ | 识别构成连贯文本片段的不同主题,并沿着这些主题进行分割。 |
评估
评估是评估 LLM 驱动的应用程序的性能和有效性的过程。它涉及根据一组预定义的标准或基准测试模型的响应,以确保它满足所需的质量标准并实现预期目的。此过程对于构建可靠的应用程序至关重要。
LangSmith 通过以下几种方式帮助完成此过程
- 它通过其跟踪和注释功能简化了数据集的创建和管理
- 它提供了一个评估框架,帮助你定义指标并在你的数据集中运行你的应用程序
- 它允许你跟踪结果随时间的变化,并自动按计划或作为 CI/代码的一部分运行你的评估器
要了解更多信息,请查看此 LangSmith 指南。
跟踪
跟踪本质上是一系列应用程序从输入到输出所采取的步骤。跟踪包含称为运行
的单个步骤。这些可以是来自模型、检索器、工具或子链的单个调用。跟踪使你能够观察链和代理的内部情况,对于诊断问题至关重要。
要深入了解,请查看此 LangSmith 概念指南。