LangChain1.0智能体开发:检索增强生成(RAG)

检索增强生成(RAG)是一种常见的智能体应用。阅读本文您将获得:

  • 检索增强生成(RAG)基础知识
  • LangChain实现RAG的环境搭建
  • LangChain如何构建索引
  • LangChain实现Agentic RAG
  • LangChain实现Two-Step RAG
  • 多种架构的RAG的特点及适用场景

1、概述

大模型赋能的最强大应用之一,便是功能完善的问答(Q&A)聊天机器人。这类应用能够针对特定来源的信息回答问题,其核心技术是检索增强生成(Retrieval Augmented Generation,简称 RAG)。

本文将展示如何基于非结构化文本数据源构建一个简易的问答应用,并演示以下两种实现方式:

  • Agentic RAG:通过工具执行搜索的RAG智能体,这是一种通用性较强的实现方案。
  • Two-Step RAG:每次查询仅需调用一次大模型的两步式RAG链,该方案速度快、效果好,适用于处理简单查询。

1.1 核心概念

  • 索引构建(Indexing):从数据源获取数据并为其创建索引的流程,该流程通常在独立进程中执行。
  • 检索与生成(Retrieval and generation):检索增强生成(RAG)的实际执行过程,在运行时接收用户查询,从索引中检索相关数据,再将这些数据传递给模型。完成数据索引构建后,我们将以智能体作为任务编排框架,实现检索与生成步骤。

1.2 应用场景

在本文中,我们将构建一个能够针对某网站内容回答问题的应用程序。我们将使用的特定网站是 Lilian Weng 撰写的《LLM Powered Autonomous Agents》博客文章。通过这个应用,我们可以就该博客文章的内容提出问题并获得解答。 我们只需约40行代码,就能创建一个简易的索引构建流程和检索增强生成(RAG)链来实现上述功能。完整代码见第4.1部分和4.2部分。

2、环境搭建

2.1 安装依赖

bash 复制代码
pip install langchain langchain-text-splitters langchain-community bs4

2.2 LangSmith

使用LangChain构建的许多应用程序,都会包含多个步骤,且这些步骤中会多次调用大模型。随着这些应用程序复杂度的提升,能够查看智能体内部具体的运行情况,会变得至关重要。而实现这一需求的最佳方式,就是借助LangSmith工具。在注册LangSmith账号后,可以设置如下环境变量,以开启追踪日志记录功能:

python 复制代码
import os

os.environ["LANGSMITH_TRACING"] = "true"
os.environ["LANGSMITH_API_KEY"] = "your-langsmith-api-key"

2.3 组件

我们需要从LangChain的集成组件集中选择三个组件。

  • 选择一个聊天模型:本文使用Qwen/Qwen3-8B模型,由硅基流动提供的OpenAI接口。
bash 复制代码
pip install -U "langchain-openai"
python 复制代码
from langchain.chat_models import init_chat_modeldef init_model():
model = init_chat_model(api_key = api_key,base_url = api_base,model = "Qwen/Qwen3-8B",model_provider = "openai",temperature = 0.7,)
  • 选择一个嵌入模型:本文使用BAAI/bge-m3模型,由硅基流动提供的OpenAI接口。
python 复制代码
from langchain_openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings(model="BAAI/bge-m3", api_key=api_key, base_url=api_base)
  • 选择一个向量存储(数据库):本文使用内存向量存储。
bash 复制代码
pip install -U "langchain-core"
python 复制代码
from langchain_core.vectorstores import InMemoryVectorStore
vector_store = InMemoryVectorStore(embeddings)

3、构建索引

索引构建的常见流程如下:

  • 加载(Load):首先需要加载数据,这一步通过文档加载器(Document Loaders)完成。
  • 分割(Split):文本分割器(Text splitters)将大型文档(Documents)拆分为更小的文本块(chunks)。这一步对数据索引构建和将数据传入模型均有帮助 ------ 因为大型文本块不仅难以检索,还无法适配模型有限的上下文窗口。
  • 存储(Store):需要一个位置来存储和索引这些分割后的文本块,以便后续进行检索。这一步通常通过向量存储(VectorStore)和嵌入模型(Embeddings model)实现。

3.1 加载文档

