Skip to main content

概述

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

概念

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

预览

在本指南中,我们将构建一个回答网站内容问题的应用。我们将使用的具体网站是 Lilian Weng 的 LLM Powered Autonomous Agents 博客文章,这使我们能够询问关于文章内容的问题。 我们可以用约 40 行代码创建一个简单的索引管道和 RAG 链。请参见下方的完整代码片段:

设置

安装

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

LangSmith

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

组件

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

1. 索引

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

加载文档

我们首先需要加载博客文章内容。我们可以使用 DocumentLoaders 来实现,它们是从数据源加载数据并返回 Document 对象列表的对象。
深入了解 DocumentLoader:从数据源加载数据并返回 Documents 列表的对象。

分割文档

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

存储文档

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

2. 检索和生成

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

RAG 智能体

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

RAG 链

在上述智能体 RAG 形式中,我们允许 LLM 自行判断生成工具调用来帮助回答用户查询。这是一个良好的通用解决方案,但也有一些权衡: 另一种常见方法是两步链,在这种方法中我们总是运行搜索(可能使用原始用户查询)并将结果作为单次 LLM 查询的上下文。这导致每次查询只有一次推理调用,以灵活性换取更低的延迟。 在这种方法中,我们不再循环调用模型,而是进行单次传递。 我们可以通过从智能体中移除工具,并将检索步骤整合到自定义提示中来实现这个链:
让我们试试:
LangSmith 追踪中,我们可以看到检索到的上下文被整合到模型提示中。 在受限的场景中,当我们通常确实想要对用户查询进行语义搜索以获取额外上下文时,这是一种快速有效的简单查询方法。
上述 RAG 链将检索到的上下文整合到该次运行的单个系统消息中。智能体 RAG 形式一样,我们有时希望在应用状态中包含原始源文档以访问文档元数据。我们可以通过以下方式为两步链实现这一点:
  1. 在状态中添加一个键来存储检索到的文档
  2. 通过 middleware 钩子(如 before_model)添加一个新节点来填充该键(以及注入上下文)。

安全性:间接提示注入

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

后续步骤

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