最强大的基于 LLM 的应用之一是复杂的问答 (Q&A) 聊天机器人,它们通过为 LLM 提供对一组数据的结构化访问来增强 LLM。 这可能是私有数据、最新数据,或不属于 LLM 训练数据的数据。 这些应用使用一种称为检索增强生成的技术,或简称 RAG.
本教程将引导您构建一个回答有关长篇非结构化文本问题的应用:
- **索引内容**:创建从源获取数据并为其编制索引的管道。
- **RAG 代理**:一种通用实现,可搜索索引内容并将相关上下文传递给 LLM。
- **RAG 链**:一种两步实现,每次查询使用一次 LLM 调用。这是一种简单查询的快速有效方法。
本教程使用 LLM 驱动的自主代理 博客文章作为示例。
使用 LangSmith to 追踪 您在学习本教程时的检索和生成过程。
设置
Install core dependencies
npm i langchain @langchain/textsplitters cheerio
yarn add langchain @langchain/textsplitters cheerio
pnpm add langchain @langchain/textsplitters cheerio
欲了解更多详细信息,请参阅我们的 安装指南.
Set up LangSmith
RAG 应用按顺序执行检索和生成。当您运行本教程中的示例时, LangSmith 会为每个查询记录一条追踪,以便您可以检查检索、工具调用和模型响应。 在您 注册 LangSmith后,设置您的环境变量以开始记录追踪:
为内容编制索引
在索引步骤中,您需要获取源内容并将其转换 _为小块_ 。这种数值表示形式可以捕捉块的语义含义。将这些数值表示形式和文档块的映射存储在 VectorStore 中,当用户基于自己的数值表示形式发送查询时,您可以高效地检索相关内容。
索引通常通过四个步骤进行:
- **加载**:将您的数据源加载到
Document对象中。 - **拆分**:使用 文本拆分器 将大型
Document拆分为更小的块。这对于为数据编制索引和将数据传递给模型都很有用,因为大块更难搜索,且可能无法适应模型的有限上下文窗口或使用超过必要数量的令牌。 - **嵌入**: 嵌入向量 模型将每个文本块转换为捕捉其含义的数值向量,从而支持对内容进行相似性搜索。
- **存储**:使用 向量存储 为文本块及其嵌入向量建立索引,以便检索。
!索引_图
在以下步骤中,您将设置摄取源内容所需的组件。
加载文档
首先将博客文章内容加载到 Document 对象列表中。
使用 fetch 来获取页面,使用 cheerio 将其解析为文本。 您可以通过将 CSS 选择器传递给 loadWebPage. 来自定义 HTML 到文本的解析。在这种情况下,只有 class 为 post-content, post-title, or post-header 的元素是相关的,因此您可以选择这些元素并忽略其余部分:
如果运行此代码,它会打印:
Total characters: 43133
您也可以查看页面内容本身:
Building agents with LLM (large language model) as its core controller is...
拆分文档
加载的文档很长,这使得它太大,无法适应许多模型的上下文窗口。 即使对于那些可以将整篇文章放入上下文窗口的模型,模型也可能在非常长的输入中难以找到信息。
为了便于使用,请将 Document 拆分为文本块。这些文本块将在下一步用于嵌入和向量存储。
使用 RecursiveCharacterTextSplitter 使用常见的分隔符(如换行符)递归拆分文档,直到每个文本块达到合适的大小。 RecursiveCharacterTextSplitter 是推荐的 TextSplitter 通用文本用例的
Split blog post into 64 sub-documents.
选择嵌入模型
An 嵌入 是一个数值向量,用于捕捉博客文章每个片段的含义。Embeddings 模型将这些片段转换为向量,使得相似的含义在向量空间中位置接近,从而在用户提问时能够检索到相关内容。
您可以从多种不同的 嵌入集成 中进行选择,它们都使用相同的 Interface:
将片段和嵌入存储在 VectorStore 中
A VectorStore 持久化文档片段及其嵌入,实现相似性搜索以在用户提问时检索相关内容。 您可以从多种不同的 向量存储集成 中进行选择,它们都使用相同的 Interface。 使用您在上一步选择的嵌入模型来配置您的 VectorStore:
然后,使用上述初始化的 vector_store 对所有文档片段进行嵌入和存储:
运行后,输出如下:
Indexed 64 document chunks.
这完成了 **索引** 部分教程。您现在拥有了一个可查询的向量存储,其中包含博客文章的切分内容。
下一步是检索和生成:在运行时给定用户问题,从索引中提取相关片段并将其传递给模型以生成答案。RAG 应用程序通常分两个阶段实现这一流程:
!检索_图
本教程将逐步介绍该流程的两种实现: RAG 智能体 在需要时调用搜索工具,以及 RAG 链 总是检索一次并在单次模型调用中回答。
RAG 智能体
以下步骤展示了如何构建一个最小的 智能体 它配备了包装向量存储的检索工具。智能体决定何时搜索与用户问题相关的文档,将检索到的文档和用户问题传递给模型,然后返回答案。
Create the retrieval tool
工具 是具有明确定义输入和输出的可调用函数,被传递给模型后由模型决定何时调用它们。您可以实现一个包装向量存储的工具:
指定 responseFormat as content_and_artifact 配置工具以将原始文档附加为 工件 到每个 ToolMessage。这让您可以在应用程序中访问文档元数据,与发送到模型的字符串化表示分离。
该 k 参数设置相似性搜索返回的文档块数量。使用 k=2,向量存储返回两个嵌入最接近查询嵌入的块。
Select a chat model
您可以在下一步创建的代理中使用任何模型:
Create the agent
您现在可以使用 model 从之前的步骤和您的检索工具创建代理:
要测试此功能,请构建一个需要按顺序进行多个检索步骤才能回答的问题:
运行此代码时,您将获得以下输出:
Tool call: retrieve({"query":"standard method for Task Decomposition"})
Tool result: Source: https://lilianweng.github.io/posts/2023-06-23-agent/
Content: hard tasks into smaller and simpler steps...
Source: https://lilianweng.github.io/posts/2023-06-23-agent/
Content: System message:Think step by step and reason yourself...
Tool call: retrieve({"query":"common extensions of Task Decomposition method"})
Tool result: Source: https://lilianweng.github.io/posts/2023-06-23-agent/
Content: hard tasks into smaller and simpler steps...
Source: https://lilianweng.github.io/posts/2023-06-23-agent/
Content: be provided by other developers (as in Plugins) or self-defined...
### Standard Method for Task Decomposition
The standard method for task decomposition involves...
当您的代理运行时:
- 生成用于搜索任务分解标准方法的查询。
- 接收答案并生成第二个查询以搜索其常见扩展。
- 在收到所有必要的上下文后回答问题。
如果您在 设置中启用了 LangSmith,请打开 LangSmith,选择您的 **默认** 项目,然后在 **Traces** 标签中打开此运行的追踪。在 详情视图中检查每次检索和模型调用。您也可以将此追踪与示例 LangSmith 追踪.
Full code
此示例是自包含的:它加载博客文章、索引内容并运行查询。将设置和运行块一起复制。
如果您在 设置中启用了 LangSmith,请打开 LangSmith,选择您的 **默认** 项目,然后在 **Traces** 标签中打开此运行的追踪。您也可以将此追踪与示例 LangSmith 追踪进行比较。要了解更多关于追踪 LangChain 应用程序的信息,请参阅 使用 LangChain 进行追踪.
RAG 链
在 RAG 代理 中,您允许 LLM 自行决定是否生成 工具调用 来帮助回答用户查询。这是一个不错的通用解决方案,但也有一些权衡:
| ✅ 优势 | ⚠️ 缺点 |
|---|---|
| **仅在需要时搜索**:LLM 可以处理问候、追问和简单查询,而不会触发不必要的搜索。 | **两次推理调用**:执行搜索时,需要一次调用来生成查询,另一次来生成最终响应。 |
**情境化搜索查询**:通过将搜索视为带有 query 输入的工具,LLM 可以生成结合对话情境的查询。 | **可控性降低**:LLM 可能会在真正需要搜索时跳过搜索,或者在不需要时执行额外搜索。 |
| **允许多次搜索**:LLM 可以为单个用户查询执行多次搜索。 |
另一种常见方法是两步链式处理,在这种方法中您始终执行搜索,可以使用原始用户查询,然后将结果作为单个 LLM 查询的上下文。这每查询只需一次推理调用,以灵活性换取更低的延迟。
在这种方法中,我们不再循环调用模型,而是进行单次处理。
您可以通过从代理中移除工具,并将检索步骤合并到自定义提示词中来实现此链式处理:
该 dynamicSystemPromptMiddleware 将检索到的上下文注入系统提示词。如果您还需要在应用状态中保留带有元数据的原始文档,请使用 beforeModel 钩子通过 createMiddleware 。这让您可以在应用中访问文档元数据,与发送给模型的字符串化表示分离:
运行此代码后,您将获得以下输出:
如果您在 设置中启用了 LangSmith,请打开 LangSmith,选择您的 **默认** 项目,然后在 **追踪** 标签中打开此次运行的追踪记录。在 详情视图中检查检索到的上下文是如何传递给模型的。您也可以将此追踪与此示例 LangSmith 追踪 或多步骤 代理追踪.
这是一种在受限环境中处理简单查询的快速有效方法,当您几乎总是希望通过语义搜索运行用户查询来获取额外上下文时。
Full code
此示例是独立的:它加载博客文章、对内容进行索引并运行查询。将设置和运行块一起复制。
如果您在 设置中启用了 LangSmith,请打开 LangSmith,选择您的 **默认** 项目,并在此运行的追踪中打开 **追踪** 选项卡。您还可以将您的追踪与此示例 LangSmith 追踪进行比较。有关追踪 LangChain 应用的更多信息,请参阅 使用 LangChain 进行追踪.
安全考虑
缓解措施:
- **使用防御性提示**:明确指示模型将检索到的上下文仅视为数据,并忽略其中的任何指令。本教程中的提示包含此类说明。
- **使用分隔符包装上下文**:使用清晰的结构标记(例如
<context>...</context>之类的 XML 标签)来区分检索到的数据与指令,使模型更容易区分它们。 - **验证响应**:检查模型输出是否符合预期格式(例如纯文本),并优雅地处理意外格式。
没有任何缓解措施是万无一失的——这是当前 LLM 架构的固有局限性,其中指令和数据共享相同的上下文窗口。有关此主题的更多信息,请参阅关于 提示注入.
的后续步骤
既然您已通过 @[createAgent实现了简单的 RAG 应用,您可以加入新功能并深入研究:
- - 使用 LangSmith 数据集和评估器 评估 RAG 应用
- - 流式传输 令牌和其他信息以实现响应式用户体验
- - 添加 对话记忆 以支持多轮交互
- - 添加 长期记忆 支持跨对话线程的记忆
- - 添加 结构化响应
- - 使用以下方式部署您的应用 LangSmith 部署