首先,我们需要加载该博客文章的内容。这一步可以借助文档加载器(DocumentLoaders)完成。文档加载器是一类能从数据源读取数据,并返回文档对象(Document objects)列表的组件。 本案例中,我们将使用网络基础加载器(WebBaseLoader)。该加载器通过urllib库从网页链接(URL)加载 HTML 内容,并借助BeautifulSoup库将 HTML 解析为纯文本。我们可以通过bs_kwargs参数向BeautifulSoup解析器传入配置,从而自定义HTML转文本的解析规则。在本案例中,仅包含 "post-content""post-title" 或 "post-header" 类的HTML标签是相关的,因此我们会移除所有其他标签。

python 复制代码
import bs4
from langchain_community.document_loaders import WebBaseLoader

# Only keep post title, headers, and content from the full HTML.
bs4_strainer = bs4.SoupStrainer(class_=("post-title", "post-header", "post-content"))
loader = WebBaseLoader(
    web_paths=("https://lilianweng.github.io/posts/2023-06-23-agent/",),
    bs_kwargs={"parse_only": bs4_strainer},
)
docs = loader.load()

assert len(docs) == 1
print(f"Total characters: {len(docs[0].page_content)}")

3.2 分割文档

我们加载的文档字符数超过4.2万,对于许多模型的上下文窗口而言,这个长度过长,无法完整容纳。即便某些模型的上下文窗口能装下整篇博文,模型在处理极长输入时,也难以从中精准定位所需信息。 为解决这一问题,我们会将该文档分割成多个文本块,以便进行嵌入处理和向量存储。这样做有助于我们在运行时,仅检索出博文中与查询最相关的部分内容。 我们将使用RecursiveCharacterTextSplitter(递归字符文本分割器)。该分割器会使用换行符等常见分隔符,对文档进行递归分割,直到每个文本块的大小达到合适范围。对于通用文本场景,这是推荐使用的文本分割器。

python 复制代码
from langchain_text_splitters import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,  # chunk size (characters)
    chunk_overlap=200,  # chunk overlap (characters)
    add_start_index=True,  # track index in original document
)
all_splits = text_splitter.split_documents(docs)

print(f"Split blog post into {len(all_splits)} sub-documents.")

3.3 存储文档

现在,我们需要为这些分割后的文本块建立索引,以便在运行时对它们进行检索。构建思路是:对每个分割后的文档内容进行嵌入处理,然后将生成的嵌入向量存入向量存储(数据库)。当接收到输入查询时,我们就能通过向量搜索来检索相关文档。 借助2.3节所选的向量存储和嵌入模型,我们只需一条命令,即可完成所有分割后文档的嵌入处理与存储操作。

python 复制代码
document_ids = vector_store.add_documents(documents=all_splits)

print(document_ids[:3])

至此,流程中的索引构建部分已完成。此时,我们拥有了一个可查询的向量存储(vector_store),其中包含了我们将博客文章分割后得到的文本块内容。当接收到用户的问题时,理论上我们应当能够返回该博客文章中可解答此问题的片段。

4、检索与生成

检索增强生成(RAG)应用的工作流程通常如下:

  • 检索(Retrieve):接收用户输入后,通过检索器(Retriever)从存储系统中检索出相关的文本块。
  • 生成(Generate):模型以包含 "用户问题" 和 "检索到的数据" 的提示词为输入,生成对应的回答。

现在我们来编写实际的应用程序逻辑。我们要创建一个简易应用,该应用能接收用户的问题,搜索与该问题相关的文档,将检索到的文档和初始问题传递给模型,然后返回答案。 我们将演示以下两种实现模式:

  • Agentic RAG:一个可通过简易工具执行搜索的RAG智能体,这是一种通用性较强的实现方案。
  • Two-Step RAG:一个每次查询仅需调用一次大模型的两步式RAG链,该方案速度快、效果好,适用于处理简单查询。

4.1 Agentic RAG

RAG应用的一种实现模式,是将其设计为一个配备信息检索工具的简易智能体。我们可以通过开发一个封装了向量存储的工具,来构建一个基础的RAG智能体。

完整代码实现如下:

python 复制代码
import os
# 设置USER_AGENT环境变量
os.environ["USER_AGENT"] = "langchain-course-agent"

import bs4
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain.tools import tool
from langchain_community.document_loaders import WebBaseLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_openai import OpenAIEmbeddings
from config import api_key, api_base, langsmith_api_key


# 初始化LangSmith追踪
os.environ["LANGSMITH_TRACING"] = "true"
os.environ["LANGSMITH_API_KEY"] = langsmith_api_key
# 初始化向量嵌入模型
embeddings = OpenAIEmbeddings(model="BAAI/bge-m3", api_key=api_key, base_url=api_base)
# 初始化智能体模型
model = init_chat_model(api_key=api_key, base_url=api_base, model="Qwen/Qwen3-8B", model_provider="openai", temperature=0.7,)
# 初始化向量存储
vector_store = InMemoryVectorStore(embeddings)


# 加载并粗分博客文章
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()

# 切分文档
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
all_splits = text_splitter.split_documents(docs)

# 索引文本块
vector_store = InMemoryVectorStore(embeddings)
_ = vector_store.add_documents(documents=all_splits)


# 构建用于检索的工具
@tool(response_format="content_and_artifact")
def retrieve_context(query: str):
    """Retrieve information to help answer a query."""
    retrieved_docs = vector_store.similarity_search(query, k=2)
    serialized = "\n\n".join(
        (f"Source: {doc.metadata}\nContent: {doc.page_content}")
        for doc in retrieved_docs
    )
    return serialized, retrieved_docs

#工具列表
tools = [retrieve_context]
# 系统提示词
prompt = (
    "You have access to a tool that retrieves context from a blog post. "
    "Use the tool to help answer user queries."
)
#创建智能体
agent = create_agent(model, tools, system_prompt=prompt)
#查询
query = ("What is the standard method for Task Decomposition?\n\n"
    "Once you get the answer, look up common extensions of that method.")
for step in agent.stream({"messages": [{"role": "user", "content": query}]},stream_mode="values"):
    step["messages"][-1].pretty_print()

请注意,该智能体的操作流程如下:

  • 生成查询语句,以搜索任务分解的标准方法;
  • 接收查询结果后,生成第二个查询语句,以搜索该标准方法的常见扩展形式;
  • 获取所有必要的背景信息后,对原问题进行解答。 我们可以在LangSmith的追踪日志中查看完整的步骤序列,以及延迟数据和其他元数据。

4.2 Two-Step RAG

在4.1节智能体驱动式RAG的实现形式中,我们允许大模型自主判断是否生成工具调用,以辅助解答用户查询。这是一种不错的通用解决方案,但也存在一些权衡取舍:

✅ 优势(Benefits) ⚠️ 劣势(Drawbacks)
按需检索 ------ 大模型可处理问候语、追问及简单查询,无需触发不必要的检索操作。 两次推理调用 ------ 执行检索时,需调用一次模型生成查询语句,再调用一次生成最终回复。
语境化检索查询 ------ 将检索视为需输入查询语句的工具,大模型会结合对话语境自行构建检索查询语句。 可控性降低 ------ 大模型可能在实际需要检索时跳过检索步骤,或在无需检索时发起多余的检索操作。
支持多轮检索 ------ 针对单个用户查询,大模型可执行多次检索以获取足够信息。 -

另一种常见实现模式是两步式链(two-step chain)。在这种模式中,我们会始终执行检索操作,并将检索结果作为上下文,传入单次大模型查询中。这样一来,每个用户查询仅需一次推理调用,以牺牲灵活性为代价,换取了更低的延迟。 在这种方法中,我们不再以循环方式调用模型,而是仅执行单次处理流程。 我们可以通过以下方式实现该链:从智能体中移除工具组件,转而将检索步骤整合到自定义提示词中。

完整代码实现如下:

python 复制代码
import os
# 设置USER_AGENT环境变量
os.environ["USER_AGENT"] = "langchain-course-agent"

import bs4
from langchain.agents import create_agent
from langchain.agents.middleware import dynamic_prompt, ModelRequest
from langchain.chat_models import init_chat_model
from langchain_community.document_loaders import WebBaseLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_openai import OpenAIEmbeddings
from config import api_key, api_base, langsmith_api_key


