Skip to main content

概述

由大语言模型(LLM)支持的最强大的应用之一是复杂的问答(Q&A)聊天机器人。这些应用程序可以回答关于特定源信息的问题。这些应用程序使用一种称为检索增强生成(Retrieval Augmented Generation,或 RAG)的技术。 本教程将展示如何针对非结构化文本数据源构建一个简单的问答应用程序。我们将演示:
  1. 一个执行搜索的 RAG 代理,使用简单的工具。这是一个良好的通用实现。
  2. 一个两步 RAG ,每个查询仅使用一次 LLM 调用。这是一种快速且有效的方法,适用于简单查询。

概念

我们将涵盖以下概念:
  • 索引:从源摄取数据并对其进行索引的管道。这通常发生在单独的过程中。
  • 检索和生成:实际的 RAG 过程,它在运行时获取用户查询并从索引中检索相关数据,然后将其传递给模型。
一旦我们索引了数据,我们将使用 代理 作为我们的编排框架来实现检索和生成步骤。
本教程的索引部分将主要遵循 语义搜索教程如果您的数据已经可供搜索(即您有一个执行搜索的函数),或者您熟悉该教程的内容,请随时跳到 检索和生成 部分。

预览

在本指南中,我们将构建一个回答网站内容问题的应用程序。我们将使用的特定网站是 Lilian Weng 的 LLM Powered Autonomous Agents 博客文章,它允许我们询问有关帖子内容的各种问题。 我们可以创建一个简单的索引管道和 RAG 链来在 ~40 行代码内完成此操作。完整代码片段如下所示:
查看 LangSmith 跟踪

设置

安装

本教程需要以下 langchain 依赖项:
更多详细信息,请参阅我们的 安装指南

LangSmith

使用 LangChain 构建的许多应用程序都将包含多个步骤和多次 LLM 调用。随着这些应用程序变得越来越复杂,能够检查您的链或代理内部究竟发生了什么变得至关重要。最好的方法是使用 LangSmith 在上述链接注册后,请确保设置环境变量以开始记录跟踪:
或者,在 Python 中设置它们:

组件

我们需要从 LangChain 的集成套件中选择三个组件。 选择聊天模型:
👉 阅读 OpenAI 聊天模型集成文档
选择嵌入模型:
选择向量存储:

1. 索引

本节是 语义搜索教程 内容的缩写版本。如果您的数据已经索引并可用于搜索(即您有一个执行搜索的函数),或者如果您熟悉 文档加载器嵌入向量存储,请随时跳到下一节 检索和生成
索引通常按以下方式工作:
  1. 加载:首先我们需要加载数据。这是通过 文档加载器 完成的。
  2. 拆分文本拆分器 将大型 Documents 拆分为较小的块。这对于索引数据和将其传递给模型都有用,因为大块更难搜索且无法适应模型的有限上下文窗口。
  3. 存储:我们需要一个地方来存储和索引我们的拆分,以便以后可以搜索它们。这通常使用 向量存储嵌入 模型来完成。
index_diagram

加载文档

我们首先需要加载博客文章内容。我们可以为此使用 DocumentLoaders,它们是加载源数据并返回 Document 对象列表的对象。 在这种情况下,我们将使用 WebBaseLoader,它使用 urllib 从 Web URL 加载 HTML 并使用 BeautifulSoup 将其解析为文本。我们可以通过将参数传递给 BeautifulSoup 解析器 via bs_kwargs 来自定义 HTML -> 文本解析(参见 BeautifulSoup 文档)。在这种情况下,只有类为“post-content”、“post-title”或“post-header”的 HTML 标签是相关的,因此我们将删除所有其他标签。
深入了解更多 DocumentLoader:从源加载数据作为 Documents 列表的对象。
  • 集成:160+ 种集成可供选择。
  • BaseLoader:基础接口的 API 参考。

拆分文档

我们加载的文档超过 42k 个字符,这对于许多模型的上下文窗口来说太长了。即使对于那些可以将整篇文章放入其上下文窗口的模型,模型也可能难以在非常长的输入中找到信息。 为了处理这个问题,我们将把 Document 拆分为用于嵌入和向量存储的块。这将有助于我们在运行时仅检索博客文章中最相关的部分。 语义搜索教程 一样,我们使用 RecursiveCharacterTextSplitter,它将使用常见的分隔符(如新行)递归地拆分文档,直到每个块大小合适。这是用于通用文本用例的推荐文本拆分器。
深入了解更多 TextSplitter:将 Document 对象列表拆分为较小块以供存储和检索的对象。

存储文档

现在我们需要索引我们的 66 个文本块,以便在运行时搜索它们。遵循 语义搜索教程,我们的方法是将每个文档拆分的内容 嵌入 并将这些嵌入插入到 向量存储 中。给定输入查询,我们可以随后使用向量搜索检索相关文档。 我们可以使用在 教程开始时 选择的向量存储和嵌入模型,在一个命令中嵌入并存储所有文档拆分。
深入了解更多 Embeddings:围绕文本嵌入模型的包装器,用于将文本转换为嵌入。
  • 集成:30+ 种集成可供选择。
  • 接口: 基础接口的 API 参考。
