LangChain教程 - 构建一个检索增强生成 (RAG) 应用程序
系列文章索引
LangChain教程 - 系列文章
大型语言模型 (LLM) 的强大应用之一是复杂的问答 (Q&A) 聊天机器人。此类应用能够回答有关特定信息源的问题。这些应用程序使用了一种被称为检索增强生成 (Retrieval-Augmented Generation, RAG) 的技术。
本教程将向你展示如何在文本数据源上构建一个简单的问答应用程序。我们将逐步讲解典型的问答架构,并展示如何实现更复杂的问答技术。另外,我们还会介绍如何使用 LangSmith 来帮助我们跟踪和理解应用程序的运行情况。当应用程序复杂性增加时,LangSmith 将变得越来越有用。
什么是 RAG?
RAG 是一种通过检索额外数据来增强大型语言模型知识的技术。
虽然 LLM 可以处理各种广泛主题,但其知识受限于训练时使用的公共数据,通常只包含模型训练时之前的数据。如果你想构建能够处理私有数据或模型训练后新增数据的 AI 应用,就需要为模型提供特定的信息。这个过程称为Retrieval Augmented Generation (RAG),即从外部数据源获取信息并将其插入模型的提示中。
LangChain 提供了很多组件来帮助构建问答应用程序,以及更通用的 RAG 应用程序。
RAG 的基本组成
一个典型的 RAG 应用程序有两个主要组件:
- 索引:这是一个从数据源获取数据并创建索引的管道,通常离线完成。
- 检索与生成:在应用运行时,RAG 链会接收用户查询,检索相关数据,然后将数据传递给模型。
完整流程通常如下:
索引流程:
- 加载数据:首先需要加载数据,通常使用 Document Loaders。
- 文本切分:使用文本切分器将大文档分割成较小的块,这样既有利于索引,也有利于将其传递给模型,因为大块文本难以检索且无法完全放入模型的上下文窗口中。
- 存储数据:将分割后的文档存储并创建索引,通常使用向量数据库 (VectorStore) 和嵌入模型 (Embeddings)。
检索与生成流程:
- 检索:根据用户输入,使用 Retriever 从存储的文本块中检索出相关的数据。
- 生成:使用 LLM 或聊天模型生成答案,提示中包含用户问题和检索到的文本数据。
环境准备
我们将在 Jupyter Notebook 中编写代码。Jupyter 是学习和使用 LLM 系统的理想工具,因为可以在交互式环境中便捷地调试。要安装必要的库,可以使用以下命令:
%pip install --quiet --upgrade langchain langchain-community langchain-chroma
此外,我们还将使用 LangSmith 来跟踪应用程序的运行情况。你可以通过如下代码设置环境变量来启用 LangSmith:
import getpass
import os
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_API_KEY"] = getpass.getpass()
实现 RAG 应用
我们将构建一个可以回答有关网页内容问题的问答应用,具体使用 Lilian Weng 的博客文章《LLM Powered Autonomous Agents》作为数据源。
1. 加载数据
首先,我们需要使用 Document Loader 来加载博客内容。文档加载器可以从指定的数据源(如网页)提取文本,并将其转换为文档对象。我们使用 WebBaseLoader
加载网页,并使用 BeautifulSoup 进行文本解析,只保留带有 post-content
、post-title
或 post-header
类的 HTML 标签内容。
import bs4
from langchain_community.document_loaders import WebBaseLoader
# 只保留文章标题、标题和内容部分的 HTML 标签
loader = WebBaseLoader(
web_paths=("https://lilianweng.github.io/posts/2023-06-23-agent/",),
bs_kwargs=dict(
parse_only=bs4.SoupStrainer(class_=("post-content", "post-title", "post-header"))
),
)
docs = loader.load()
2. 切分文本
加载的文档长度可能过长,超出模型的上下文窗口,因此我们需要将其切分成较小的块。这里使用 RecursiveCharacterTextSplitter
来将文档分割为 1000 个字符的块,并在块之间重叠 200 个字符,以避免重要信息被切分掉。
from langchain_text_splitters import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
splits = text_splitter.split_documents(docs)
3. 存储数据
我们将文档分块后需要存储这些数据,以便后续可以根据查询进行检索。这里我们使用 Chroma 向量数据库,并使用 OpenAI 的嵌入模型生成向量表示。
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings
vectorstore = Chroma.from_documents(documents=splits, embedding=OpenAIEmbeddings())
4. 检索数据
接下来,我们编写检索逻辑。Retriever
是一个接口,用于根据输入字符串返回相关的文档。我们将使用向量数据库的相似度检索来找到最相似的文档块。
retriever = vectorstore.as_retriever(search_type="similarity", search_kwargs={"k": 6})
retrieved_docs = retriever.invoke("任务分解有哪些方法?")
5. 生成答案
最后,将检索到的文档和用户问题传递给语言模型,并生成答案。我们可以使用 OpenAI 的 GPT 模型,并通过提示模板生成答案。
from langchain_openai import ChatOpenAI
from langchain import hub
llm = ChatOpenAI(model="gpt-4o-mini")
prompt = hub.pull("rlm/rag-prompt")
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
rag_chain.invoke("任务分解是什么?")
输出示例:
任务分解是将复杂任务分解为更小、更易管理的步骤的过程。该过程通常使用“思维链”或“思维树”等技术,这些技术可以指导模型逐步思考,将复杂任务分解为多个简单任务。任务分解可以通过模型提示、任务特定的指令或人工输入来实现。
6. 清理资源
完成操作后,可以删除向量数据库中的数据。
vectorstore.delete_collection()
完整代码实例
# 导入必要的库
import getpass
import os
import bs4
from langchain_community.document_loaders import WebBaseLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain import hub
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
# 设置 OpenAI API 密钥
os.environ["OPENAI_API_KEY"] = getpass.getpass("请输入你的 OpenAI API 密钥: ")
# 1. 加载网页内容
# 只保留网页中带有 'post-content'、'post-title' 或 'post-header' 类的标签内容
loader = WebBaseLoader(
web_paths=("https://lilianweng.github.io/posts/2023-06-23-agent/",),
bs_kwargs=dict(
parse_only=bs4.SoupStrainer(class_=("post-content", "post-title", "post-header"))
),
)
docs = loader.load()
# 2. 切分文本
# 将文档分割为 1000 个字符的块,块之间有 200 个字符的重叠
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
splits = text_splitter.split_documents(docs)
# 3. 存储分割后的文档到向量数据库
# 使用 OpenAI 嵌入模型生成文档向量表示,并存储到 Chroma 向量数据库中
vectorstore = Chroma.from_documents(documents=splits, embedding=OpenAIEmbeddings())
# 4. 构建检索器
# 使用相似度检索找到与查询最相关的文档
retriever = vectorstore.as_retriever(search_type="similarity", search_kwargs={"k": 6})
# 5. 定义 RAG 提示模板
prompt = hub.pull("rlm/rag-prompt")
# 格式化检索到的文档
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
# 6. 定义 RAG 链
# 创建检索增强生成链:包括检索、格式化文档、提示模板和语言模型生成答案
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| ChatOpenAI(model="gpt-4o-mini")
| StrOutputParser()
)
# 7. 生成答案
# 传入问题并获取答案
response = rag_chain.invoke("任务分解是什么?")
# 打印答案
print(response)
# 8. 清理向量数据库
vectorstore.delete_collection()
总结
通过本文教程,我们学习了如何构建一个简单的 RAG 问答应用,并逐步实现了从加载数据、分割文本、存储向量到检索和生成答案的完整流程。