概述
LLM 实现的最强大应用之一是复杂的问答聊天机器人。这些是能够回答有关特定源信息的问题的应用程序。这些应用程序使用一种称为检索增强生成(Retrieval Augmented Generation)或 RAG 的技术。 本教程将展示如何构建一个基于非结构化文本数据源的简单问答应用程序。我们将演示:概念
我们将涵盖以下概念:- 索引:从源摄取数据并对其进行索引的管道。这通常发生在单独的进程中。
- 检索和生成:实际的 RAG 过程,在运行时获取用户查询并从索引中检索相关数据,然后将其传递给模型。
预览
在本指南中,我们将构建一个回答有关网站内容的应用程序。我们将使用的特定网站是 Lilian Weng 的 LLM Powered Autonomous Agents 博客文章,这允许我们询问有关帖子内容的问题。 我们可以创建简单的索引管道和 RAG 链来在约 40 行代码中完成此操作。请参阅下面的完整代码片段:展开完整代码片段
展开完整代码片段
设置
安装
本教程需要以下 langchain 依赖项:LangSmith
您使用 LangChain 构建的许多应用程序都包含多个步骤和多次 LLM 调用。随着这些应用程序变得更加复杂,能够检查链或代理内部发生的事情变得至关重要。做到这一点的最佳方法是使用 LangSmith。 在上述链接注册后,确保设置您的环境变量以开始记录追踪:组件
我们需要从 LangChain 的集成套件中选择三个组件。 选择聊天模型:- OpenAI
- Anthropic
- Azure
- Google Gemini
- AWS Bedrock
- HuggingFace
- OpenRouter
- OpenAI
- Azure
- Google Gemini
- Google Vertex
- AWS
- HuggingFace
- Ollama
- Cohere
- MistralAI
- Nomic
- NVIDIA
- Voyage AI
- IBM watsonx
- Fake
- Isaacus
- In-memory
- Amazon OpenSearch
- AstraDB
- Chroma
- FAISS
- Milvus
- MongoDB
- PGVector
- PGVectorStore
- Pinecone
- Qdrant
1. 索引
索引通常按如下方式工作:- 加载:首先我们需要加载数据。这是使用文档加载器完成的,它们是从源加载数据并返回文档对象列表的对象。
- 分割:文本分割器将大
文档分割成较小的块。这对于索引数据和将其传递给模型都很有用,因为大块更难搜索并且不会放入模型的有限上下文窗口中。 - 存储:我们需要某个地方来存储和索引我们的分块,以便以后可以搜索它们。这通常使用向量存储和嵌入模型完成。
加载文档
我们需要首先加载博客文章内容。我们可以使用文档加载器,它们从源加载数据并返回文档对象列表。 在这种情况下,我们将使用WebBaseLoader,它使用 urllib 从 Web URL 加载 HTML,并使用 BeautifulSoup 将其解析为文本。我们可以通过 bs_kwargs 将参数传递到 BeautifulSoup 解析器来自定义 HTML -> 文本解析(请参阅 BeautifulSoup 文档)。在这种情况下,只有带有类”post-content”、“post-title”或”post-header”的 HTML 标签是相关的,因此我们将删除所有其他标签。
DocumentLoader:从源加载数据作为 文档 列表的对象。
- 集成:160+ 集成可供选择。
BaseLoader:基础接口的 API 参考。
分割文档
我们加载的文档超过 42k 个字符,这对于许多模型的上下文窗口来说太长了。即使对于可以将其放入上下文窗口的模型,模型也可能在非常长的输入中难以找到信息。 为了处理这个问题,我们将文档 分割成用于嵌入和向量存储的块。这将帮助我们在运行时仅检索博客文章中最相关的部分。
与语义搜索教程一样,我们使用 RecursiveCharacterTextSplitter,它将递归地使用常见分隔符(如新行)分割文档,直到每个块达到合适的大小。这是用于通用文本用例的推荐文本分割器。
TextSplitter:将 文档 列表分割成更小块用于存储和检索的对象。
存储文档
现在我们需要索引我们 66 个文本块,以便在运行时可以搜索它们。按照语义搜索教程的方法,我们的方法是嵌入每个文档分块的内容,并将其插入向量存储。给定输入查询,我们然后可以使用向量搜索来检索相关文档。 我们可以使用向量存储和嵌入模型在单个命令中嵌入和存储我们所有的文档分块(请参阅教程开头选择的内容)。嵌入:文本嵌入模型的包装器,用于将文本转换为嵌入。
向量存储:向量数据库的包装器,用于存储和查询嵌入。
这完成了管道的索引部分。到目前为止,我们有了一个可查询的向量存储,包含我们博客文章的分块内容。给定用户问题,我们应该能够返回回答该问题的博客文章片段。
2. 检索和生成
RAG 应用程序通常按如下方式工作:
现在让我们编写实际的应用程序逻辑。我们想要创建一个简单的应用程序,接收用户问题,搜索与该问题相关的文档,将检索到的文档和初始问题传递给模型,并返回答案。
我们将演示:
RAG 代理
RAG 应用程序的一种表述是作为带有检索工具的简单代理。我们可以通过实现包装向量存储的工具来组装一个最小的 RAG 代理:- 生成查询以搜索任务分解的标准方法;
- 收到答案后,生成第二个查询以搜索其常见扩展;
- 收到所有必要的上下文后,回答问题。
RAG 链
在上述代理 RAG表述中,我们允许 LLM 使用其判断来生成工具调用以帮助回答用户查询。这是一个良好的通用解决方案,但有一些权衡:| ✅ 优势 | ⚠️ 缺点 |
|---|---|
| 仅在需要时搜索——LLM 可以在不触发不必要搜索的情况下处理问候语、跟进和简单查询。 | 两次推理调用——当执行搜索时,需要一次调用来生成查询,另一次来产生最终响应。 |
上下文感知搜索查询——通过将搜索作为带有 query 输入的工具对待,LLM 可以生成包含对话上下文的自己的查询。 | 减少控制——LLM 可能会在实际需要时跳过搜索,或在不必要时发出额外搜索。 |
| 允许多次搜索——LLM 可以执行多次搜索以支持单个用户查询。 |
返回源文档
返回源文档
安全性:间接提示注入
为了缓解这个问题:- 使用防御性提示:明确指示模型仅将检索到的上下文视为数据,并忽略其中的任何指令。本教程中的提示包含此类指令。
- 使用分隔符包装上下文:使用清晰的结构标记(例如 XML 标签如
<context>...</context>)将检索到的数据与指令分开,使模型更容易区分它们。 - 验证响应:检查模型输出是否与预期格式(例如纯文本)匹配,并优雅地处理意外格式。
下一步
现在我们已经通过create_agent 实现了简单的 RAG 应用程序,我们可以轻松地添加新功能并深入了解:
- 流式传输令牌和其他信息以获得响应式用户体验
- 添加对话记忆以支持多轮交互
- 添加长期记忆以支持跨对话线程的记忆
- 添加结构化响应
- 使用 LangSmith 部署部署您的应用程序