VectorStore:围绕向量数据库的包装器,用于存储和查询嵌入。
  • 集成:40+ 种集成可供选择。
  • 接口: 基础接口的 API 参考。
至此完成了管道的 索引 部分。此时,我们拥有一个可查询的向量存储,其中包含我们博客文章的分块内容。给定用户问题,我们应该能够返回回答该问题的博客文章片段。

2. 检索和生成

RAG 应用程序通常按以下方式工作:
  1. 检索:给定用户输入,使用 检索器 从存储中检索相关拆分。
  2. 生成模型 使用包含问题和检索数据的提示生成答案。
retrieval_diagram 现在让我们编写实际的应用程序逻辑。我们要创建一个简单的应用程序,它接受用户问题,搜索与该问题相关的文档,将检索到的文档和初始问题传递给模型,并返回答案。 我们将演示:
  1. 一个执行搜索的 RAG 代理,使用简单的工具。这是一个良好的通用实现。
  2. 一个两步 RAG ,每个查询仅使用一次 LLM 调用。这是一种快速且有效的方法,适用于简单查询。

RAG 代理

RAG 应用程序的一种形式是一个带有检索信息工具的简单 代理。我们可以通过实现一个包装我们的向量存储的 工具 来组装一个最小的 RAG 代理:
在这里,我们使用 工具装饰器 配置工具,将原始文档作为 工件 附加到每个 ToolMessage。这将使我们在应用程序中访问文档元数据,而无需发送字符串化表示给模型。
检索工具不限于单个字符串 query 参数,如上例所示。您可以通过添加参数强制 LLM 指定额外的搜索参数——例如,类别:
有了我们的工具,我们可以构建代理:
让我们测试一下。我们构造了一个通常需要迭代检索序列才能回答的问题:
请注意代理:
  1. 生成查询以搜索任务分解的标准方法;
  2. 收到答案后,生成第二个查询以搜索其常见扩展;
  3. 收到所有必要上下文后,回答问题。
我们可以在 LangSmith 跟踪 中看到完整的步骤序列,以及延迟和其他元数据。
您可以直接使用 LangGraph 框架添加更深层次的控制和自定义——例如,您可以添加步骤来评估文档相关性并重写搜索查询。查看 LangGraph 的 Agentic RAG 教程 以了解更高级的公式。

RAG 链

在上述 代理式 RAG 公式中,我们允许 LLM 自行决定生成 工具调用 以帮助回答用户查询。这是一个良好的通用解决方案,但也带来了一些权衡: 另一种常见的方法是两步链,我们总是运行搜索(可能使用原始用户查询)并将结果作为上下文中包含在单个 LLM 查询中。这导致每个查询只有一次推理调用,以牺牲灵活性为代价换取降低的延迟。 在这种方法中,我们不再循环调用模型,而是进行单次传递。 我们可以通过从代理中移除工具并将检索步骤合并到自定义提示中来实施此链:
让我们试试这个:
LangSmith 跟踪 中,我们可以看到检索到的上下文被合并到模型提示中。 这是一种快速且有效的方法,适用于受约束设置中的简单查询,当我们通常希望通过语义搜索运行用户查询以获取更多上下文时。
上述 RAG 链将检索到的上下文合并到该运行的单个系统消息中。代理式 RAG 公式一样,我们有时希望在应用程序状态中包含原始源文档以访问文档元数据。我们可以通过以下方式对两步链情况执行此操作:
  1. 添加一个键来存储检索到的文档
  2. 通过 中间件钩子 添加一个新节点,例如 before_model 来填充该键(以及注入上下文)。

安全性:间接提示注入

RAG 应用程序容易受到 间接提示注入 的影响。检索到的文档可能包含类似于指令的文本(例如,“以 JSON 格式响应”或“忽略之前的指令”)。由于检索到的上下文与您的系统提示共享相同的上下文窗口,模型可能会无意中遵循嵌入在数据中的指令,而不是您预期的提示。例如,本教程中索引的博客文章包含描述 Auto-GPT JSON 响应格式的文本。如果用户查询检索到该块,模型可能会输出 JSON 而不是自然语言答案。
为了缓解这种情况:
  1. 使用防御性提示:明确指示模型将检索到的上下文仅视为数据,并忽略其中的任何指令。本教程中的提示包括此类指令。
  2. 用定界符包装上下文:使用清晰的结构标记(例如,XML 标签 <context>...</context>)将检索到的数据与指令分开,使模型更容易区分它们。
  3. 验证响应:检查模型的输出是否符合预期格式(例如,纯文本),并以优雅的方式处理意外格式。
没有缓解措施是万无一失的——这是当前 LLM 架构的固有局限性,其中指令和数据共享相同的上下文窗口。有关此主题的更多信息,请参阅关于 提示注入 的研究。

下一步

既然我们已经通过 create_agent 实现了简单的 RAG 应用程序,我们可以轻松添加新功能并深入探索: