概念指南
本节包含 LangChain 主要部分的介绍。
架构
LangChain 作为框架由多个部分组成。下图显示了它们之间的关系。
@langchain/core
此包包含不同组件的基本抽象和将它们组合在一起的方法。LLM、向量存储、检索器等核心组件的接口在此处定义。此处未定义任何第三方集成。依赖项特意保持非常轻量级。
此包是 LangChain 生态系统中大多数其他包的必需项,必须单独安装。
@langchain/community
此包包含由 LangChain 社区维护的第三方集成。关键合作伙伴包被分离出来(见下文)。这包含各种组件(LLM、向量存储、检索器)的所有集成。此包中的所有依赖项都是可选的,以保持包尽可能轻量级。
合作伙伴包
虽然长尾集成位于 @langchain/community
中,但我们将流行的集成拆分为它们自己的包(例如,langchain-openai
、langchain-anthropic
等)。这样做是为了改进对这些重要集成的支持。
langchain
主要 langchain
包包含链、代理和检索策略,它们构成了应用程序的认知架构。这些不是第三方集成。此处的所有链、代理和检索策略都不特定于任何一个集成,而是跨所有集成通用。
LangGraph.js
LangGraph.js 是 langchain
的扩展,旨在通过将步骤建模为图中的边和节点,构建强大的、有状态的多参与者 LLM 应用程序。
LangGraph 公开了创建常见代理类型的高级接口,以及用于组合自定义流的低级 API。
LangSmith
一个开发者平台,可以让您调试、测试、评估和监控 LLM 应用程序。
安装
如果您想使用高级抽象,则应安装 langchain
包。
- npm
- Yarn
- pnpm
npm i langchain @langchain/core
yarn add langchain @langchain/core
pnpm add langchain @langchain/core
如果您想使用特定集成,则需要单独安装它们。有关集成列表以及如何安装它们的更多信息,请参见 此处。
要使用 LangSmith,您需要设置一个 LangSmith 开发者帐户 此处 并获取 API 密钥。之后,您可以通过设置环境变量来启用它。
export LANGCHAIN_TRACING_V2=true
export LANGCHAIN_API_KEY=ls__...
# Reduce tracing latency if you are not in a serverless environment
# export LANGCHAIN_CALLBACKS_BACKGROUND=true
LangChain 表达式语言
LangChain 表达式语言(LCEL)是一种声明性方式,可以轻松地将链组合在一起。LCEL 从一开始就被设计为**支持将原型投入生产,无需代码更改**,从最简单的“提示 + LLM”链到最复杂的链(我们看到人们成功地在生产环境中运行了具有 100 多个步骤的 LCEL 链)。为了突出您可能想要使用 LCEL 的几个原因
一流的流式支持 当你用 LCEL 构建你的链时,你会得到最佳的首次令牌时间(第一个输出块出现之前的时间)。对于某些链,这意味着例如,我们将令牌直接从 LLM 流式传输到流式输出解析器,并且你会以与 LLM 提供程序输出原始令牌相同的速率接收解析后的增量输出块。
优化的并行执行 每当你的 LCEL 链具有可以并行执行的步骤(例如,如果你从多个检索器中获取文档)时,我们会自动执行它以实现最小的延迟。
重试和回退 为你的 LCEL 链的任何部分配置重试和回退。这是一种在规模上使你的链更可靠的好方法。我们目前正在努力为重试/回退添加流式支持,这样你就可以在没有任何延迟成本的情况下获得额外的可靠性。
访问中间结果 对于更复杂的链,在最终输出产生之前访问中间步骤的结果通常非常有用。这可以用来让最终用户知道某些事情正在发生,甚至仅仅是为了调试你的链。
无缝 LangSmith 跟踪 随着你的链越来越复杂,了解每一步到底发生了什么变得越来越重要。使用 LCEL,所有步骤都会自动记录到 LangSmith,以实现最大的可观察性和可调试性。
可运行接口
为了尽可能轻松地创建自定义链,我们实现了一个 "Runnable" 协议。许多 LangChain 组件都实现了 Runnable
协议,包括聊天模型、LLM、输出解析器、检索器、提示模板等等。还有一些用于处理可运行对象的实用原语,你可以在下面阅读。
这是一个标准接口,它使定义自定义链以及以标准方式调用它们变得容易。标准接口包括
输入类型 和 输出类型 随组件而异
组件 | 输入类型 | 输出类型 |
---|---|---|
提示 | 对象 | PromptValue |
聊天模型 | 单个字符串、聊天消息列表或 PromptValue | 聊天消息 |
LLM | 单个字符串、聊天消息列表或 PromptValue | 字符串 |
输出解析器 | LLM 或聊天模型的输出 | 取决于解析器 |
检索器 | 单个字符串 | 文档列表 |
工具 | 单个字符串或对象,取决于工具 | 取决于工具 |
组件
LangChain 为各种用于构建 LLM 的组件提供标准的可扩展接口和外部集成。LangChain 实现了一些组件,我们依靠一些第三方集成,还有一些是混合的。
聊天模型
使用消息序列作为输入并返回聊天消息作为输出的语言模型(与使用纯文本相反)。这些通常是较新的模型(较旧的模型通常是 LLM
,见下文)。聊天模型支持为对话消息分配不同的角色,这有助于区分来自 AI、用户和指令(如系统消息)的消息。
虽然底层模型是消息输入、消息输出,但 LangChain 包装器也允许这些模型以字符串作为输入。这使它们具有与 LLM 相同的接口(并且更易于使用)。当字符串作为输入传递时,它将在传递给底层模型之前在内部转换为 HumanMessage
。
LangChain 不托管任何聊天模型,而是依靠第三方集成。
我们在构建聊天模型时有一些标准化参数
model
: 模型的名称
聊天模型还接受其他特定于该集成的参数。
一些聊天模型已针对工具调用进行了微调,并为此提供专用 API。通常,此类模型在工具调用方面比非微调模型更好,建议在需要工具调用的用例中使用它们。有关更多信息,请参见 工具调用部分。
有关如何使用聊天模型的具体信息,请参见 此处相关的操作指南。
多模态
一些聊天模型是多模态的,可以接受图像、音频甚至视频作为输入。这些仍然不太常见,这意味着模型提供商尚未在定义 API 的“最佳”方式上达成一致。多模态输出甚至更不常见。因此,我们让多模态抽象保持相当轻量级,并计划在该领域成熟时进一步巩固多模态 API 和交互模式。
在 LangChain 中,大多数支持多模态输入的聊天模型也接受 OpenAI 内容块格式中的这些值。到目前为止,这仅限于图像输入。对于支持视频和其他字节输入的模型(如 Gemini),API 还支持本机模型特定表示。
有关如何使用多模态模型的具体信息,请参见 此处相关的操作指南。
LLM
将字符串作为输入并返回字符串的语言模型。这些传统上是较旧的模型(较新的模型通常是 聊天模型,见上文)。
虽然底层模型是字符串输入、字符串输出,但 LangChain 包装器也允许这些模型以消息作为输入。这使它们具有与 聊天模型 相同的接口。当消息作为输入传递时,它们将在传递给底层模型之前在内部格式化为字符串。
LangChain 不托管任何 LLM,而是依靠第三方集成。
有关如何使用 LLM 的具体信息,请参见 此处相关的操作指南。
消息类型
一些语言模型以消息数组作为输入并返回一条消息。有几种不同的消息类型。所有消息都有一个 role
、content
和 response_metadata
属性。
role
描述了谁在说消息。标准角色是“user”、“assistant”、“system”和“tool”。LangChain 为不同的角色提供不同的消息类。
content
属性描述了消息的内容。这可以是以下几种不同的内容
- 字符串(大多数模型处理这种类型的内容)
- 对象列表(这用于多模态输入,其中对象包含有关该输入类型和该输入位置的信息)
可选地,消息可以具有 name
属性,它可以用于区分具有相同角色的多个发言者。例如,如果聊天历史记录中有两个用户,则区分它们会很有用。并非所有模型都支持此功能。
HumanMessage
这表示角色为“user”的消息。
AIMessage
这表示角色为“assistant”的消息。除了 content
属性外,这些消息还具有
response_metadata
response_metadata
属性包含有关响应的附加元数据。此处的數據通常特定于每个模型提供商。这是存储 log-probs 和令牌使用情况等信息的地方。
tool_calls
这些表示语言模型调用工具的决定。它们作为 AIMessage
输出的一部分包含在内。可以使用 .tool_calls
属性从那里访问它们。
此属性返回一个 ToolCall
列表。ToolCall
是一个具有以下参数的对象
name
: 应调用的工具的名称。args
: 该工具的参数。id
: 该工具调用的 ID。
SystemMessage
这表示角色为“system”的消息,它告诉模型如何表现。并非所有模型提供商都支持此功能。
ToolMessage
这表示角色为“tool”的消息,它包含调用工具的结果。除了 role
和 content
之外,此消息还有
tool_call_id
字段,它传达被调用以产生此结果的工具的调用 ID。artifact
字段,可用于传递工具执行的任意工件,这些工件对跟踪很有用,但不应发送给模型。
(Legacy) FunctionMessage
这是一个旧的消息类型,对应于 OpenAI 的旧版函数调用 API。ToolMessage
应改用以对应于更新的工具调用 API。
这表示函数调用的结果。除了 role
和 content
之外,此消息还有一个 name
参数,它传达被调用以产生此结果的函数的名称。
提示模板
提示模板有助于将用户输入和参数转换为语言模型的指令。这可以用来指导模型的响应,帮助它理解上下文并生成相关且连贯的基于语言的输出。
提示模板以对象作为输入,其中每个键都代表提示模板中要填充的变量。
提示模板输出 PromptValue。此 PromptValue 可以传递给 LLM 或聊天模型,也可以强制转换为字符串或消息数组。PromptValue 存在的原因是为了便于在字符串和消息之间切换。
有几种不同的提示模板类型
字符串 PromptTemplates
这些提示模板用于格式化单个字符串,通常用于更简单的输入。例如,构造和使用 PromptTemplate 的一种常见方法如下
import { PromptTemplate } from "@langchain/core/prompts";
const promptTemplate = PromptTemplate.fromTemplate(
"Tell me a joke about {topic}"
);
await promptTemplate.invoke({ topic: "cats" });
ChatPromptTemplates
这些提示模板用于格式化消息数组。这些“模板”由模板数组本身组成。例如,构造和使用 ChatPromptTemplate 的一种常见方法如下
import { ChatPromptTemplate } from "@langchain/core/prompts";
const promptTemplate = ChatPromptTemplate.fromMessages([
["system", "You are a helpful assistant"],
["user", "Tell me a joke about {topic}"],
]);
await promptTemplate.invoke({ topic: "cats" });
在上面的例子中,这个 ChatPromptTemplate 在调用时会构建两个消息。第一个是系统消息,它没有要格式化的变量。第二个是 HumanMessage,它将根据用户传入的 topic
变量进行格式化。
MessagesPlaceholder
这个提示模板负责在特定位置添加消息数组。在上面的 ChatPromptTemplate 中,我们看到了如何格式化两个消息,每个消息都是一个字符串。但如果我们想让用户传入一个消息数组,并将这些消息放到特定位置,该怎么办?这就是 MessagesPlaceholder 的作用。
import {
ChatPromptTemplate,
MessagesPlaceholder,
} from "@langchain/core/prompts";
import { HumanMessage } from "@langchain/core/messages";
const promptTemplate = ChatPromptTemplate.fromMessages([
["system", "You are a helpful assistant"],
new MessagesPlaceholder("msgs"),
]);
promptTemplate.invoke({ msgs: [new HumanMessage({ content: "hi!" })] });
这将产生两个消息的数组,第一个是系统消息,第二个是我们传入的 HumanMessage。如果我们传入 5 个消息,那么它将总共产生 6 个消息(系统消息加上 5 个传入的消息)。这对于让消息数组放到特定位置非常有用。
另一种不显式使用 MessagesPlaceholder
类来实现相同目的的方法是
const promptTemplate = ChatPromptTemplate.fromMessages([
["system", "You are a helpful assistant"],
["placeholder", "{msgs}"], // <-- This is the changed part
]);
有关如何使用提示模板的具体信息,请参阅 此处的相关操作指南。
示例选择器
为了获得更好的性能,一种常见的提示技巧是将示例作为提示的一部分包含在内。这被称为 少样本提示。这为语言模型提供了有关其应如何行为的具体示例。有时这些示例被硬编码到提示中,但在更高级的情况下,动态选择它们可能更好。示例选择器是负责选择示例并将示例格式化为提示的类。
有关如何使用示例选择器的具体信息,请参阅 此处的相关操作指南。
输出解析器
此处的資訊指的是解析器,解析器會接受模型的文字輸出,並嘗試將其解析為更結構化的表示。越來越多的模型支持函數(或工具)调用,它会自动处理这种情况。建议使用函数/工具调用,而不是输出解析。有关此功能的文档,请参阅 此处。
负责接收模型的输出,并将其转换为更适合下游任务的格式。在使用 LLM 生成结构化数据或规范化来自聊天模型和 LLM 的输出时很有用。
输出解析器必须实现两种主要方法
- "获取格式说明":返回一个字符串,其中包含有关语言模型的输出应如何格式化的说明。
- "解析":接受一个字符串(假定为来自语言模型的响应),并将其解析为某种结构。
然后还有一个可选的方法
- "使用提示解析":接受一个字符串(假定为来自语言模型的响应)和一个提示(假定为生成此响应的提示),并将其解析为某种结构。主要是在 OutputParser 想要以某种方式重试或修复输出时提供提示,并且需要来自提示的信息才能做到这一点。
输出解析器接受字符串或 BaseMessage
作为输入,可以返回任意类型。
LangChain 有许多不同类型的输出解析器。以下列出了 LangChain 支持的输出解析器。下表包含各种信息
**名称**:输出解析器的名称
**支持流式传输**:输出解析器是否支持流式传输。
**输入类型**:预期的输入类型。大多数输出解析器都可以在字符串和消息上运行,但有些(如 OpenAI 函数)需要具有特定参数的消息。
**输出类型**:解析器返回的对象的输出类型。
**描述**:我们对这个输出解析器的评论,以及何时使用它。
名称 | 支持流式传输 | 输入类型 | 输出类型 | 描述 |
---|---|---|---|---|
JSON | ✅ | 字符串 | BaseMessage | Promise<T> | 返回指定的 JSON 对象。可以指定 Zod 模式,它将返回该模型的 JSON。 |
XML | ✅ | 字符串 | BaseMessage | Promise<XMLResult> | 返回标签对象。在需要 XML 输出时使用。与擅长编写 XML 的模型一起使用(例如 Anthropic 的模型)。 |
CSV | ✅ | 字符串 | BaseMessage | Array[string] | 返回一个逗号分隔值的数组。 |
结构化 | 字符串 | BaseMessage | Promise<TypeOf<T>> | 从 LLM 响应解析结构化 JSON。 | |
HTTP | ✅ | 字符串 | Promise<Uint8Array> | 解析 LLM 响应,然后通过 HTTP(s) 发送。在服务器/边缘调用 LLM,然后将内容/流发送回客户端时很有用。 |
字节 | ✅ | 字符串 | BaseMessage | Promise<Uint8Array> | 解析 LLM 响应,然后通过 HTTP(s) 发送。用于从服务器/边缘流式传输 LLM 响应到客户端。 |
日期时间 | 字符串 | Promise<Date> | 将响应解析为 Date 。 | |
正则表达式 | 字符串 | Promise<Record<string, string>> | 使用正则表达式模式解析给定的文本,并返回包含解析输出的对象。 |
有关如何使用输出解析器的具体信息,请参阅 此处的相关操作指南。
聊天历史记录
大多数 LLM 应用程序都具有对话界面。对话的一个重要组成部分是能够参考对话中先前引入的信息。至少,对话系统应该能够直接访问过去消息的某个窗口。
ChatHistory
的概念指的是 LangChain 中的一个类,它可以用来包装任意链。这个 ChatHistory
将跟踪底层链的输入和输出,并将它们作为消息附加到消息数据库中。以后的交互将加载这些消息,并将它们作为输入的一部分传递给链。
文件
LangChain 中的 Document 对象包含有关某些数据的資訊。它有两个属性
pageContent: string
:此文档的内容。目前仅为字符串。metadata: Record<string, any>
:与此文档关联的任意元数据。可以跟踪文档 ID、文件名等。
文件加载器
这些类加载 Document 对象。LangChain 与各种数据源集成了数百个集成,可以从这些数据源加载数据:Slack、Notion、Google Drive 等。
每个 DocumentLoader 都有其特定的参数,但它们都可以使用 .load
方法以相同的方式调用。一个示例用例如下所示
import { CSVLoader } from "@langchain/community/document_loaders/fs/csv";
const loader = new CSVLoader();
// <-- Integration specific parameters here
const docs = await loader.load();
有关如何使用文件加载器的具体信息,请参阅 此处的相关操作指南。
文字分割器
加载文档后,通常需要将其转换为更适合应用程序的格式。最简单的示例是,可能需要将长文档分割成更小的块,这些块可以适合模型的上下文窗口。LangChain 有许多内置的文档转换器,可以轻松地分割、组合、过滤和以其他方式操作文档。
当要处理长篇文本时,需要将文本分割成块。这听起来很简单,但其中存在很多潜在的复杂性。理想情况下,需要将语义相关的文本片段放在一起。什么是“语义相关”可能取决于文本的类型。此笔记本展示了执行此操作的几种方法。
总的来说,文本分割器的工作原理如下
- 将文本分割成小的、语义上有意义的块(通常是句子)。
- 开始将这些小块组合成更大的块,直到达到某个大小(以某种函数度量)。
- 达到该大小后,将该块作为独立的文本片段,然后开始创建一个新的文本片段,并带有部分重叠(以保留块之间的上下文)。
这意味着可以使用两个不同的轴来定制文本分割器
- 如何分割文本
- 如何衡量块的大小
有关如何使用文本分割器的具体信息,请参阅 此处的相关操作指南。
嵌入模型
嵌入模型会创建文本片段的向量表示。可以将向量视为数字数组,它捕获文本的语义含义。通过以这种方式表示文本,可以执行数学运算,从而可以执行诸如查找语义上最相似的其他文本片段之类的操作。这些自然语言搜索功能是许多类型的 上下文检索 的基础,在这些检索中,我们为 LLM 提供了它有效地响应查询所需的相關数据。
Embeddings
类是一个专为与文本嵌入模型交互而设计的类。有许多不同的嵌入模型提供商(OpenAI、Cohere、Hugging Face 等)和本地模型,此类旨在为所有这些模型提供标准接口。
LangChain 中的 Embeddings 基本类提供了两种方法:一种用于嵌入文档,另一种用于嵌入查询。前者将多个文本作为输入,而后者将单个文本作为输入。将它们作为两种独立方法的原因是,一些嵌入提供商对文档(要搜索的文档)和查询(搜索查询本身)具有不同的嵌入方法。
有关如何使用嵌入模型的具体信息,请参阅 此处的相关操作指南。
向量存储
存储和搜索非结构化数据的最常见方法之一是将数据嵌入并存储结果嵌入向量,然后在查询时嵌入非结构化查询并检索与嵌入查询“最相似”的嵌入向量。向量存储负责存储嵌入数据并为您执行向量搜索。
大多数向量存储还可以存储有关嵌入向量的元数据,并在相似性搜索之前支持对该元数据进行过滤,使您能够更好地控制返回的文档。
可以通过以下方式将向量存储转换为检索器接口
const vectorstore = new MyVectorStore();
const retriever = vectorstore.asRetriever();
有关如何使用向量存储的具体信息,请参阅 此处的相关操作指南。
检索器
检索器是一种接口,它根据非结构化查询返回相关文档。它们比向量存储更通用。检索器不需要能够存储文档,只需要返回(或检索)它们。检索器可以从向量存储中创建,但也足够广泛,可以包括 Exa 搜索(网络搜索)和 Amazon Kendra。
检索器接收字符串查询作为输入,并返回一个 Document
数组作为输出。
有关如何使用检索器的详细信息,请参阅 此处的相关操作指南。
键值存储
对于某些技术,例如 使用多个向量对文档进行索引和检索,拥有某种键值 (KV) 存储非常有用。
LangChain 包含一个 BaseStore
接口,它允许存储任意数据。但是,需要 KV 存储的 LangChain 组件接受更具体的 BaseStore<string, Uint8Array>
实例,该实例存储二进制数据(称为 ByteStore
),并在内部负责对其特定需求进行数据编码和解码。
这意味着作为用户,您只需要考虑一种类型的存储,而无需为不同类型的数据考虑不同的存储。
接口
所有 BaseStores
都支持以下接口。请注意,该接口允许一次修改多个键值对
mget(keys: string[]): Promise<(undefined | Uint8Array)[]>
:获取多个键的内容,如果键不存在则返回None
mset(keyValuePairs: [string, Uint8Array][]): Promise<void>
:设置多个键的内容mdelete(keys: string[]): Promise<void>
:删除多个键yieldKeys(prefix?: string): AsyncGenerator<string>
:生成存储中所有键,可以选择通过前缀进行过滤
有关键值存储实现,请参阅 本节。
工具
工具是专为模型调用而设计的实用程序:它们的输入旨在由模型生成,它们的输出旨在传递回模型。只要您希望模型控制代码的某些部分或调用外部 API,就需要工具。
工具包括
- 工具的名称。
- 对工具功能的描述。
- 定义工具输入的 JSON 架构。
- 一个函数。
当工具绑定到模型时,名称、描述和 JSON 架构将作为上下文提供给模型。
给定工具列表和一组指令,模型可以请求使用特定输入调用一个或多个工具。典型的用法可能如下所示
// Define a list of tools
const tools = [...];
const llmWithTools = llm.bindTools([tool]);
const aiMessage = await llmWithTools.invoke("do xyz...");
// AIMessage(tool_calls=[ToolCall(...), ...], ...)
模型返回的 AIMessage
可能与其关联 tool_calls
。阅读 本指南,了解有关响应类型可能是什么样的更多信息。
选择工具后,通常您需要调用它们,然后将结果传递回模型,以便它可以完成正在执行的任何任务。
通常有两种不同的方式来调用工具并传递回响应
仅使用参数调用
当您仅使用参数调用工具时,您将获得原始工具输出(通常是字符串)。以下是这种情况的示例
import { ToolMessage } from "@langchain/core/messages";
const toolCall = aiMessage.tool_calls[0]; // ToolCall(args={...}, id=..., ...)
const toolOutput = await tool.invoke(toolCall.args);
const toolMessage = new ToolMessage({
content: toolOutput,
name: toolCall.name,
tool_call_id: toolCall.id,
});
请注意,content
字段通常将传递回模型。如果您不想将原始工具响应传递给模型,但仍然希望保留它,您可以转换工具输出,但也可以将其作为工件传递(阅读有关 ToolMessage.artifact
的更多信息)
// Same code as above
const responseForModel = someTransformation(response);
const toolMessage = new ToolMessage({
content: responseForModel,
tool_call_id: toolCall.id,
name: toolCall.name,
artifact: response,
});
使用 ToolCall
调用
调用工具的另一种方法是使用模型生成的完整 ToolCall
来调用它。当您执行此操作时,工具将返回一个 ToolMessage
。这样做的好处是,您不必自己编写逻辑将工具输出转换为 ToolMessage。以下是这种情况的示例
const toolCall = aiMessage.tool_calls[0];
const toolMessage = await tool.invoke(toolCall);
如果您以这种方式调用工具,并希望为 ToolMessage
包含一个 工件,您需要让工具返回包含两个项目的元组:content
和 artifact
。阅读有关 定义返回工件的工具 的更多信息。
最佳实践
在设计模型使用的工具时,请务必记住
- 具有明确 工具调用 API 的聊天模型在工具调用方面将比未微调的模型更出色。
- 如果工具具有精心选择的名称、描述和 JSON 架构,模型将表现更好。这是一种提示工程的另一种形式。
- 简单、范围狭窄的工具比复杂的工具更容易被模型使用。
相关
有关如何使用工具的详细信息,请参阅 工具操作指南。
要使用预构建的工具,请参阅 工具集成文档。
工具包
工具包是工具的集合,这些工具旨在协同使用以完成特定任务。它们具有便捷的加载方法。
所有工具包都公开了一个 getTools
方法,该方法返回一个工具数组。因此,您可以执行以下操作
// Initialize a toolkit
const toolkit = new ExampleTookit(...)
// Get list of tools
const tools = toolkit.getTools()
代理
语言模型本身无法采取行动 - 它们只输出文本。LangChain 的一个重要用例是创建代理。代理是使用 LLM 作为推理工程师来确定要采取哪些行动以及这些行动的输入应该是什么的系统。然后,这些行动的结果可以反馈到代理中,它可以确定是否需要采取更多行动,或者是否可以完成。
LangGraph 是 LangChain 的扩展,专门用于创建高度可控和可定制的代理。请查看该 文档,以深入了解代理概念。
LangChain 中有一个我们正在逐步弃用的传统代理概念:AgentExecutor
。AgentExecutor 本质上是代理的运行时。它是一个很好的入门场所,但是,当您开始拥有更多定制的代理时,它缺乏灵活性。为了解决这个问题,我们构建了 LangGraph 作为这种灵活、高度可控的运行时。
如果您仍在使用 AgentExecutor,请不要担心:我们仍然有一份关于 如何使用 AgentExecutor 的指南。但是,建议您开始迁移到 LangGraph。为了帮助您完成此操作,我们整理了一份 迁移指南,说明如何执行此操作。
ReAct 代理
构建代理的一种流行架构是 ReAct。ReAct 在一个迭代过程中结合了推理和行动 - 事实上,“ReAct” 这个名称代表“推理”和“行动”。
一般流程如下所示
- 模型将“思考”如何针对输入和任何先前的观察采取下一步操作。
- 然后模型将从可用工具中选择一个操作(或选择响应用户)。
- 模型将为该工具生成参数。
- 代理运行时(执行程序)将解析出所选工具并使用生成的參數调用它。
- 执行程序将将工具调用的结果作为观察结果返回给模型。
- 此过程将重复,直到代理选择响应。
有一些基于通用提示的实现不需要任何模型特定功能,但最可靠的实现使用诸如 工具调用 之类功能来可靠地格式化输出并减少差异。
请参阅 LangGraph 文档 以获取更多信息,或参阅 本操作指南 以获取有关迁移到 LangGraph 的具体信息。
回调
LangChain 提供了一个回调系统,允许您挂接到 LLM 应用程序的不同阶段。这对日志记录、监控、流式传输和其他任务非常有用。
您可以通过使用整个 API 中可用的 callbacks
参数来订阅这些事件。该参数是处理程序对象的列表,这些处理程序对象应实现下面更详细介绍的一种或多种方法。
回调事件
事件 | 事件触发器 | 关联方法 |
---|---|---|
聊天模型开始 | 聊天模型开始时 | handleChatModelStart |
LLM 开始 | LLM 开始时 | handleLLMStart |
LLM 新标记 | LLM 或聊天模型发出新标记时 | handleLLMNewToken |
LLM 结束 | LLM 或聊天模型结束时 | handleLLMEnd |
LLM 错误 | LLM 或聊天模型出错时 | handleLLMError |
链开始 | 链开始运行时 | handleChainStart |
链结束 | 链结束时 | handleChainEnd |
链错误 | 链出错时 | handleChainError |
工具开始 | 工具开始运行时 | handleToolStart |
工具结束 | 工具结束时 | handleToolEnd |
工具错误 | 工具出错时 | handleToolError |
代理操作 | 代理采取操作时 | handleAgentAction |
代理完成 | 代理结束时 | handleAgentEnd |
检索器开始 | 检索器开始时 | handleRetrieverStart |
检索器结束 | 检索器结束时 | handleRetrieverEnd |
检索器错误 | 检索器出错时 | handleRetrieverError |
文本 | 运行任意文本时 | handleText |
回调处理程序
CallbackHandlers
是实现 CallbackHandler
接口的对象,该接口为每个可订阅的事件提供一个方法。当事件触发时,CallbackManager
会在每个处理程序上调用相应的方法。
传递回调
callbacks
属性在 API 中的大多数对象(模型、工具、代理等)的两个不同位置可用
- 请求回调:在请求时除了输入数据之外还传递。在所有标准
Runnable
对象上可用。这些回调会被定义它们的父对象的子对象继承。例如,chain.invoke({foo: "bar"}, {callbacks: [handler]})
。 - 构造函数回调:在构造函数中定义,例如
new ChatAnthropic({ callbacks: [handler], tags: ["a-tag"] })
。在这种情况下,回调将用于该对象的所有调用,并且只作用于该对象。例如,如果您使用构造函数回调初始化聊天模型,然后在链中使用它,那么回调只会在调用该模型时被调用。
构造函数回调的作用域仅限于定义它们的父对象。它们不会被该对象的子对象继承。
如果您正在创建一个自定义链或可运行对象,您需要记住将请求时间回调传播到任何子对象。
有关如何使用回调的具体信息,请参阅 此处的相关操作指南。
技术
流式传输
单个 LLM 调用通常比传统的资源请求运行时间长得多。当您构建需要多个推理步骤的更复杂的链或代理时,这种情况会加剧。
幸运的是,LLM 以迭代方式生成输出,这意味着可以在最终响应准备好之前显示合理的中间结果。因此,在可用时立即使用输出已成为构建使用 LLM 的应用程序的 UX 的重要组成部分,以帮助缓解延迟问题,而 LangChain 旨在对流式传输提供一流的支持。
下面,我们将讨论 LangChain 中与流式传输相关的概念和注意事项。
.stream()
LangChain 中的大多数模块都包含 .stream()
方法作为符合人体工程学原理的流式传输接口。.stream()
返回一个迭代器,您可以使用 for await...of
循环使用它。以下是一个使用聊天模型的示例
import { ChatAnthropic } from "@langchain/anthropic";
import { concat } from "@langchain/core/utils/stream";
import type { AIMessageChunk } from "@langchain/core/messages";
const model = new ChatAnthropic({ model: "claude-3-sonnet-20240229" });
const stream = await model.stream("what color is the sky?");
let gathered: AIMessageChunk | undefined = undefined;
for await (const chunk of stream) {
console.log(chunk);
if (gathered === undefined) {
gathered = chunk;
} else {
gathered = concat(gathered, chunk);
}
}
console.log(gathered);
对于不支持原生流式传输的模型(或其他组件),此迭代器只生成一个片段,但您仍然可以在调用它们时使用相同的通用模式。使用 .stream()
还会自动以流式传输模式调用模型,而无需提供其他配置。
每个输出片段的类型取决于组件的类型 - 例如,聊天模型会生成 AIMessageChunks
。由于此方法是 LangChain 表达式语言 的一部分,因此您可以使用 输出解析器 处理来自不同输出的格式差异,以转换每个生成的片段。
您可以查看 本指南,以详细了解如何使用 .stream()
。
.streamEvents()
虽然 .stream()
方法直观,但它只能返回链的最终生成的价值。对于单个 LLM 调用来说,这很好,但当您构建多个 LLM 调用组合在一起的更复杂的链时,您可能希望使用链的中间值以及最终输出 - 例如,在构建文档聊天应用程序时,将源与最终生成的内容一起返回。
有一些方法可以使用 回调 来做到这一点,或者通过构建您的链,使其通过像链接的 .assign()
调用这样的方式将中间值传递到末尾,但 LangChain 还包含一个 .streamEvents()
方法,该方法将回调的灵活性与 .stream()
的符合人体工程学原理的特性相结合。当被调用时,它返回一个迭代器,该迭代器生成 各种类型的事件,您可以根据项目的需要对其进行过滤和处理。
以下是一个小示例,它只打印包含流式聊天模型输出的事件
import { StringOutputParser } from "@langchain/core/output_parsers";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { ChatAnthropic } from "@langchain/anthropic";
const model = new ChatAnthropic({ model: "claude-3-sonnet-20240229" });
const prompt = ChatPromptTemplate.fromTemplate("tell me a joke about {topic}");
const parser = new StringOutputParser();
const chain = prompt.pipe(model).pipe(parser);
const eventStream = await chain.streamEvents(
{ topic: "parrot" },
{ version: "v2" }
);
for await (const event of eventStream) {
const kind = event.event;
if (kind === "on_chat_model_stream") {
console.log(event);
}
}
您可以粗略地将其视为回调事件的迭代器(虽然格式不同) - 并且您可以在几乎所有 LangChain 组件上使用它!
请参阅 本指南,以获取有关如何使用 .streamEvents()
的更详细的信息,或参阅 本指南,以了解如何在链中流式传输自定义事件。
回调
在 LangChain 中,使用 回调 系统可以以最底层的方式从 LLM 流式传输输出。您可以传递一个回调处理程序,该处理程序处理 handleLLMNewToken
事件到 LangChain 组件中。当该组件被调用时,该组件中包含的任何 LLM 或 聊天模型 会使用生成的令牌调用回调。在回调中,您可以将令牌管道到其他目标,例如 HTTP 响应。您还可以处理 handleLLMEnd
事件以执行任何必要的清理工作。
您可以查看 本操作指南部分,以获取有关使用回调的更多详细信息。
回调是 LangChain 中引入的第一个流式传输技术。虽然功能强大且可推广,但对于开发者来说,它们可能很笨拙。例如
- 您需要显式地初始化和管理一些聚合器或其他流来收集结果。
- 执行顺序没有明确保证,理论上回调可能会在
.invoke()
方法完成之后运行。 - 提供者通常会让您传递一个额外的参数来流式传输输出,而不是一次返回所有输出。
- 您通常会忽略实际模型调用的结果,而使用回调结果。
令牌
大多数模型提供者用来衡量输入和输出的单位是称为令牌的单位。令牌是语言模型在处理或生成文本时读取和生成的的基本单位。令牌的确切定义可能因模型训练的具体方式而异 - 例如,在英语中,令牌可以是一个单词,例如“apple”,或一个单词的一部分,例如“app”。
当您向模型发送提示时,提示中的单词和字符会使用分词器编码为令牌。然后模型会流式传输回生成的输出令牌,分词器会将它们解码为人类可读的文本。以下示例显示了 OpenAI 模型如何对 LangChain is cool!
进行分词
您可以看到它被拆分为 5 个不同的令牌,并且令牌之间的边界与单词边界并不完全相同。
语言模型使用令牌而不是更直观的“字符”的原因与它们处理和理解文本的方式有关。在高层次上,语言模型根据初始输入和之前的生成迭代地预测其下一个生成的输出。使用令牌训练模型,语言模型可以处理具有意义的语言单位(如单词或子词),而不是单个字符,这使得模型更容易学习和理解语言的结构,包括语法和语境。此外,使用令牌还可以提高效率,因为与字符级别处理相比,模型处理的文本单位更少。
函数/工具调用
我们将工具调用与函数调用互换使用。虽然函数调用有时是指对单个函数的调用,但我们将所有模型都视为可以返回每个消息中的多个工具或函数调用。
工具调用允许 聊天模型 通过生成与用户定义模式匹配的输出来响应给定的提示。
虽然名称意味着模型正在执行某些操作,但实际上并非如此!模型只生成工具的参数,而实际运行工具(或不运行工具)取决于用户。一个常见的例子是,您不想使用生成的參數调用函数,例如,您希望从非结构化文本中 提取与某些模式匹配的结构化输出。您将为模型提供一个“提取”工具,该工具接受与所需模式匹配的参数,然后将生成的输出视为最终结果。
工具调用并非普遍存在,但许多流行的 LLM 提供者都支持,包括 Anthropic、Cohere、Google、Mistral、OpenAI,甚至通过 Ollama 来支持本地运行的模型。
LangChain 为工具调用提供了一个标准化接口,该接口在不同的模型之间保持一致。
标准接口包括
ChatModel.bind_tools()
:一种指定哪些工具可供模型调用的方法。此方法接受 LangChain 工具 以及模型特定格式。AIMessage.tool_calls
:模型返回的AIMessage
上的属性,用于访问模型请求的工具调用。
工具使用
在模型调用工具之后,您可以通过调用该工具,然后将参数传递回模型来使用该工具。LangChain 提供了 Tool
抽象来帮助您处理此问题。
一般流程如下
- 使用聊天模型响应查询来生成工具调用。
- 使用生成的工具调用作为参数来调用相应的工具。
- 将工具调用的结果格式化为
ToolMessages
。 - 将所有消息列表传递回模型,以便它可以生成最终答案(或调用更多工具)。
这就是工具调用 代理 执行任务和回答查询的方式。
查看以下一些更专注的指南
结构化输出
大型语言模型 (LLM) 能够生成任意文本。这使模型能够对各种输入做出适当的响应,但在某些用例中,将 LLM 的输出限制为特定格式或结构可能会有用。这被称为 **结构化输出**。
例如,如果输出要存储在关系数据库中,那么如果模型生成的输出符合定义的模式或格式,会更容易。从非结构化文本中 提取特定信息 是另一个特别有用这种情况。最常见的输出格式是 JSON,但其他格式(如 XML)也很有用。下面我们将讨论一些从 LangChain 中的模型获取结构化输出的方法。
.withStructuredOutput()
为了方便起见,一些 LangChain 聊天模型支持 .withStructuredOutput()
方法。此方法仅需要一个模式作为输入,并返回一个与请求的模式匹配的对象。通常,此方法仅存在于支持以下描述的更高级方法之一的模型上,并且将在内部使用其中之一。它负责导入合适的输出解析器并以模型的正确格式格式化模式。
这是一个示例
import { z } from "zod";
const joke = z.object({
setup: z.string().describe("The setup of the joke"),
punchline: z.string().describe("The punchline to the joke"),
rating: z.number().optional().describe("How funny the joke is, from 1 to 10"),
});
// Can also pass in JSON schema.
// It's also beneficial to pass in an additional "name" parameter to give the
// model more context around the type of output to generate.
const structuredLlm = model.withStructuredOutput(joke);
await structuredLlm.invoke("Tell me a joke about cats");
{
setup: "Why don't cats play poker in the wild?",
punchline: "Too many cheetahs.",
rating: 7
}
我们建议将此方法作为使用结构化输出的起点
- 它在内部使用其他特定于模型的功能,而无需导入输出解析器。
- 对于使用工具调用的模型,不需要特殊提示。
- 如果支持多种底层技术,则可以提供
method
参数来 切换使用哪一种。
你可能需要或需要使用其他技术,如果
- 你正在使用的聊天模型不支持工具调用。
- 你正在处理非常复杂的模式,并且模型难以生成符合模式的输出。
有关更多信息,请查看此 操作指南。
你也可以查看 此表,以获取支持 .withStructuredOutput()
的模型列表。
原始提示
让模型结构化输出最直观的方式是礼貌地询问。除了你的查询之外,你还可以提供描述你想要哪种输出的说明,然后使用 输出解析器 解析输出,将原始模型消息或字符串输出转换为更易于操作的内容。
原始提示的最大好处是它的灵活性
- 原始提示不需要任何特殊的模型功能,只需要足够的推理能力来理解传递的模式。
- 你可以提示任何你想要的格式,而不仅仅是 JSON。如果你的模型在某种类型的数据(如 XML 或 YAML)上训练得更加深入,这将非常有用。
但是,也有一些缺点
- LLM 是非确定性的,提示 LLM 以完全正确的格式一致地输出数据,以便进行顺利的解析可能出奇地困难并且特定于模型。
- 单个模型根据它们训练的数据会有不同的特性,优化提示可能非常困难。有些模型可能更擅长解释 JSON 模式,其他模型可能最适合 TypeScript 定义,而其他模型可能更喜欢 XML。
虽然模型提供商提供的功能可能会提高可靠性,但无论你选择哪种方法,提示技术对于微调你的结果仍然很重要。
JSON 模式
一些模型,例如 Mistral、OpenAI、Together AI 和 Ollama,支持一项名为 **JSON 模式** 的功能,通常通过配置启用。
启用后,JSON 模式将限制模型的输出始终为某种有效的 JSON。它们通常需要一些自定义提示,但通常比完全原始提示轻得多,更类似于 "你必须始终返回 JSON"
。该 输出通常更容易解析。
它通常也比工具调用更简单直接,并且比工具调用更常见,并且在提示和塑造结果方面比工具调用提供了更大的灵活性。
这是一个示例
import { JsonOutputParser } from "@langchain/core/output_parsers";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { ChatOpenAI } from "@langchain/openai";
const model = new ChatOpenAI({
model: "gpt-4o",
modelKwargs: {
response_format: { type: "json_object" },
},
});
const 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}`;
const prompt = ChatPromptTemplate.fromTemplate(TEMPLATE);
const chain = prompt.pipe(model).pipe(new JsonOutputParser());
await chain.invoke({ question: "What is the powerhouse of the cell?" });
{
answer: "The powerhouse of the cell is the mitochondrion.",
followup_question: "Would you like to learn more about the functions of mitochondria?"
}
有关支持 JSON 模式的模型提供商的完整列表,请参见 此表。
工具调用
对于支持它的模型,工具调用 在结构化输出方面非常方便。它消除了围绕如何最好地提示模式的猜测,而支持内置的模型功能。
它首先通过使用 .bind_tools()
方法将所需的模式直接或通过 LangChain 工具 绑定到 聊天模型 来实现。然后,模型将生成一个包含 tool_calls
字段的 AIMessage
,该字段包含与所需形状匹配的 args
。
在 LangChain 中,你可以使用多种可接受的格式将工具绑定到模型。以下是一个使用 Zod 的示例
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
import { ChatOpenAI } from "@langchain/openai";
const toolSchema = z.object({
answer: z.string().describe("The answer to the user's question"),
followup_question: z
.string()
.describe("A followup question the user could ask"),
});
const model = new ChatOpenAI({
model: "gpt-4o",
temperature: 0,
});
const modelWithTools = model.bindTools([
{
type: "function",
function: {
name: "response_formatter",
description:
"Always use this tool to structure your response to the user.",
parameters: zodToJsonSchema(toolSchema),
},
},
]);
const aiMessage = await modelWithTools.invoke(
"What is the powerhouse of the cell?"
);
aiMessage.tool_calls?.[0].args;
{
answer: 'The powerhouse of the cell is the mitochondrion.',
followup_question: 'What is the main function of the mitochondrion in the cell?'
}
工具调用是一种让模型生成结构化输出的一般一致的方式,并且是 .withStructuredOutput()
方法在模型支持时使用的默认技术。
以下操作指南是使用函数/工具调用进行结构化输出的良好实践资源
少样本提示
提高模型性能最有效的方法之一是向模型提供一些你想让它做的示例。在模型提示中添加示例输入和预期输出的技术被称为“少样本提示”。在进行少样本提示时,需要考虑以下几点
- 示例是如何生成的?
- 每个提示中有多少个示例?
- 示例在运行时是如何选择的?
- 示例在提示中是如何格式化的?
以下是每个方面的注意事项。
1. 生成示例
少样本提示的第一步也是最重要的一步是提出一个好的示例数据集。好的示例应该在运行时相关、清晰、信息丰富,并提供模型之前不知道的信息。
从高级别来看,生成示例的基本方法是
- 手动:一个人或几个人生成他们认为有用的示例。
- 更好的模型:一个更好的(可能是更昂贵/更慢)模型的响应被用作较差(可能是更便宜/更快)模型的示例。
- 用户反馈:用户(或标记者)对与应用程序的交互提供反馈,并根据该反馈生成示例(例如,所有具有积极反馈的交互都可以变成示例)。
- LLM 反馈:与用户反馈相同,但该过程是自动化的,通过让模型自我评估来实现。
哪种方法最好取决于你的任务。对于需要真正理解少量核心原则的任务,手工制作一些真正好的示例可能很有价值。对于正确行为空间更广阔、更细致的任务,以更自动化的方式生成许多示例可能很有用,这样更可能有一些与任何运行时输入高度相关的示例。
单轮与多轮示例
在生成示例时,另一个需要考虑的维度是示例实际上展示了什么。
最简单的示例类型只有用户输入和预期的模型输出。这些是单轮示例。
一种更复杂的示例类型是整个对话,其中模型最初响应不正确,然后用户告诉模型如何纠正答案。这被称为多轮示例。多轮示例对于更细致的任务可能有用,在这些任务中,展示常见错误并详细说明它们为什么错误以及应该做些什么可能有用。
2. 示例数量
有了示例数据集后,我们需要考虑每个提示中应该有多少个示例。关键权衡是,更多的示例通常会提高性能,但更大的提示会增加成本和延迟。并且超过某个阈值,过多的示例可能会开始让模型感到困惑。找到合适的示例数量高度依赖于模型、任务、示例的质量以及你的成本和延迟限制。据传,模型越好,它需要执行良好所需的示例就越少,并且你越快达到添加更多示例时急剧递减的回报。但是,可靠地回答这个问题的最佳/唯一方法是进行一些实验,使用不同数量的示例。
3. 选择示例
假设我们不是将整个示例数据集添加到每个提示中,那么我们需要有一种方法根据给定的输入从我们的数据集中选择示例。我们可以这样做
- 随机地
- 通过(语义或基于关键字)输入的相似性
- 基于其他一些约束,如令牌大小
LangChain 有许多 ExampleSelectors
,它们使使用任何这些技术变得容易。
通常,通过语义相似性选择会导致最佳的模型性能。但这有多重要,再次取决于模型和任务,值得进行实验。
4. 格式化示例
如今,大多数最先进的模型都是聊天模型,因此我们将重点介绍为这些模型格式化示例。我们的基本选择是在系统提示中插入示例
- 作为字符串
- 作为它们自己的消息
如果我们将示例作为字符串插入系统提示中,我们需要确保模型清楚地知道每个示例从哪里开始,以及哪些部分是输入,哪些部分是输出。不同的模型对不同的语法有更好的响应,例如 ChatML、XML、TypeScript 等。
如果我们将示例作为消息插入,其中每个示例都表示为一系列人类、AI 消息,我们可能还想为消息分配 名称,例如 "exampleUser"
和 "exampleAssistant"
,以明确表明这些消息对应于与最新输入消息不同的参与者。
格式化工具调用示例
格式化示例作为消息的一个棘手的地方是,当我们的示例输出包含工具调用时。这是因为不同的模型对任何工具调用生成时允许的消息序列类型有不同的约束。
- 一些模型要求任何包含工具调用的 AIMessage 必须紧随其后的是每个工具调用的 ToolMessages。
- 一些模型还要求任何 ToolMessages 必须紧随其后的是一个 AIMessage,然后才是下一个 HumanMessage。
- 一些模型要求如果聊天历史记录中包含任何工具调用/ToolMessages,则将工具传递给模型。
这些要求是模型特定的,应检查您正在使用的模型。如果您的模型要求在工具调用之后使用 ToolMessages 和/或在 ToolMessages 之后使用 AIMessages,而您的示例只包含预期的工具调用,而不包含实际的工具输出,您可以尝试在每个示例的末尾添加包含通用内容的虚拟 ToolMessages / AIMessages,以满足 API 约束。
在这些情况下,将示例作为字符串而不是消息插入特别值得尝试,因为使用虚拟消息可能会对某些模型产生负面影响。
您可以查看一个案例研究,了解 Anthropic 和 OpenAI 如何在两个不同的工具调用基准上对不同的少样本提示技术做出反应,这里。
检索
LLM 在一个庞大但固定的数据集上进行训练,这限制了它们对私有信息或最新信息进行推理的能力。用特定事实微调 LLM 是缓解这种情况的一种方法,但通常不适合事实召回,而且可能很昂贵。检索是指向 LLM 提供相关信息以改善其对给定输入的响应的过程。检索增强生成 (RAG) 是使用检索到的信息来对 LLM 生成(输出)进行接地的过程。
RAG 只有在检索到的文档相关性和质量良好的情况下才有效。幸运的是,可以采用一组新兴技术来设计和改进 RAG 系统。我们专注于对这些技术的分类和总结(见下图),并在以下部分中分享一些高级策略指导。您可以(也应该)尝试将不同的部分组合在一起。您可能还会发现此 LangSmith 指南对于展示如何评估应用程序的不同迭代很有用。
查询翻译
首先,考虑您的 RAG 系统的用户的输入。理想情况下,RAG 系统可以处理各种各样的输入,从措辞不当的问题到复杂的多分支查询。使用 LLM 来审查和选择性地修改输入是查询翻译背后的核心思想。 这充当一个通用缓冲区,优化原始用户输入以适应您的检索系统。例如,这可以像提取关键字一样简单,也可以像为复杂查询生成多个子问题一样复杂。
名称 | 何时使用 | 描述 |
---|---|---|
多查询 | 当您需要涵盖问题的多个方面时。 | 从多个角度重写用户问题,为每个重写的问题检索文档,返回所有查询的唯一文档。 |
分解(Python 食谱) | 当一个问题可以分解成更小的子问题时。 | 将问题分解成一组子问题/问题,这些子问题可以按顺序解决(使用第一个问题 + 检索来解决第二个问题)或并行解决(将每个答案合并成最终答案)。 |
后退(Python 食谱) | 当需要更高层次的概念理解时。 | 首先提示 LLM 询问有关更高层次的概念或原则的通用后退问题,并检索有关它们的既定事实。使用此接地来帮助回答用户问题。 |
HyDE(Python 食谱) | 如果您在使用原始用户输入检索相关文档时遇到挑战。 | 使用 LLM 将问题转换为回答问题的假设文档。使用嵌入的假设文档来检索真实的文档,前提是文档-文档相似度搜索可以产生更多相关的匹配。 |
路由
其次,考虑您 RAG 系统可用的数据源。您希望跨多个数据库或跨结构化和非结构化数据源进行查询。使用 LLM 来审查输入并将其路由到适当的数据源是跨源查询的一种简单有效的方法。
名称 | 何时使用 | 描述 |
---|---|---|
逻辑路由 | 当您可以用规则提示 LLM 来决定将输入路由到哪里时。 | 逻辑路由可以使用 LLM 推理查询并选择最合适的数据存储。 |
语义路由 | 当语义相似性是确定将输入路由到哪里的一种有效方法时。 | 语义路由嵌入查询和(通常是一组)提示。然后根据相似性选择合适的提示。 |
查看我们的 Python 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 | 向量存储 + 文档存储 | 否 | 如果您的页面包含许多最佳单独索引的较小独立信息片段,但最佳全部检索。 | 这涉及为每个文档索引多个块。然后找到嵌入空间中最相似的块,但您检索整个父文档并返回它(而不是单个块)。 |
多向量 | 向量存储 + 文档存储 | 有时在索引期间 | 如果您能够从文档中提取您认为比文本本身更相关的索引信息。 | 这涉及为每个文档创建多个向量。每个向量可以通过多种方式创建——例如,包括文本的摘要和假设问题。 |
时间加权向量存储 | 向量存储 | 否 | 如果您有与文档关联的时间戳,并且您希望检索最新的文档 | 这根据语义相似性(如正常向量检索)和最近度(查看索引文档的时间戳)来获取文档 |
第五,考虑提高相似度搜索本身质量的方法。嵌入模型将文本压缩为固定长度(向量)表示,这些表示捕获文档的语义内容。这种压缩对于搜索/检索很有用,但将很大的压力放在单个向量表示上,使其捕获文档的语义细微差别/细节。在某些情况下,不相关或冗余的内容会稀释嵌入的语义效用。
除了基础的操作以外,还有以下技巧可以提升检索质量。词向量擅长捕捉语义信息,但在处理基于关键词的查询时可能会遇到困难。许多 向量数据库 提供内置的 混合搜索 功能,将关键词和语义相似性相结合,从而融合了两种方法的优点。此外,许多向量数据库还拥有 最大边缘相关性 搜索功能,旨在通过避免返回相似和冗余的文档来使搜索结果多样化。
名称 | 何时使用 | 描述 |
---|---|---|
混合搜索 | 当需要结合关键词和语义相似性时。 | 混合搜索结合了关键词和语义相似性,融合了两种方法的优点。 |
最大边缘相关性 (MMR) | 当需要使搜索结果多样化时。 | MMR 试图通过避免返回相似和冗余的文档来使搜索结果多样化。 |
后处理
第六,考虑如何过滤或排名检索到的文档。如果你 组合来自多个来源的文档,这非常有用,因为它可以降低不太相关的文档的排名,并且 / 或 压缩相似的文档。
名称 | 索引类型 | 使用 LLM | 何时使用 | 描述 |
---|---|---|---|---|
上下文压缩 | 任何 | 有时 | 如果你发现检索到的文档包含太多无关信息,并分散了 LLM 的注意力。 | 这在另一个检索器之上添加了一个后处理步骤,仅从检索到的文档中提取最相关的信息。这可以使用词向量或 LLM 完成。 |
集成 | 任何 | 否 | 如果你有多种检索方法,并且想要尝试将它们结合起来。 | 这会从多个检索器中获取文档,然后将它们组合起来。 |
重新排序 | 任何 | 是 | 如果你想根据相关性对检索到的文档进行排名,特别是如果你想组合来自多个检索方法的结果。 | 给定一个查询和一个文档列表,重新排序将文档从与查询语义最相关到最不相关的顺序进行索引。 |
请查看我们的 Python RAG 从零开始视频,了解有关 RAG-Fusion 的信息,该视频介绍了一种针对多个查询进行后处理的方法:从多个角度重写用户问题,为每个重写的问题检索文档,并将多个搜索结果列表的排名组合起来,生成一个统一的排名,使用 倒数排名融合 (RRF)。
生成
最后,考虑在你的 RAG 系统中建立自纠正机制。 RAG 系统可能会受到低质量检索(例如,如果用户问题超出了索引的范围)和 / 或生成幻觉的影响。一个简单的检索-生成管道无法检测或自我纠正这些类型的错误。在 "流工程" 的概念已在 代码生成环境中引入:使用单元测试迭代构建代码问题的答案,以检查和自我纠正错误。几项研究已将此应用于 RAG,例如 Self-RAG 和 Corrective-RAG。在这两种情况下,都会在 RAG 答案生成流程中执行文档相关性、幻觉和 / 或答案质量检查。
我们发现,图是一种可靠地表达逻辑流的好方法,并且使用 LangGraph 实现了这些论文中的几个想法,如下图所示(红色 - 路由,蓝色 - 回退,绿色 - 自纠正)
- 路由:自适应 RAG (论文)。将问题路由到不同的检索方法,如上所述。
- 回退:纠正式 RAG (论文)。如果文档与查询无关,则回退到网络搜索。
- 自纠正:Self-RAG (论文)。修复包含幻觉或未解决问题的答案。
名称 | 何时使用 | 描述 |
---|---|---|
Self-RAG | 当需要修复包含幻觉或无关内容的答案时。 | Self-RAG 在 RAG 答案生成流程中执行文档相关性、幻觉和答案质量检查,迭代构建答案并自我纠正错误。 |
Corrective-RAG | 当需要为低相关性文档提供回退机制时。 | Corrective-RAG 包含一个回退机制(例如,网络搜索),如果检索到的文档与查询无关,可以确保更高的质量和更相关的检索。 |
查看几个展示使用 LangGraph 的 RAG 的视频和教程。
文本分割
LangChain 提供了许多不同类型的 `文本分割器`。它们可以在主 `langchain` 包中找到,但也可以单独在 `@langchain/textsplitters` 包中使用。
表格列
- 名称: 文本分割器的名称
- 类: 实现此文本分割器的类
- 分割依据: 此文本分割器如何分割文本
- 添加元数据: 此文本分割器是否添加关于每个块来自何处的元数据。
- 描述: 对分割器的描述,包括何时使用它的建议。
名称 | 类 | 分割依据 | 添加元数据 | 描述 |
---|---|---|---|---|
递归 | RecursiveCharacterTextSplitter | 用户定义的字符列表 | 递归地分割文本。这种分割尝试将相关文本片段保持在一起。这是 `推荐的方法`,建议从这种方法开始分割文本。 | |
代码 | 多种语言 | 代码 (Python, JS) 特定字符 | 根据代码语言特有的字符分割文本。提供 15 种不同的语言可供选择。 | |
令牌 | 多种类 | 令牌 | 根据令牌分割文本。存在几种不同的令牌测量方法。 | |
字符 | CharacterTextSplitter | 用户定义的字符 | 根据用户定义的字符分割文本。一种比较简单的方法。 |
评估
评估是评估 LLM 驱动的应用程序的性能和有效性的过程。它涉及根据一组预定义的标准或基准测试模型的响应,以确保它符合所需的质量标准并实现预期目的。此过程对于构建可靠的应用程序至关重要。
LangSmith 通过以下几种方式帮助完成此过程
- 它通过跟踪和标注功能简化了数据集的创建和管理。
- 它提供了一个评估框架,可以帮助你定义指标并在你的数据集中运行你的应用程序。
- 它允许你跟踪结果随时间的变化,并自动按计划或作为 CI/Code 的一部分运行你的评估器。
要了解更多信息,请查看 LangSmith 指南。
跟踪
跟踪本质上是你的应用程序从输入到输出所执行的一系列步骤。跟踪包含称为 `运行` 的单个步骤。这些可以是模型、检索器、工具或子链的单个调用。跟踪使你能够在你的链和代理内部进行观察,对于诊断问题至关重要。
要深入了解,请查看 LangSmith 概念指南。
生成式 UI
LangChain.js 提供了一些模板和示例,展示了生成式 UI 以及其他将数据从服务器流式传输到客户端的方法,特别是在 React/Next.js 中。
你可以在官方的 LangChain.js Next.js 模板 中找到生成式 UI 的模板。
对于流式传输代理响应和中间步骤,你可以在 此处找到模板和文档。
最后,可以在这里找到流式传输工具调用和结构化输出 这里。