Skip to main content

概述

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

概念

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

预览

在本指南中,我们将构建一个回答网站内容问题的应用程序。我们将使用的特定网站是 Lilian Weng 的 LLM Powered Autonomous Agents 博客文章,这允许我们询问关于该文章内容的问题。 我们可以在大约 40 行代码中创建一个简单的索引管道和 RAG 链。请参见下面的完整代码片段:
查看 LangSmith 追踪

设置

安装

本教程需要以下 langchain 依赖:
有关更多详情,请参阅我们的安装指南

LangSmith

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

组件

我们需要从 LangChain 的集成套件中选择三个组件。 选择一个聊天模型:
👉 Read the OpenAI chat model integration docs
选择一个向量嵌入模型:
选择一个向量存储:

1. 索引

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

加载文档

我们需要首先加载博客文章内容。我们可以使用文档加载器来完成此操作,它们是从来源加载数据并返回 Document 对象列表的对象。 在本例中,我们将使用 WebBaseLoader,它使用 urllib 从 Web URL 加载 HTML,并使用 BeautifulSoup 将其解析为文本。我们可以通过 bs_kwargs 传递参数给 BeautifulSoup 解析器来自定义 HTML -> 文本解析(参见 BeautifulSoup 文档)。在本例中,只有 class 为 “post-content”、“post-title” 或 “post-header” 的 HTML 标签是相关的,因此我们将移除所有其他标签。

分割文档

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

存储文档

现在我们需要索引我们的 66 个文本块,以便在运行时可以搜索它们。按照语义搜索教程,我们的方法是嵌入每个文档分割的内容并将这些向量嵌入插入到向量存储中。给定一个输入查询,我们可以使用向量搜索检索相关文档。 我们可以使用在教程开始时选择的向量存储和向量嵌入模型,通过一个命令嵌入和存储所有文档分割。
这完成了管道的索引部分。此时,我们有一个可查询的向量存储,其中包含我们博客文章的分块内容。给定一个用户问题,我们理想情况下应该能够返回回答该问题的博客文章片段。

2. 检索和生成

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

RAG 智能体

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

RAG 链

在上面的智能体式 RAG 中,我们允许 LLM 自行决定是否生成工具调用来帮助回答用户查询。这是一个好的通用解决方案,但有一些权衡: 另一种常见方法是两步链,其中我们始终运行搜索(可能使用原始用户查询)并将结果作为上下文纳入单个 LLM 查询。这导致每次查询只有一次推理调用,以灵活性为代价换取更低的延迟。 在这种方法中,我们不再循环调用模型,而是进行单次传递。 我们可以通过从智能体中移除工具并将检索步骤合并到自定义提示词中来实现此链:
LangSmith 追踪中我们可以看到检索到的上下文被纳入了模型提示词。 这是在受限场景下处理简单查询的快速有效方法,我们通常确实希望通过语义搜索运行用户查询以获取额外上下文。

安全性:间接提示词注入

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

后续步骤

现在我们已经通过 create_agent 实现了一个简单的 RAG 应用程序,我们可以轻松地引入新功能并深入探索: