AI知识库

53AI知识库

学习大模型的前沿技术与行业应用场景


一文带你了解 LangChain 核心概念
发布日期:2024-09-05 21:06:45 浏览次数: 1730


概念指南

本节介绍了 LangChain 的关键部分,包括:

  • 架构:LangChain 的整体设计和结构。
  • 组件:LangChain 中的各个功能模块。
  • 技巧:在使用 LangChain 时的一些实用方法和技巧。

LangChain 架构

LangChain 作为一个框架,由多个package(包)组成。

langchain-core

这个package(包)包含了不同组件的基本抽象和组合方式。核心组件(如 LLMs向量存储检索器等)的接口都在这里定义。此包不包含任何第三方集成,依赖项被有意保持得非常轻量。

langchain

主要的 langchain 包包含了组成应用程序认知架构chains, agentsretrieval strategies(检索策略)。这里的内容并非第三方集成。所有的chains, agentsretrieval strategies(检索策略)都不是针对某一特定集成,而是适用于所有集成的通用功能。

langchain-community

这个package(包)包含了由 LangChain 社区维护的第三方集成。关键合作伙伴的包被单独分离出来。此package(包)包含了各种组件的所有集成(如 LLMs、向量存储、检索器)。为了保持包的轻量化,此包中的所有依赖项都是可选的。

Partner packages

虽然大多数集成都在 langchain-community 包中,但我们仍将一些流行的集成分离成它们自己的包(例如 langchain-openailangchain-anthropic 等)。这样做是为了更好地支持这些重要的集成。

langgraph

langgraphlangchain 的扩展,旨在通过将步骤建模为图中的边和节点,构建稳健且具有状态的多角色 LLM 应用程序

LangGraph 提供了用于创建常见类型代理的高级接口,以及用于组合自定义流程的低级 API。

langserve

用于将 LangChain chains部署为  REST API 的包。它使生产级别的 API 快速上线变得更加容易。

LangSmith

一个开发者平台,用于调试测试评估监控 LLM 应用程序

LangChain 表达式语言 (LCEL)

LangChain 表达式语言 (LCEL) 是一种声明式方式,用于将 LangChain 组件串联在一起。LCEL 从一开始就设计为支持将原型直接投入生产,无需修改代码,无论是最简单的“提示 + LLM”链,还是最复杂的链(我们见过在生产环境中成功运行包含数百个步骤的 LCEL 链)。

以下是使用 LCEL 的几个理由:

  • 一流的流式支持:当使用 LCEL 构建chains(链条)时,能够获得最佳的首字节时间(从输出的第一个块开始的时间)。对于某些chains(链条),这意味着我们直接从 LLM 流式传输令牌到流式输出解析器,你将以与 LLM 提供者输出原始令牌相同的速度获得解析后的增量输出块。

  • 异步支持:任何使用 LCEL 构建的chains(链条)都可以通过同步 API(例如,在 Jupyter notebook 中进行原型开发时)以及异步 API(例如,在 LangServe 服务器中)调用。这使得可以在原型和生产中使用相同的代码,具有良好的性能,并能够在同一服务器中处理多个并发请求。

  • 并行执行优化:当 LCEL chains(链条)中有可并行执行的步骤(例如,从多个检索器中获取文档)时,我们会自动执行,并在同步和异步接口中以最小的延迟完成任务。

  • 重试和回退:可以为 LCEL chains(链条)中的任何部分配置重试和回退。这是一种提高大规模chains(链条)可靠性的重要方式。我们目前正在添流式重试/回退支持,这样你可以在不增加延迟的情况下获得更高的可靠性。

  • 访问中间结果:对于更复杂的链条,访问中间步骤的结果通常非常有用,即使在最终输出生成之前。这可以用于让最终用户知道某些操作正在进行,或者仅用于调试你的链条。你可以使用流式访问中间结果,并且在每个 LangServe 服务器上都可以使用。

  • 输入和输出模式:输入和输出模式为每个 LCEL 链条提供了基于 Pydantic 和 JSONSchema 推断的模式。这可以用于输入和输出的验证,是 LangServe 的核心组成部分。

  • 无缝的 LangSmith 跟踪:随着你的链条变得越来越复杂,理解每个步骤的具体操作变得越来越重要。使用 LCEL,所有步骤都自动记录到 LangSmith,以实现最大程度的可观察性和可调试性,文档地址:https://docs.smith.langchain.com

LCEL 旨在为LLMChainConversational RetrievalChain等传统子类链的行为和定制提供一致性。许多传统链条隐藏了重要的细节,如提示,而随着更多不同类型的模型出现,定制化变得越来越重要。

Runnable interface

为了尽可能简化自定义链的创建,我们实现了一个“Runnable”协议。许多 LangChain 组件都实现了 Runnable 协议,包括chat models(聊天模型)LLMsoutput parsers(输出解析器)retrievers(检索器)prompt templates(提示模板)等。还有一些用于处理 Runnable 的有用原语,详情可见下文。

这是一个标准接口,它使定义自定义链以及以标准方式调用它们变得容易。标准接口包括:

  • stream:stream back chunks of the response.
  • invoke:call the chain on an input.
  • batch:call the chain on a list of inputs.

这些方法还有对应的异步方法,可与 asyncio 的 await 语法一起使用,以实现并发:

  • astream:异步返回响应的分块数据流
  • ainvoke:异步对单个输入调用链
  • abatch:异步对输入列表调用链
  • astream_log:在返回最终响应的同时,流式返回中间步骤
  • astream_events:beta 版本,流式返回链条中发生的事件(在 langchain-core 0.1.14 中引入)

输入类型和输出类型因组件而异:

组件输入类型输出类型
Prompt字典PromptValue
ChatModel单个字符串、聊天消息列表或 PromptValueChatMessage
LLM单个字符串、聊天消息列表或 PromptValue字符串
OutputParserLLM 或 ChatModel 的输出取决于解析器
Retriever单个字符串文档列表
Tool单个字符串或字典(取决于工具)取决于工具

所有的 Runnable 都提供输入和输出模式,以便检查输入和输出:

  • input_schema:根据 Runnable 的结构自动生成的输入 Pydantic 模型
  • output_schema:根据 Runnable 的结构自动生成的输出 Pydantic 模型

Components(组件)

LangChain 提供了标准的、可扩展的接口和外部集成,这些接口和集成对于使用 LLM 构建应用非常有用。LangChain 自己实现了一些组件,对于其他组件我们依赖第三方集成,还有一些则是两者的结合。

Chat models(聊天模型)

使用消息序列作为输入并返回聊天消息作为输出的语言模型(与使用纯文本不同)。这些通常是较新的模型(较旧的模型通常是 LLM)。聊天模型支持为对话消息分配不同的角色,有助于区分来自 AI、用户以及系统消息等指令的消息。

尽管底层模型是以消息为输入和输出的,LangChain 的封装器也允许这些模型接受字符串作为输入。这意味着你可以轻松地将聊天模型用作 LLM。