# 初始化LangSmith追踪
os.environ["LANGSMITH_TRACING"] = "true"
os.environ["LANGSMITH_API_KEY"] = langsmith_api_key
# 初始化向量嵌入模型
embeddings = OpenAIEmbeddings(model="BAAI/bge-m3", api_key=api_key, base_url=api_base)
# 初始化智能体模型
model = init_chat_model(api_key = api_key,base_url = api_base,model = "Qwen/Qwen3-8B",model_provider = "openai",temperature = 0.7,)
# 初始化向量存储
vector_store = InMemoryVectorStore(embeddings)

# 加载并粗分博客文章
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()

# 切分文档
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
all_splits = text_splitter.split_documents(docs)

# 索引文本块
vector_store = InMemoryVectorStore(embeddings)
_ = vector_store.add_documents(documents=all_splits)

#构建动态系统提示词,加入检索文本块
@dynamic_prompt
def prompt_with_context(request: ModelRequest) -> str:
    """Inject context into state messages."""
    last_query = request.state["messages"][-1].text
    retrieved_docs = vector_store.similarity_search(last_query)

    docs_content = "\n\n".join(doc.page_content for doc in retrieved_docs)

    system_message = (
        "You are a helpful assistant. Use the following context in your response:"
        f"\n\n{docs_content}"
    )
    return system_message
#创建智能体
agent = create_agent(model, tools=[], middleware=[prompt_with_context])
#查询
query = ("What is the standard method for Task Decomposition?\n\n"
    "Once you get the answer, look up common extensions of that method.")
for step in agent.stream({"messages": [{"role": "user", "content": query}]},stream_mode="values"):
    step["messages"][-1].pretty_print()

在受限环境下,对于简单查询而言,这是一种快速且高效的方法 ------ 此时我们通常希望通过检索用户查询,以获取更多上下文信息。

5、RAG架构比较

根据系统需求,RAG可通过多种方式实现。下表展示了不同架构的RAG的特点和适用场景:

架构(Architecture) 描述(Description) 可控性(Control) 灵活性(Flexibility) 延迟(Latency) 适用场景(Use Case)
两步式 RAG(2-Step RAG) 检索过程始终在生成过程之前进行,流程简单且可预测 ✅ 高 ❌ 低 ⚡ 快 常见问题解答(FAQs)、文档问答机器人
智能体驱动式 RAG(Agentic RAG) 由大型语言模型(LLM)驱动的智能体,在推理过程中决定何时检索、如何检索 ❌ 低 ✅ 高 ⏳ 可变 可调用多种工具的研究助手
混合式 RAG(Hybrid) 融合前两种架构的特点,并加入验证环节 ⚖️ 中等 ⚖️ 中等 ⏳ 可变 需质量验证的特定领域问答(如医疗、法律领域问答)

两步式 RAG(2-Step RAG)的延迟通常更具可预测性,因为大模型调用的最大次数是已知且固定的(一次问答仅调用一次大模型)。这种可预测性的前提是,LLM 推理时间是影响延迟的主导因素。但在实际场景中,延迟还可能受到检索环节性能的影响,例如接口(API)响应时间、网络延迟或数据库查询效率,这些因素会因所使用的工具和基础设施不同而存在差异。

相关推荐
xixixi777772 小时前
攻击链重构的具体实现思路和分析报告
开发语言·python·安全·工具·攻击链
Learn Beyond Limits2 小时前
Data Mining Tasks|数据挖掘任务
人工智能·python·神经网络·算法·机器学习·ai·数据挖掘
韩立学长2 小时前
【开题答辩实录分享】以《证劵数据可视化分析项目设计与实现》为例进行答辩实录分享
python·信息可视化·vue
蓝桉~MLGT2 小时前
Python学习历程——模块
开发语言·python·学习
知忆_IS3 小时前
【问题解决】Label Studio上传文件数量超限解决方案
python·目标检测·label studio
武子康3 小时前
Java-169 Neo4j CQL 实战速查:字符串/聚合/关系与多跳查询
java·开发语言·数据库·python·sql·nosql·neo4j
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ3 小时前
MyBatis Plus中执行原生SQL语句方法
python·sql·mybatis
AI大模型3 小时前
小白也能训大模型!Hugging Face用「200页手册」亲自教学,连踩的坑都告诉你了...
程序员·llm·agent