概念指南
本节包含 LangChain 主要部分的介绍。
架构
LangChain 作为框架包含几个部分。以下图表显示了它们之间的关系。
@langchain/core
此包包含不同组件的基本抽象以及将它们组合在一起的方式。LLM、向量存储、检索器等核心组件的接口在此处定义。此处未定义任何第三方集成。为了保持尽可能轻量级,依赖项被有意地保留在非常轻量级的状态。
@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
yarn add langchain
pnpm add langchain
如果您想使用特定集成,您将需要单独安装它们。查看此处以获取集成列表以及如何安装它们。
要使用 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”链到最复杂的链(我们已经看到人们成功地在生产环境中运行了包含数百个步骤的 LCEL 链)。为了突出您可能想使用 LCEL 的一些原因
一流的流式支持 使用 LCEL 构建链时,您将获得最佳的首次令牌时间(首个输出块出现之前的时间)。对于某些链来说,这意味着我们可以直接从 LLM 流式传输令牌到流式输出解析器,并且您将以与 LLM 提供商输出原始令牌相同的速率获得解析后的增量输出块。
优化的并行执行 每当您的 LCEL 链具有可以并行执行的步骤(例如,如果从多个检索器获取文档)时,我们都会自动执行它以获得最小的延迟。
重试和回退 为 LCEL 链的任何部分配置重试和回退。这是使您的链在规模上更可靠的好方法。我们目前正在努力为重试/回退添加流式支持,因此您可以在没有任何延迟成本的情况下获得额外的可靠性。
访问中间结果 对于更复杂的链来说,在生成最终输出之前访问中间步骤的结果通常非常有用。这可以用于让最终用户知道某些事情正在发生,或者只是为了调试您的链。您可以流式传输中间结果,它在每个 LangServe 服务器上可用。
输入和输出模式 输入和输出模式为每个 LCEL 链提供从链结构推断的模式。这可以用于验证输入和输出,并且是 LangServe 的重要组成部分。
无缝 LangSmith 追踪 随着链变得越来越复杂,了解每一步到底发生了什么变得越来越重要。使用 LCEL,所有步骤都会自动记录到 LangSmith 中,以获得最大的可观察性和可调试性。
可运行接口
为了尽可能轻松地创建自定义链,我们实现了一个 "Runnable" 协议。许多 LangChain 组件都实现了 Runnable
协议,包括聊天模型、LLM、输出解析器、检索器、提示模板等等。还有一些用于处理可运行程序的有用原语,您可以在下面阅读相关内容。
这是一个标准接口,它使定义自定义链以及以标准方式调用它们变得容易。标准接口包括
输入类型 和 输出类型 随组件而异
组件 | 输入类型 | 输出类型 |
---|---|---|
提示 | 对象 | PromptValue |
聊天模型 | 单个字符串、聊天消息列表或 PromptValue | ChatMessage |
LLM | 单个字符串、聊天消息列表或 PromptValue | 字符串 |
输出解析器 | LLM 或聊天模型的输出 | 取决于解析器 |
检索器 | 单个字符串 | 文档列表 |
工具 | 单个字符串或对象,取决于工具 | 取决于工具 |
组件
LangChain 提供标准的、可扩展的接口和外部集成,用于各种组件,这些组件对于使用 LLM 进行构建非常有用。LangChain 实现了一些组件,我们依靠第三方集成来实现一些组件,而另一些组件则是两者兼而有之。
聊天模型
使用消息序列作为输入并返回聊天消息作为输出的语言模型(与使用纯文本相反)。这些通常是较新的模型(较旧的模型通常是 LLM
,见下文)。聊天模型支持为对话消息分配不同的角色,有助于区分来自人工智能、用户和指令(例如系统消息)的消息。
尽管底层模型是消息输入、消息输出,但 LangChain 包装器也允许这些模型以字符串作为输入。这使它们具有与 LLM 相同的接口(并且更易于使用)。当字符串作为输入传入时,它将在传递到底层模型之前在幕后转换为 HumanMessage
。
LangChain 不托管任何聊天模型,而是依靠第三方集成。
我们在构建 ChatModels 时有一些标准化参数
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 或 ChatModel,也可以转换为字符串或消息数组。此 PromptValue 存在的目的是为了方便在字符串和消息之间切换。
提示模板有几种不同的类型。
字符串提示模板
这些提示模板用于格式化单个字符串,通常用于更简单的输入。例如,构造和使用提示模板的常见方法如下:
import { PromptTemplate } from "@langchain/core/prompts";
const promptTemplate = PromptTemplate.fromTemplate(
"Tell me a joke about {topic}"
);
await promptTemplate.invoke({ topic: "cats" });
聊天提示模板
这些提示模板用于格式化消息数组。这些“模板”本身由模板数组组成。例如,构造和使用 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
变量进行格式化。
消息占位符
此提示模板负责在特定位置添加消息数组。在上面的 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 中的文档对象包含有关某些数据的信息。它有两个属性。
pageContent: string
:此文档的内容。目前仅为字符串。metadata: Record<string, any>
:与该文档关联的任意元数据。可以跟踪文档 ID、文件名等。
文档加载器
这些类加载文档对象。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
MAY 具有与之关联的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 模式的模型提供商的完整列表,请参阅 此表。
工具调用
对于支持它的模型,工具调用 在结构化输出方面非常方便。它消除了围绕如何最好地提示模式的猜测,转而使用内置的模型功能。
它通过首先将所需的模式直接或通过 LangChain 工具 绑定到 聊天模型 (使用 .bind_tools()
方法)来工作。然后,模型将生成一个包含一个 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 系统可以处理各种输入,从措辞不当的问题到复杂的 multipart 查询。使用 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 的处理能力越来越强,可以处理数百万个 token。
有两种方法可以解决这种紧张关系:(1)多向量检索器使用 LLM 将文档转换为任何形式(例如,通常转换为摘要),该形式非常适合索引,但会将完整文档返回给 LLM 以进行生成。(2)父文档检索器嵌入文档块,但也返回完整文档。这样做的目的是兼顾两方面的优势:使用简洁的表示(摘要或块)进行检索,但使用完整文档进行答案生成。
名称 | 索引类型 | 使用 LLM | 何时使用 | 说明 |
---|---|---|---|---|
向量存储 | 向量存储 | 否 | 如果你刚刚开始,并且正在寻找快速简单的工具。 | 这是最简单的方法,也是最容易上手的方法。它涉及为每段文本创建嵌入。 |
父文档 | 向量存储 + 文档存储 | 否 | 如果你的页面包含许多最佳地单独索引的较小片段,但最佳地一起检索。 | 这涉及为每个文档索引多个块。然后,你会找到在嵌入空间中最为相似的块,但你会检索整个父文档并返回它(而不是单个块)。 |
多向量 | 向量存储 + 文档存储 | 有时在索引期间 | 如果你能够从文档中提取你认为比文本本身更适合索引的信息。 | 这涉及为每个文档创建多个向量。每个向量可以通过无数种方式创建 - 例如,文本的摘要和假设问题。 |
时间加权向量存储 | 向量存储 | 否 | 如果你与文档关联了时间戳,并且想要检索最新的文档 | 这会根据语义相似性(如正常向量检索)和最新性(查看索引文档的时间戳)的组合来获取文档。 |
第五,考虑如何提高相似性搜索本身的质量。嵌入模型将文本压缩成固定长度(向量)表示,这些表示捕获文档的语义内容。这种压缩对于搜索/检索很有用,但会给这种单一向量表示带来沉重的负担,要求它捕获文档的语义细微差别/细节。在某些情况下,不相关或冗余的内容会降低嵌入的语义有用性。
有一些额外的技巧可以提高检索的质量。嵌入在捕获语义信息方面非常出色,但在处理基于关键词的查询时可能很困难。许多向量存储提供了内置的混合搜索,可以将关键词和语义相似性结合起来,从而兼顾两种方法的优势。此外,许多向量存储都有最大边际相关性,它试图使搜索结果多样化,以避免返回相似和冗余的文档。
名称 | 何时使用 | 说明 |
---|---|---|
混合搜索 | 当结合基于关键词的相似性和语义相似性时。 | 混合搜索结合了关键词和语义相似性,兼顾了两种方法的优势。 |
最大边缘相关性 (MMR) | 当需要使搜索结果多样化时。 | MMR 试图使搜索结果多样化,以避免返回相似且重复的文档。 |
后处理
第六,考虑过滤或排名检索到的文档的方法。如果您正在 组合从多个来源返回的文档,这非常有用,因为它可以降低不太相关的文档的排名,或者/和 压缩类似的文档。
名称 | 索引类型 | 使用 LLM | 何时使用 | 说明 |
---|---|---|---|---|
上下文压缩 | 任何 | 有时 | 如果您发现检索到的文档包含太多不相关的信息,并且分散了 LLM 的注意力。 | 这在另一个检索器之上添加了一个后处理步骤,并仅从检索到的文档中提取最相关的信息。这可以使用嵌入或 LLM 来完成。 |
集成 | 任何 | 否 | 如果您有多种检索方法,并且想尝试将它们组合起来。 | 这从多个检索器中获取文档,然后将它们组合起来。 |
重新排名 | 任何 | 是 | 如果您想根据相关性对检索到的文档进行排名,特别是如果您想组合来自多种检索方法的结果。 | 给定一个查询和一个文档列表,Rerank 会根据查询的语义相关性对文档进行索引,从最高到最低。 |
请参阅我们关于 RAG-Fusion 的 Python 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 的多个视频和食谱
文本拆分
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 的模板。
要流式传输代理响应和中间步骤,您可以在 此处找到模板和文档。
最后,流式传输工具调用和结构化输出可以在 此处 找到。