当字符串作为输入传递时,它会被转换为 HumanMessage,然后传递给底层模型。

LangChain 不托管任何聊天模型,我们依赖第三方集成。

在构建聊天模型(ChatModels)时,我们有一些标准化的参数:

  • model: 模型名称
  • temperature: 采样温度
  • timeout: 请求超时时间
  • max_tokens: 生成的最大 token 数
  • stop: 默认停止序列
  • max_retries: 请求重试的最大次数
  • api_key: 模型提供商的 API 密钥
  • base_url: 发送请求的端点

重要注意事项:

  • 标准参数仅适用于提供具有预期功能参数的模型提供商。例如,有些提供商不提供最大输出 token 的配置,因此 max_tokens 在这些情况下无法支持。
  • 标准参数目前仅在具有独立集成包的集成(例如 langchain-openai, langchain-anthropic 等)中强制执行,不适用于 langchain-community 中的模型。
  • 聊天模型还接受其他特定于该集成的参数。要查找聊天模型支持的所有参数,请访问该模型的 API 参考文档。

一些聊天模型经过专门微调以用于工具调用,并提供了专门的 API。通常,此类模型在工具调用方面表现优于未微调的模型,建议在需要工具调用的场景中使用。有关更多信息,请参阅工具调用部分(https://python.langchain.com/v0.2/docs/concepts/#functiontool-calling)。

有关如何使用聊天模型的具体信息,请参阅这里的相关操作指南(https://python.langchain.com/v0.2/docs/how_to/#chat-models)

Multimodality(多模态)

一些聊天模型是多模态的,能够接受图像音频甚至视频作为输入。目前,这些模型仍较为少见,因此模型提供商尚未对定义 API 的“最佳”方式达成标准。多模态输出更为少见。因此,我们的多模态抽象保持了相对轻量,并计划在领域成熟时进一步巩固多模态 API 和交互模式。

LangChain 中,大多数支持多模态输入的聊天模型也接受 OpenAI 的内容块格式的这些值。目前,这主要限制于图像输入。对于像 Gemini 这样的模型,支持视频和其他字节输入,API 也支持原生的、模型特定的表示方式。

有关如何使用多模态模型的具体信息,请参阅这里的相关操作指南(https://python.langchain.com/v0.2/docs/how_to/#multimodal)

有关具有多模态模型的 LangChain 模型提供商的完整列表,请查看此表格(https://python.langchain.com/v0.2/docs/integrations/chat/#advanced-features)

LLMs

警告: 纯文本输入/输出的 LLM 通常是较旧或较低级的模型。许多新的热门模型在用于聊天完成时表现最佳,即使在非聊天用例中也是如此。

语言模型接受字符串作为输入并返回字符串。这些通常是较旧的模型(较新的模型通常是Chat Models:https://python.langchain.com/v0.2/docs/concepts/#chat-models)。

尽管底层模型是字符串输入和输出,LangChain 的封装器也允许这些模型接受消息作为输入。这使得它们具有与聊天模型相同的接口。当消息作为输入传递时,它们会在底层被格式化为字符串,然后传递给底层模型。

LangChain 不托管任何 LLM,而是依赖于第三方集成

有关如何使用 LLM 的具体信息,请参阅操作指南(https://python.langchain.com/v0.2/docs/how_to/#llms)

Messages(消息)

一些语言模型接受消息列表作为输入并返回消息。有几种不同类型的消息。所有消息都有role(角色), content(内容), 和response_metadata(响应元数据)属性。

1、role(角色)描述了谁在说这条消息。标准角色包括“user(用户)”、“assistant(助手)”、“system(系统)”和“tool(工具)”。LangChain 为不同角色提供了不同的消息类。

2、content(内容)描述了消息的内容。这可以是几种不同的形式:

  • 字符串(大多数模型处理这种类型的内容)
  • 字典列表(用于多模态输入,其中字典包含有关输入类型和输入位置的信息)

此外,消息还可以有一个可选的 name 属性,用于区分具有相同角色的多个发言者。例如,如果聊天历史中有两个用户,区分他们可能会很有用。但并非所有模型都支持此功能。

HumanMessage
这表示一个角色为“用户”的消息。

AIMessage
这表示一个角色为“助手”的消息。除了 content 属性,这些消息还具有:

  • response_metadata

    response_metadata 属性包含有关响应的额外元数据。这里的数据通常特定于每个模型提供者。这是存储诸如日志概率和令牌使用等信息的地方。

  • tool_calls

    这些表示语言模型调用工具的决策。它们作为 AIMessage 输出的一部分包含在内。可以通过 .tool_calls 属性从中访问。

    这个属性返回一个 ToolCalls 列表。ToolCall 是一个包含以下参数的字典:

    • name: 应调用的工具的名称。
    • args: 该工具的参数。
    • id: 工具调用的 id。

SystemMessage
这表示一个角色为“系统”的消息,告诉模型如何行为。并不是所有模型提供者都支持此功能。

ToolMessage
这表示一个角色为“工具”的消息,包含调用工具的结果。除了role(角色)content(内容)外,这条消息还有:

  • tool_call_id 字段,传达调用工具的 id,以生成此结果。
  • artifact 字段,可用于传递工具执行的任意工件,这些工件对于跟踪很有用,但不应发送到模型。

(Legacy)FunctionMessage
这是一种旧版消息类型,对应于 OpenAI 的遗留函数调用 API。应使用 ToolMessage 来匹配更新后的工具调用 API。

它表示函数调用的结果。除role(角色)content(内容)之外,这条消息还包含一个 name 参数,用于指示生成此结果的函数的名称。

Prompt templates(提示模板)

Prompt templates(提示模板)帮助将用户输入参数转换为语言模型的指令。这可以用来引导模型的响应,帮助它理解上下文,并生成相关且连贯的语言输出。

提示模板接受一个字典作为输入,其中每个键代表要填充的提示模板中的变量。

提示模板输出一个 PromptValue。这个 PromptValue 可以传递给 LLM(语言模型)或 ChatModel(聊天模型),也可以转换为字符串或消息列表。之所以存在这个 PromptValue,是为了方便在字符串和消息之间切换。

有几种不同类型的提示模板:

1、字符串提示模板
字符串提示模板用于格式化单个字符串,通常用于较简单的输入。例如,构建和使用提示模板的一种常见方式如下:

from langchain_core.prompts import PromptTemplate

prompt_template = PromptTemplate.from_template("Tell me a joke about {topic}")

prompt_template.invoke({"topic""cats"})

2、聊天提示模板
聊天提示模板用于格式化消息列表。这些“模板”由多个子模板组成。例如,构建和使用聊天提示模板的一种常见方式如下:

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 变量进行格式化。

3、MessagesPlaceholder
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!")]})

这将生成一个包含两个消息的列表,第一个是系统消息,第二个是我们传入的用户消息。如果我们传入了 5 条消息,它将总共生成 6 条消息(系统消息加上 5 条传入的消息)。这对于将消息列表插入到特定位置非常有用。

另一种不显式使用 MessagesPlaceholder 类来完成相同任务的方法是:

prompt_template = ChatPromptTemplate.from_messages([
    ("system""You are a helpful assistant"),
    ("placeholder""{msgs}"# <-- This is the changed part
])

有关如何使用提示模板的具体信息,请参阅这里的相关操作指南:(https://python.langchain.com/v0.2/docs/how_to/#prompt-templates)

Example selectors(示例选择器)

一种常见的提升性能的提示技术是将示例作为提示的一部分。这被称为少样本提示(few-shot prompting)。这种方法给语言模型提供了如何应对的具体示例。有时,这些示例是硬编码到提示中的,但对于更高级的情况,动态选择示例可能更为方便。示例选择器是负责选择并将示例格式化为提示的类

有关如何使用示例选择器的具体信息,请参阅这里的相关操作指南:(https://python.langchain.com/v0.2/docs/how_to/#example-selectors)。

Output parsers输出解析器

注意
这里的信息指的是将模型的文本输出解析为更结构化表示的解析器。越来越多的模型支持函数(或工具)调用,这会自动处理这个过程。建议使用函数/工具调用,而不是输出解析。有关详细信息,请参阅:https://python.langchain.com/v0.2/docs/concepts/#function-tool-calling

Output parser(输出解析器)负责将模型的输出转换为更适合下游任务的格式。它在使用语言模型生成结构化数据或标准化聊天模型和语言模型的输出时非常有用。

LangChain 支持多种不同类型的输出解析器。以下是 LangChain 支持的输出解析器列表。表格包含以下信息:

  • Name(名称): 输出解析器的名称
  • Supports Streaming(支持流式处理): 输出解析器是否支持流式处理
  • Has Format Instructions(具有格式说明): 输出解析器是否具有格式说明。一般情况下,输出解析器都会提供格式说明,除非(a)所需的模式在提示中没有指定,而是在其他参数中指定(如 OpenAI 函数调用),或(b)当 OutputParser 包装了另一个 OutputParser 时。
  • Calls LLM(调用 LLM): 这个输出解析器本身是否调用了 LLM。通常,只有那些尝试纠正格式错误的输出解析器才会这样做。
  • Input Type:(输入类型): 预期的输入类型。大多数输出解析器处理字符串和消息,但某些解析器(如 OpenAI Functions)需要带有特定参数的消息。
  • Output Type(输出类型): 解析器返回的对象的输出类型。
  • Description(描述): 我们对这个输出解析器的评论以及何时使用它。
名称支持流式处理具有格式说明调用 LLM输入类型输出类型描述
JSON
str | MessageJSON 对象返回指定的 JSON 对象。可以指定 Pydantic 模型,将返回该模型的 JSON 格式。可能是获取结构化数据的最可靠输出解析器,不使用函数调用。
XML
str | Messagedict返回标签的字典。用于需要 XML 输出的情况。与擅长生成 XML 的模型(如 Anthropic 的)一起使用。
CSV
str | MessageList[str]返回逗号分隔值的列表。
OutputFixing

str | Message
包装另一个输出解析器。如果该解析器出错,则将错误信息和不良输出传递给 LLM,请求其修复输出。
RetryWithError

str | Message
包装另一个输出解析器。如果该解析器出错,则将原始输入、不良输出和错误信息传递给 LLM,请求其修复。与 OutputFixingParser 相比,此解析器还发送原始指令。
Pydantic

str | Messagepydantic.BaseModel使用用户定义的 Pydantic 模型,并返回该格式的数据。
YAML

str | Messagepydantic.BaseModel使用用户定义的 Pydantic 模型,并返回该格式的数据。使用 YAML 编码。
PandasDataFrame

str | Messagedict适用于与 pandas DataFrame 进行操作。
Enum

str | MessageEnum将响应解析为提供的枚举值之一。
Datetime

str | Messagedatetime.datetime将响应解析为 datetime 字符串。
Structured

str | MessageDict[str, str]返回结构化信息的输出解析器。由于仅允许字段为字符串,因此其功能不如其他输出解析器强大。适用于处理较小的 LLM。

有关如何使用输出解析器的具体信息,请参阅这里的相关操作指南:(https://python.langchain.com/v0.2/docs/how_to/#output-parsers)。

Chat history(聊天记录)

大多数语言模型应用具有对话界面。对话的一个基本组件是能够引用对话中早期引入的信息。最基本的,对话系统应该能够直接访问一些历史消息。

ChatHistory 的概念指的是 LangChain 中的一个类,它可以用来包装任意chain(链)。这种 ChatHistory 将跟踪底层chain(链)的输入和输出,并将它们作为消息附加到消息数据库中。未来的交互将加载这些消息,并将其作为输入的一部分传递给chain(链)。

Documents(文档)

LangChain 中的 Document 对象包含有关某些数据的信息。它有两个属性:

  • page_content: str — 文档的内容。目前仅为字符串类型。
  • metadata: dict — 与此文档相关的任意元数据。可以跟踪文档 ID、文件名等。

Document loaders(文档加载器)

该类(DocumentLoader)用于加载 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()

有关如何使用文档加载器的具体信息,请参阅这里的相关操作指南:(https://python.langchain.com/v0.2/docs/how_to/#document-loaders)。

Text splitters(文本分割器)

一旦加载了文档,你通常会希望对其进行转换,以更好地适应你的应用程序。最简单的例子是,你可能希望将长文档拆分为适合模型上下文窗口的小块。LangChain 提供了许多内置的文档转换器,使得split(拆分)combine(合并)filter(过滤)以及其他操作变得容易。

处理长文本时,必须将这些文本拆分成小块。尽管这听起来简单,但其中潜藏着许多复杂性。理想情况下,你希望将语义相关的文本片段保持在一起。“语义相关”可能取决于文本的类型。这里展示了几种实现方法。

从高层次来看,文本分割器的工作流程如下:

  1. 将文本拆分成小的、语义上有意义的块(通常是句子)。
  2. 开始将这些小块合并成更大的块,直到达到某个指定的大小(按某种函数测量)。
  3. 一旦达到该大小,将这个块作为独立的文本,然后开始创建一个新的文本块,并与前一个块有一些重叠(以保持块之间的上下文)。

这意味着你可以在两个不同的维度上自定义你的文本分割器:

  • 如何拆分文本
  • 如何测量块的大小

有关如何使用文本分割器的具体信息,请参阅这里的相关操作指南:(https://python.langchain.com/v0.2/docs/how_to/#text-splitters)。

Embedding models(嵌入模型)

嵌入模型创建文本的向量表示。你可以将向量视为一个数字数组,它捕捉了文本的语义意义。通过这种方式表示文本,你可以执行数学操作,从而实现诸如搜索语义最相似的其他文本等功能。这些自然语言搜索能力支撑了许多类型的上下文检索,在这些检索中,我们为语言模型提供了其有效响应查询所需的相关数据。

Embeddings 类是用于与文本嵌入模型交互的类。不同的嵌入模型提供商(如 OpenAI、Cohere、Hugging Face 等)以及本地模型都可以通过这个类来标准化接口。

LangChain 中的基础 Embeddings 类提供了两个方法:

  • 一个用于嵌入文档
  • 另一个用于嵌入查询

前者接受多个文本作为输入,而后者接受单个文本。之所以将这两个方法分开,是因为某些嵌入提供商对文档(用于检索)和查询(搜索查询本身)的嵌入方法有所不同。

有关如何使用嵌入模型的具体信息,请参阅这里的相关操作指南:(https://python.langchain.com/v0.2/docs/how_to/#embedding-models)。

Vector stores(向量存储)

存储搜索非结构化数据的一种常见方式是将数据嵌入并存储生成的嵌入向量,然后在查询时对非结构化查询进行嵌入,并检索与嵌入查询“最相似”的嵌入向量。向量存储负责为你存储嵌入的数据并执行向量搜索。

大多数向量存储还可以存储有关嵌入向量的元数据,并支持在相似度搜索之前对这些元数据进行过滤,这让你可以更好地控制返回的文档。

向量存储可以通过以下方式转换为检索器接口:

vectorstore = MyVectorStore()
retriever = vectorstore.as_retriever()

有关如何使用向量存储的具体信息,请参阅这里的相关操作指南:(https://python.langchain.com/v0.2/docs/how_to/#vector-stores)。

Retrievers检索器

检索器是一个接口,用于根据非结构化查询返回文档。它比向量存储更为通用。检索器不需要具备存储文档的能力,只需返回(或检索)文档。检索器可以从向量存储创建,但也足够广泛,涵盖了像Wikipedia search(维基百科搜索)Amazon Kendra 这样的工具。

检索器接受一个字符串查询作为输入,并返回一个 Document 列表作为输出。

有关如何使用检索器的具体信息,请参阅这里的相关操作指南:(https://python.langchain.com/v0.2/docs/how_to/#retrievers)。

Key-value stores(键值存储)

对于一些技术,如使用多个向量进行索引和检索或缓存嵌入,拥有一种键值(KV)存储形式是有帮助的。

LangChain 包含一个 BaseStore 接口,允许存储任意数据。然而,LangChain 组件中需要 KV 存储的部分接受更具体的 BaseStore[str, bytes] 实例,用于存储二进制数据(称为 ByteStore),并在内部处理数据的编码和解码,以满足其特定需求。

这意味着作为用户,你只需要考虑一种存储类型,而不是针对不同类型数据使用不同的存储。

Interface(接口)

所有 BaseStore 都支持以下接口。请注意,该接口允许一次修改多个键值对:

  • 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]: 生成存储中的所有键, optionally 使用前缀进行过滤。

有关键值存储实现的更多信息,请参阅该部分:(https://python.langchain.com/v0.2/docs/integrations/stores/)。

Tools(工具)

Tools(工具)是设计用来由模型调用的实用程序:它们的输入设计为由模型生成,输出则设计为传递回模型。当你希望模型控制代码的部分或调用外部 API 时,就需要使用工具。

一个工具包括:

  • name:工具的名称
  • description:工具的功能描述
  • JSON schema:定义工具输入的 JSON 模式
  • a 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。有关响应类型的更多信息,请阅读此指南:(https://python.langchain.com/v0.2/docs/concepts/#aimessage)。

一旦选择的工具被调用,结果可以传递回模型,以便模型完成其正在执行的任务。通常有两种不同的方式来调用工具并传递响应:

1、Invoke with just the arguments(仅使用参数进行调用)

当你仅使用参数调用工具时,你会得到工具的原始输出(通常是一个字符串)。这通常看起来像这样:

# 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 的内容:(https://python.langchain.com/v0.2/docs/concepts/#toolmessage))传递。

... # 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
)

2、Invoke with ToolCall(使用 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 包含一个附加信息,你需要让工具返回两个东西。有关定义返回附加信息的工具的更多信息,请参阅这里:(https://python.langchain.com/v0.2/docs/how_to/tool_artifacts/)。

Toolkits(工具包)

工具包是设计为一起使用的工具集合,用于特定任务。它们具有便捷的加载方法。

所有工具包都暴露了一个 get_tools 方法,该方法返回一个工具列表。因此,你可以这样做:

# Initialize a toolkit
toolkit = ExampleTookit(...)

# Get list of tools
tools = toolkit.get_tools()

Agents(代理)

单独的语言模型不能采取行动——它们只能输出文本。LangChain 的一个重要用例是创建代理。代理是系统,利用 LLM 作为推理引擎来确定应该采取哪些行动以及这些行动的输入是什么。这些行动的结果可以反馈到代理中,代理再决定是否需要更多的行动,或者是否可以结束。

LangGraph 是 LangChain 的一个扩展,专门用于创建高度可控和可定制的代理。

LangChain 中有一个过时的代理概念,我们正计划逐步弃用:AgentExecutor。AgentExecutor 实质上是代理的运行时环境。它是一个很好的起点,但随着你开始创建更定制的代理,它的灵活性不足。为了解决这个问题,我们构建了 LangGraph 作为一个灵活且高度可控的运行时环境。

如果你仍在使用 AgentExecutor,请不要担心:我们仍然提供有关如何使用 AgentExecutor 的指南。然而,建议你开始过渡到 LangGraph。为此,我们准备了一个过渡指南。

ReAct agents

构建代理的一个流行架构是 ReAct。ReAct 结合了推理和行动的迭代过程——实际上,“ReAct” 的名称代表了 “Reason” 和 “Act”。

一般流程如下:

  1. 模型会“思考”在响应输入和任何先前观察结果时采取的步骤。
  2. 模型将从可用工具中选择一个行动(或选择回应用户)。
  3. 模型会生成对该工具的参数。
  4. 代理运行时(执行器)会解析选择的工具,并用生成的参数调用它。
  5. 执行器将工具调用的结果作为观察结果返回给模型。

这个过程会重复,直到代理选择回应。

有一些基于提示的通用实现不需要模型特定的功能,但最可靠的实现使用工具调用等功能来可靠地格式化输出并减少变异。

有关更多信息,请参见 LangGraph 文档的操作指南:(https://langchain-ai.github.io/langgraph/)。

Callbacks(回调)

LangChain 提供了一个回调系统,允许你在 LLM 应用程序的各个阶段进行钩取。这对日志记录、监控、流式处理和其他任务非常有用。

你可以通过使用 API 中可用的 callbacks 参数来订阅这些事件。这个参数是一个处理程序对象的列表,这些对象应实现以下方法中的一个或多个,具体如下:

  • on_start: 在操作开始时调用。
  • on_step: 在操作的每一步调用。
  • on_end: 在操作结束时调用。
  • on_error: 在操作出错时调用。

这些回调方法可以用来实现不同的功能,如记录模型的操作、监控模型的性能、流式传输模型的输出等。通过回调系统,你可以更好地控制和观察模型的行为及其输出。

回调事件(Callback Events)

事件事件触发关联方法
Chat model start当聊天模型开始时on_chat_model_start
LLM start当语言模型(LLM)开始时on_llm_start
LLM new token当语言模型或聊天模型发出新 token 时on_llm_new_token
LLM ends当语言模型或聊天模型结束时on_llm_end
LLM errors当语言模型或聊天模型出错时on_llm_error
Chain start当链开始运行时on_chain_start
Chain end当链结束时on_chain_end
Chain error当链出错时on_chain_error
Tool start当工具开始运行时on_tool_start
Tool end当工具结束时on_tool_end
Tool error当工具出错时on_tool_error
Agent action当代理执行动作时on_agent_action
Agent finish当代理结束时on_agent_finish
Retriever start当检索器开始时on_retriever_start
Retriever end当检索器结束时on_retriever_end
Retriever error当检索器出错时on_retriever_error
Text当任意文本运行时on_text
Retry当重试事件运行时on_retry

这些事件和方法允许你在应用程序的各个阶段插入自定义逻辑,以便实现更复杂的功能和更细致的控制。

回调处理程序(Callback Handlers)

回调处理程序可以是同步的或异步的:

  • 同步回调处理程序(Sync Callback Handlers) 实现了 BaseCallbackHandler 接口。
  • 异步回调处理程序(Async Callback Handlers) 实现了 AsyncCallbackHandler 接口。

在运行时,LangChain 配置了适当的回调管理器(如 CallbackManagerAsyncCallbackManager),负责在事件触发时调用每个“注册”回调处理程序上的相应方法。

传递回调(Passing Callbacks)

回调属性可以在 API 中的多数对象(如模型、工具、代理等)中找到,主要有两个不同的位置:

  1. 请求时回调(Request Time Callbacks)

  • 这些回调在请求时作为额外的输入数据传递。
  • 可用于所有标准的 Runnable 对象。
  • 这些回调会被对象的所有子对象继承。例如,可以使用 chain.invoke({"number": 25}, {"callbacks": [handler]}) 来设置回调。
  • 构造函数回调(Constructor Callbacks)

    • 使用构造函数时传递回调,例如 chain = TheNameOfSomeChain(callbacks=[handler])
    • 这些回调仅限于定义它们的对象,不会被对象的任何子对象继承。

    注意:构造函数回调仅限于定义它们的对象,不能被子对象继承。如果你正在创建自定义链或可运行对象,需要记住手动将请求时回调传递给任何子对象。

    Python <= 3.10 的异步处理

    • 对于 RunnableLambdaRunnableGenerator 或其他异步工具,在 Python <= 3.10 中,需要手动将回调传递给子对象,因为 LangChain 在这种情况下无法自动传播回调。
    • 这可能是你无法从自定义可运行对象或工具中看到事件的常见原因。

    有关如何使用回调的详细信息,请参见相关的如何做指南:(https://python.langchain.com/v0.2/docs/how_to/#callbacks)。

    Techniques(技巧)

    流式传输(Streaming)

    在 LLM 调用中,通常会运行较长时间,这在构建需要多个推理步骤的复杂链或代理时尤为明显。幸运的是,LLM 生成输出是逐步进行的,这意味着在最终响应准备好之前,可以展示合理的中间结果。实时消费输出已经成为构建 LLM 应用程序时缓解延迟问题的重要部分,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)

    .astream_events()

    虽然 .stream() 方法直观易用,但它只能返回链的最终生成值。这对于单个 LLM 调用可能足够,但当你构建多个 LLM 调用的复杂链时,可能希望同时使用链的中间值和最终输出。例如,在构建基于文档的聊天应用时,可能需要返回源信息和最终生成内容。

    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() 的更详细信息,请参见此指南:(https://python.langchain.com/v0.2/docs/how_to/streaming/#using-stream-events),其中包括列出可用事件的表格。

    Callbacks(回调)

    在 LangChain 中,从 LLM 流式传输输出的最低级别方法是通过回调系统。你可以将处理 on_llm_new_token 事件的回调处理程序传递给 LangChain 组件。当该组件被调用时,其中包含的任何 LLM 或聊天模型都会使用生成的token调用回调。在回调中,你可以将这些令牌传输到其他目的地,例如 HTTP 响应中。你还可以处理 on_llm_end 事件,以执行任何必要的清理工作。

    回调是 LangChain 中引入的第一种流式传输技术。尽管功能强大且具有广泛适用性,但对开发者来说可能比较复杂。例如:

    • 你需要显式地初始化并管理一些聚合器或其他流以收集结果。
    • 执行顺序并没有明确保证,理论上你可能在 .invoke() 方法完成后运行回调。
    • 提供者通常要求你传递额外参数来流式传输输出,而不是一次性返回所有结果。
    • 你通常会忽略实际模型调用的结果,而更关注回调结果。

    Tokens

    大多数模型提供者使用的输入输出测量单位是令牌(token)。token是语言模型在处理或生成文本时读取和生成的基本单位。token的确切定义可能会根据模型的训练方式有所不同——例如,在英语中,token可以是单个词语,如“apple”,也可以是部分词语,如“app”。

    当你向模型发送提示时,提示中的单词和字符会通过分词器(tokenizer)编码为令牌。然后,模型会流式返回生成的输出token,这些令牌由分词器解码为人类可读的文本。以下示例展示了 OpenAI 模型如何将“LangChain is cool!”进行分词。

    你可以看到,这段文本被分成了5个不同的tokens,并且这些tokens之间的边界并不完全与单词的边界相同。

    语言模型使用tokens而不是“字符”这样更直观的单位,原因与它们处理和理解文本的方式有关。从高层次上讲,语言模型基于初始输入和之前生成的内容,迭代地预测下一个生成的输出。使用tokens进行训练使得语言模型能够处理携带意义的语言单位(如单词或子词),而不是单个字符,这使得模型更容易学习和理解语言的结构,包括语法和上下文。此外,使用tokens还可以提高效率,因为与字符级处理相比,模型处理的文本单位更少。

    Function/tool calling(函数/工具调用)

    提示
    我们将工具调用和函数调用这两个术语交替使用。尽管函数调用有时特指单个函数的调用,我们将所有模型视为在每条消息中可以返回多个工具或函数调用。

    工具调用允许聊天模型通过生成符合用户定义的架构的输出来响应给定的提示。

    虽然名称暗示模型正在执行某些操作,但实际上并非如此!模型只是生成工具的参数,是否真正运行该工具(或不运行)由用户决定。一个常见的例子是在某些情况下你可能不希望用生成的参数来调用函数,比如你想从非结构化文本中提取符合某种架构的结构化输出。你可以为模型提供一个参数符合所需架构的“提取”工具,然后将生成的输出视为你的最终结果。

    工具调用并不是通用的功能,但许多流行的LLM提供商支持此功能,包括Anthropic、Cohere、Google、Mistral、OpenAI,甚至通过Ollama支持本地运行的模型。

    LangChain提供了一个跨不同模型一致的标准化工具调用接口。

    该标准接口包括:

    • **ChatModel.bind_tools()**:用于指定模型可以调用哪些工具的方法。此方法接受LangChain工具以及Pydantic对象。
    • AIMessage.tool_calls:模型返回的AIMessage上的一个属性,用于访问模型请求的工具调用。

    工具使用

    在模型调用工具后,您可以通过调用工具来使用它,然后将参数传递回模型。LangChain 提供了 Tool 抽象以帮助您处理此过程。

    一般流程如下:

    1. 使用聊天模型根据查询生成工具调用。
    2. 使用生成的工具调用作为参数调用相应的工具。
    3. 将工具调用的结果格式化为ToolMessages

    这就是工具调用代理执行任务和回答查询的方式。

    Structured output(结构化输出)

    LLMs 能够生成任意文本。这使得模型能够对各种输入做出适当的响应,但在某些用例中,约束 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)

    我们推荐在处理结构化输出时,从以下方法开始:

    • 它在后台使用了其他模型特定的功能,无需导入输出解析器。
    • 对于支持工具调用的模型,无需特别的提示。
    • 如果支持多种底层技术,可以通过传递一个方法参数来切换使用哪一种技术。

    Raw prompting(原始提示)

    获取模型生成结构化输出的最直观方法是直接请求。在查询之外,您可以提供描述所需输出类型的说明,然后使用输出解析器将模型生成的原始消息或字符串输出转换为更易于操作的格式。

    原始提示的最大优点是其灵活性:

    • 原始提示不需要任何特殊的模型功能,只需足够的推理能力来理解传递的模式。
    • 您可以提示任何您希望的格式,而不仅仅是 JSON。如果您使用的模型更多地训练于某种类型的数据(如 XML 或 YAML),这可能很有用。

    但也存在一些缺点:

    • LLM 是非确定性的,提示 LLM 一致地以完全正确的格式输出数据以便平滑解析可能意外地困难且模型特定。
    • 个别模型根据其训练数据具有独特的表现,优化提示可能非常困难。有些模型可能更擅长解释 JSON 模式,其他可能在 TypeScript 定义方面表现更好,还有一些可能更喜欢 XML。
    • 虽然模型提供商提供的功能可能会提高可靠性,但无论您选择哪种方法,提示技术仍然对于调整结果非常重要。

    JSON mode

    一些模型,如 MistralOpenAITogether AIOllama,支持一种称为 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?'}

    Tool calling(工具调用)

    对于支持工具调用的模型,它可以非常方便地生成结构化输出。它通过内置的模型功能消除了如何最佳提示模式的猜测。

    它的工作方式是首先通过 .bind_tools() 方法将所需的模式直接绑定到聊天模型或通过 LangChain 工具。然后,模型将生成一个包含 tool_calls 字段的 AIMessage,其中包含与所需格式匹配的参数。

    以下是将工具绑定到 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() 方法在模型支持时使用的默认技术。

    Few-shot prompting(少量示例提示)

    一种提高模型性能的有效方法是:向模型提供示例,以展示你希望它执行的任务。将示例输入和预期输出添加到模型提示中的技术称为“少量示例提示”。这种技术基于《语言模型是少量学习者》(Language Models are Few-Shot Learners)论文。进行少量示例提示时,需要考虑以下几个方面:

    1. 生成示例

    生成有效示例的第一步也是最重要的步骤。好的示例应该在运行时相关、清晰、信息丰富,并提供模型之前未曾了解的信息。

    生成示例的基本方法有:

    • 手动生成:由人类生成他们认为有用的示例。
    • 更好的模型:使用更高级(通常更昂贵/更慢)模型的响应作为较差(通常更便宜/更快)模型的示例。
    • 用户反馈:用户(或标注者)对与应用程序的交互留下反馈,并根据反馈生成示例(例如,所有具有正面反馈的交互可以转化为示例)。
    • LLM反馈:与用户反馈类似,但通过让模型自我评估来自动化这个过程。

    哪种方法最好取决于你的任务。对于需要深刻理解少数核心原则的任务,手动制作一些高质量示例可能更有价值。对于正确行为的范围较广且更复杂的任务,可以通过自动化生成大量示例,以提高在运行时获取相关示例的可能性。

    单轮与多轮示例

    生成示例时,另一个要考虑的维度是示例实际展示的内容。

    最简单的示例只包含用户输入和预期的模型输出,这些是单轮示例。

    另一种复杂的示例是整个对话示例,通常模型最初的响应不正确,然后用户告诉模型如何纠正答案。这被称为多轮示例。多轮示例对于更复杂的任务很有用,因为它可以展示常见错误,并详细说明错误的原因和应做的正确处理方法。

    1. 示例数量

    一旦拥有示例数据集,我们需要考虑每个提示中应包含多少示例。关键权衡在于更多示例通常可以提高性能,但较大的提示会增加成本和延迟。超过某个阈值后,示例过多可能会开始使模型困惑。找到合适的示例数量高度依赖于模型、任务、示例质量以及你的成本和延迟约束。一般来说,模型越好,它需要的示例就越少,并且添加更多示例的回报递减会更快。但是,可靠回答这个问题的最好方法是通过实验不同数量的示例来测试。

    1. 选择示例

    假设我们不是将整个示例数据集添加到每个提示中,我们需要有选择示例的方式。我们可以:

    • 随机选择
    • 基于(语义或关键词)相似性选择
    • 基于其他约束,如token大小

    LangChain 提供了多个 ExampleSelectors,方便使用这些技术。

    通常,基于语义相似性选择会导致最好的模型性能。但这有多重要仍取决于模型和任务,是值得实验的。

    1. 格式化示例

    目前最先进的模型多为聊天模型,因此我们将重点讨论如何为这些模型格式化示例。我们的基本选项是将示例插入:

    • 系统提示中作为字符串
    • 作为单独的消息

    如果我们将示例插入系统提示中作为字符串,我们需要确保模型清楚每个示例的开始位置以及哪些部分是输入,哪些是输出。不同的模型对不同的语法响应更好,例如 ChatMLXMLTypeScript 等。

    如果我们将示例作为消息插入,其中每个示例表示为一系列 HumanAI 消息,我们可能还需要为这些消息分配名称,如“example_user”“example_assistant”,以明确这些消息对应于不同的角色,而不是最新的输入消息。

    格式化工具调用示例

    在格式化工具调用示例时,可能会遇到一些挑战,因为不同的模型对生成工具调用的消息序列有不同的约束。

    • 一些模型要求任何包含工具调用的 AIMessage 紧跟着 ToolMessages 。
    • 一些模型还要求任何 ToolMessages 紧跟着 AIMessage 之后,才能出现下一个 HumanMessage。
    • 一些模型要求如果聊天历史中有工具调用/ToolMessages,必须将工具传递给模型。

    这些要求是模型特定的,应根据你使用的模型进行检查。如果你的模型要求在工具调用后添加 ToolMessages 和/或在 ToolMessages 后添加 AIMessages,并且你的示例仅包含预期的工具调用而没有实际的工具输出,你可以尝试在每个示例的末尾添加带有通用内容的虚拟 ToolMessages/AIMessages 以满足 API 约束。在这种情况下,尤其值得实验将示例插入为字符串还是消息,因为虚拟消息可能会对某些模型产生不利影响。

    Retrieval(检索)

    语言模型(LLMs)在一个大型但固定的数据集上进行训练,这限制了它们对私人或最新信息的推理能力。通过特定事实的微调是缓解这一问题的一种方法,但它通常不适合事实回忆,并且可能成本较高。检索是为语言模型提供相关信息的过程,以改进其对给定输入的响应。检索增强生成(RAG)是通过使用检索到的信息来使语言模型的生成(输出)更具实际依据的过程

    RAG的效果取决于检索文档的相关性和质量。幸运的是,出现了一系列新兴技术,可以用于设计和改进RAG系统。我们已经专注于对这些技术进行分类和总结(见下图),并将在以下部分分享一些高级战略指导。你可以并且应该尝试将不同的技术组合使用。

    1、Query Translation

    首先,考虑一下用户输入到你的RAG系统中的内容。理想情况下,RAG系统能够处理各种输入,从措辞不当的问题到复杂的多部分查询。使用语言模型来审查和(可选地)修改输入是查询翻译的核心思想。这作为一个通用的缓冲区,用于优化原始用户输入以适应你的检索系统。例如,这可以简单到提取关键词,也可以复杂到为复杂查询生成多个子问题。

    名称何时使用描述
    多查询当你需要涵盖问题的多个视角时。从多个视角重写用户问题,为每个重写的问题检索文档,返回所有查询的唯一文档。
    分解当一个问题可以分解为更小的子问题时。将问题分解为一组子问题/问题,可以顺序解决(使用第一个问题的答案 + 检索来回答第二个问题)或并行解决(将每个答案汇总成最终答案)。
    回退当需要更高层次的概念理解时。首先让LLM提出一个关于更高层次概念或原则的通用回退问题,并检索相关事实。使用这些基础信息来帮助回答用户问题。
    HyDE如果你在使用原始用户输入检索相关文档时遇到挑战。使用LLM将问题转换为假设文档,以回答问题。使用嵌入的假设文档来检索真实文档,假设文档间相似性搜索可以产生更相关的匹配。

    2、Routing(路由)

    其次,考虑您RAG系统可用的数据源。您可能需要查询多个数据库或同时查询结构化和非结构化数据源。使用LLM来审查输入并将其路由到适当的数据源是一种简单而有效的跨源查询方法。

    名称使用时机描述
    Logical routing当您可以使用规则提示LLM来决定路由输入的去处时。逻辑路由可以利用LLM来推理查询并选择最合适的数据存储。
    Semantic routing当语义相似性是一种有效的确定输入路由位置的方法时。语义路由会将查询和通常的一组提示嵌入,然后根据相似性选择合适的提示。

    3、Query Construction(查询构建)

    第三步,考虑你的数据源是否需要特定的查询格式。许多结构化数据库使用SQL,而向量存储通常有特定的语法来应用关键词过滤器。使用LLM将自然语言查询转换为查询语法是一种流行且强大的方法。特别是,文本到SQL、文本到Cypher和元数据过滤器的查询分析分别对结构化数据库、图数据库和向量数据库的交互非常有用。

    名称使用时机描述
    文本到SQL当用户提问需要从关系数据库中获取信息,并且该数据库可以通过SQL访问时。使用LLM将用户输入转换为SQL查询。
    文本到Cypher当用户提问需要从图数据库中获取信息,并且该数据库可以通过Cypher访问时。使用LLM将用户输入转换为Cypher查询。
    自查询当用户提问时,更适合通过根据元数据获取文档,而不是与文本相似性进行检索。使用LLM将用户输入转换为两部分内容:(1)用于语义检索的字符串,(2)与之配套的元数据过滤器。这种方法适用于问题涉及文档的元数据(而不是内容本身)。

    4、Indexing(索引)

    第四,考虑文档索引的设计。一个简单而有效的想法是将用于检索的文档与用于生成的文档分开。索引通常使用嵌入型和向量存储,将文档中的语义信息压缩为固定大小的向量。

    许多RAG方法关注将文档拆分成块,并根据与输入问题的相似性检索一些块,以供LLM使用。然而,块的大小和数量可能很难设置,如果它们未能提供LLM回答问题所需的完整上下文,则会影响结果。此外,LLMs越来越能够处理数百万个tokens。

    有两种方法可以解决这种紧张关系:

    • (1)多向量检索器使用LLM将文档转换为适合索引的形式(例如,通常转换为摘要),但将完整文档返回给LLM进行生成。
    • (2)ParentDocument检索器嵌入文档块,但也返回完整文档。这个想法是兼顾两者的优势:使用简洁的表示(摘要或块)进行检索,但使用完整文档进行回答生成。
    名称索引类型使用LLM适用场景描述
    Vector store向量存储如果你刚开始入门并寻找简单快捷的方法。这是最简单的方法,最容易上手。涉及为每段文本创建嵌入。
    ParentDocument向量存储 + 文档存储如果你的页面包含很多较小的、独立的信息块,它们最好单独索引,但一起检索效果最佳。涉及为每个文档索引多个块。然后在嵌入空间中找到最相似的块,但检索整个父文档并返回(而不是单个块)。
    Multi Vector向量存储 + 文档存储有时在索引时如果你能从文档中提取出比文本本身更相关的信息来索引。涉及为每个文档创建多个向量。每个向量可以以多种方式创建,例如文本摘要或假设问题。
    Time-Weighted Vector store向量存储如果你的文档有时间戳,并且你希望检索最新的文档。根据语义相似性(如正常的向量检索)和时效性(查看索引文档的时间戳)的组合来获取文档。

    5、提高相似性搜索的质量

    第五,考虑如何提高相似性搜索的质量。嵌入模型将文本压缩为固定长度的向量表示,捕捉文档的语义内容。这种压缩对搜索和检索非常有用,但也对单个向量表示施加了很大的压力,要求它能够捕捉文档的语义细微差别和详细信息。在某些情况下,不相关或冗余的内容可能会削弱嵌入的语义有效性。

    ColBERT 是一种有趣的方法,通过更高粒度的嵌入来解决这个问题:

    • (1) 为文档和查询中的每个 token 生成一个受上下文影响的嵌入;
    • (2) 计算每个查询 token 与所有文档 token 之间的相似度;
    • (3) 取最大值;
    • (4) 对所有查询 token 执行此操作;
    • (5) 将所有查询 token 的最大分数(步骤 3 中)相加,得到查询文档的相似度分数。

    这种基于 token 的评分方法可以产生强大的效果。

    要提高检索质量,还有一些额外的技巧可以使用。嵌入模型在捕捉语义信息方面表现出色,但在处理基于关键词的查询时可能会遇到挑战。为此,许多向量存储提供了内置的混合搜索功能,它将关键词匹配与语义相似度结合起来,从而融合了两种方法的优势。此外,许多向量存储还支持最大边际相关性(Maximal Marginal Relevance,MMR),这一方法旨在多样化搜索结果,避免返回相似和冗余的文档,从而提高检索结果的质量和覆盖面。 以下是改进检索质量的技术总结:

    名称适用场景描述
    ColBERT需要更高粒度的嵌入时ColBERT 使用上下文影响的嵌入来对文档和查询中的每个词元进行嵌入,以获得更精细的查询-文档相似度得分。
    Hybrid search需要结合关键词搜索和语义相似性时混合搜索结合了关键词和语义相似性,融合了这两种方法的优点。
    Maximal Marginal Relevance (MMR)需要多样化搜索结果时MMR 试图多样化搜索结果,避免返回相似和冗余的文档。

    Post-processing

    6、对文档进行过滤或排序

    第六点,考虑对检索到的文档进行过滤或排序。这在您合并多个来源返回的文档时非常有用,因为它可以降低不相关文档的排名,或者压缩相似的文档。

    以下是关于Post-processing技术的总结:

    名称索引类型是否使用 LLM适用场景描述
    上下文压缩任意类型有时如果您发现检索到的文档包含过多无关信息,并且会干扰 LLM。这一步是在另一个检索器的基础上进行后处理,仅提取检索到的文档中最相关的信息。可以通过嵌入或 LLM 完成。
    集成任意类型如果您有多种检索方法,并且想尝试将它们结合起来。从多个检索器获取文档,然后将它们组合在一起。
    重排序任意类型如果您希望基于相关性对检索到的文档进行排序,尤其是当您希望结合多种检索方法的结果时。根据查询和文档列表,重排序器按文档与查询的语义相关性从高到低进行排序。

    7、Generation

    最后,考虑如何在您的RAG系统中构建自我纠正机制。RAG系统可能会因为低质量的检索(例如,用户问题超出了索引的范围)和/或生成中的幻觉而出现问题。一个简单的检索-生成管道无法检测或自我纠正这些类型的错误。在代码生成的背景下引入了“流工程”概念:通过单元测试迭代地构建代码问题的答案,以检查和自我纠正错误。多个研究将这一概念应用于RAG系统,如Self-RAG和Corrective-RAG。在这两种情况下,都会在RAG答案生成流程中进行文档相关性、幻觉和/或答案质量的检查。

    我们发现图表是可靠表达逻辑流程的好方法,并使用LangGraph实现了几篇论文中的一些想法,如下图所示(红色 - 路由,蓝色 - 回退,绿色 - 自我纠正):

    1. 路由:Adaptive RAG(论文)。将问题路由到不同的检索方法,如上所述。
    2. 回退:Corrective RAG(论文)。如果文档与查询不相关,则回退到网络搜索。
    3. 自我纠正:Self-RAG(论文)。修正有幻觉或未能解决问题的答案。
    名称使用时机描述
    Self-RAG当需要修正有幻觉或无关内容的答案时。Self-RAG在RAG答案生成流程中进行文档相关性、幻觉和答案质量的检查,通过迭代地构建答案并自我纠正错误来提升质量。
    Corrective-RAG当需要为低相关性文档提供回退机制时。Corrective-RAG包括回退机制(例如,回退到网络搜索),如果检索的文档与查询不相关,可以确保更高质量和更相关的检索结果。

    Text splitting(文本切分器)

    LangChain 提供了多种不同类型的文本切分器,这些切分器都在 langchain-text-splitters 包中。

    表格列说明:

    • Name(名称):文本切分器的名称
    • Classes(类):实现该文本切分器的类
    • Splits On(切分依据):该文本切分器依据什么来切分文本
    • Adds Metadata(是否添加元数据):该文本切分器是否会添加关于每个文本块来源的元数据
    • Description(描述):对切分器的描述,包括何时使用的推荐建议

    文本切分器

    Name(名称)Classes(类)Splits On(切分依据)Adds Metadata(是否添加元数据)Description(描述)
    RecursiveRecursiveCharacterTextSplitter, RecursiveJsonSplitter一系列用户定义的字符递归地切分文本。该方法尝试将相关的文本片段保持在一起。建议作为开始切分文本的方法。
    HTMLHTMLHeaderTextSplitter, HTMLSectionSplitterHTML 特定字符基于 HTML 特定字符切分文本。特别地,这会添加有关文本块来源的相关信息(基于 HTML)。
    MarkdownMarkdownHeaderTextSplitterMarkdown 特定字符基于 Markdown 特定字符切分文本。特别地,这会添加有关文本块来源的相关信息(基于 Markdown)。
    Code多种语言类编程语言特定字符(Python, JS)基于编程语言特定字符切分文本。支持 15 种不同语言的选择。
    Token多种类令牌(Tokens)基于令牌切分文本。存在几种不同的令牌度量方式。
    CharacterCharacterTextSplitter用户定义的字符基于用户定义的字符切分文本。方法较为简单。
    Semantic Chunker (Experimental)SemanticChunker句子首先按句子切分。然后如果句子之间语义足够相似,则将相邻的句子组合在一起。由 Greg Kamradt 提出。
    Integration: AI21 SemanticAI21SemanticTextSplitter
    识别形成连贯文本的不同主题并沿着这些主题切分文本。

    Evaluation(评估)

    Evaluation是评估您的 LLM 驱动的应用程序的性能和有效性的过程。它涉及将模型的响应与一组预定义的标准或基准进行比较,以确保它满足所需的质量标准并实现预期的目的。这个过程对于构建可靠的应用程序至关重要。

    LangSmith 通过以下几种方式帮助这个过程:

    • 数据集创建与策划:通过其跟踪和注释功能,使创建和策划数据集变得更容易。
    • 评估框架:提供了一个评估框架,帮助您定义指标并将应用程序在您的数据集上进行测试。
    • 结果追踪:允许您跟踪结果并自动运行评估器,您可以按照时间表运行评估器或将其作为 CI/代码的一部分。

    Tracing

    Tracing 是一种跟踪应用程序从输入到输出的每个步骤的方式。一个跟踪包含了多个步骤,这些步骤被称为“运行”(runs)。这些运行可以是模型调用、检索器调用、工具调用或子链的执行。Tracing 使您能够观察到链和代理的内部运行,帮助诊断问题


53AI,企业落地应用大模型首选服务商

产品:大模型应用平台+智能体定制开发+落地咨询服务

承诺:先做场景POC验证,看到效果再签署服务协议。零风险落地应用大模型,已交付160+中大型企业

联系我们

售前咨询
186 6662 7370
预约演示
185 8882 0121

微信扫码

与创始人交个朋友

回到顶部

 
扫码咨询