构建RAG应用实战进阶(二):让对话型检索更聪明!——代码详解与原理通俗解读

在上一篇中,我们实现了最基础的RAG(检索增强生成)应用,能让大模型结合文档知识自动问答。
本篇将带你继续升级,让RAG应用支持多轮对话智能检索上下文流式输出,并通过通俗讲解让测试工程师轻松理解每一步代码背后的设计思想。


一、为什么要让RAG支持多轮对话?

在真实测试业务场景中,用户常常不是只提一个问题,而是连续追问、补充、切换话题。
如果机器人只能记住当前一轮,体验会很割裂。我们希望:

  • 机器人能记住之前的提问和回答,理解上下文;
  • 能针对历史问题进行多步检索,提升答案准确性。

二、核心技术选型与组件

本系列采用 LangChain 最新生态,主要包含:

组件作用选型
大语言模型(LLM)生成问答/分析用户意图gpt-4o-mini(openai)
向量化模型(Embeddings)把文本转成向量,用于检索text-embedding-3-large (openai)
向量数据库(Vector Store)存放文档向量、实现高效检索InMemoryVectorStore(内存型)
from langchain_community.embeddings import DashScopeEmbeddings
from dotenv import load_dotenv
from langgraph.graph import START, MessagesState, StateGraph

from langchain_core.messages import HumanMessage
import os

# 绑定langsmith工程
os.environ["deepseek-api-key"] = "sk-e3f022d1746f415c9b0f4bc9a52a43xx"  # deepseek api-key
os.environ["LANGSMITH_TRACING"] = "true"
os.environ["LANGSMITH_PROJECT"] = "pr-warmhearted-bassoon-71"
os.environ["LANGSMITH_API_KEY"] = "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"
os.environ["LANGSMITH_ENDPOINT"] = "https://api.smith.langchain.com"
os.environ["DASHSCOPE_API_KEY"] = "sk-712a634dbaa7444d838d20b25eb938xx"  # dashscope api-key

from langchain_openai import ChatOpenAI

# LLM大模型实例化
model = ChatOpenAI(
    model="deepseek-chat",
    api_key="sk-e3f022d1746f415c9b0f4bc9a52a439a",  # todo 替换deepseek API Key  https://platform.deepseek.com/api_keys
    temperature=0.7,
    max_tokens=512,
    timeout=30,
    max_retries=3,
    base_url="https://api.deepseek.com"
)
# 创建embedding模型实例,使用通义千问的多模态大模型
load_dotenv()  # 加载环境变量
embed_model = DashScopeEmbeddings(
    model = "text-embedding-v2",
    dashscope_api_key = os.getenv("DASHSCOPE_API_KEY")
)
# 创建向量数据库实例
from langchain_chroma import Chroma

vector_store = Chroma(
    collection_name = "langchain_test",
    embedding_function=embed_model,
    persist_directory="./langchain_test_chroma_db",
)

三、文档分割与向量存储

首先我们用爬虫工具把目标博客“LLM Powered Autonomous Agents”加载并切分成适合检索的小块:

from langchain_community.document_loaders import WebBaseLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
import bs4

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)

通俗解读:

  • WebBaseLoader:自动爬取网页内容。
  • RecursiveCharacterTextSplitter:把长文本切成便于检索的小块,避免上下文丢失。

然后将这些小块“嵌入”到向量库:

_ = vector_store.add_documents(documents=all_splits)
  • add_documents:将分割后的文本存入向量库,后续检索直接用向量比对。
    在这里插入图片描述

四、对话历史如何存储?——消息序列设计

对话型RAG的“记忆”靠消息序列 MessagesState 实现,每条消息都是一条历史

消息类型说明
HumanMessage用户输入
AIMessage机器人回复(含工具调用情况)
ToolMessage工具返回的检索文档
SystemMessage系统提示/角色约束等

LangGraph 提供了内置的 MessagesState,帮我们自动管理和追加这些消息。


五、检索工具的封装与调用

我们把“向量检索”逻辑封装成一个工具(tool),让LLM可以像调API一样调用它:

from langchain_core.tools import tool

@tool(response_format="content_and_artifact")
def retrieve(query: str):
    """Retrieve information related to 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

通俗解读:

  • @tool 装饰器把普通Python函数变成 LLM 可以直接调用的“工具”。
  • similarity_search(query, k=2) 表示检索与query最相近的2条文档。
  • 返回的内容会作为 ToolMessage 加入到对话历史。

六、RAG对话流程主线:三步法

1. 生成检索请求或直接回复

def query_or_respond(state: MessagesState):
    llm_with_tools = llm.bind_tools([retrieve])
    response = llm_with_tools.invoke(state["messages"])
    return {"messages": [response]}
  • LLM会分析历史对话,决定是直接回答还是先发起检索请求。

2. 执行检索工具

from langgraph.prebuilt import ToolNode
tools = ToolNode([retrieve])
  • ToolNode自动管理工具调用,把检索结果写入历史。

3. 用检索到的内容生成最终回复

def generate(state: MessagesState):
    # 把最近的ToolMessage合并进prompt
    docs_content = "\n\n".join(doc.content for doc in tool_messages)
    system_message_content = (
        "You are an assistant for question-answering tasks. "
        "Use the following pieces of retrieved context to answer "
        "the question. If you don't know the answer, say that you "
        "don't know. Use three sentences maximum and keep the "
        "answer concise."
        "\n\n"
        f"{docs_content}"
    )
    conversation_messages = [
        message
        for message in state["messages"]
        if message.type in ("human", "system")
        or (message.type == "ai" and not message.tool_calls)
    ]
    prompt = [SystemMessage(system_message_content)] + conversation_messages
    response = llm.invoke(prompt)
    return {"messages": [response]}
  • 把检索结果和对话历史拼成一个新的Prompt,让LLM基于“上下文+新知识”精准应答。

七、LangGraph流程串接

LangGraph 让你像画流程图一样拼接对话逻辑:

from langgraph.graph import END, StateGraph, MessagesState
from langgraph.prebuilt import tools_condition

graph_builder = StateGraph(MessagesState)
graph_builder.add_node(query_or_respond)
graph_builder.add_node(tools)
graph_builder.add_node(generate)
graph_builder.set_entry_point("query_or_respond")
graph_builder.add_conditional_edges(
    "query_or_respond",
    tools_condition,
    {END: END, "tools": "tools"},
)
graph_builder.add_edge("tools", "generate")
graph_builder.add_edge("generate", END)

graph = graph_builder.compile()

通俗解读:

  • 入口先到 query_or_respond,判断是否需要检索。
  • 需要检索时进入 tools 节点(调用retrieve工具),再进入 generate 节点生成最终答案。
  • 不需要检索则直接结束。
from IPython.display import Image, display
display(Image(graph.get_graph().draw_mermaid_png()))

在这里插入图片描述


八、效果演示与代码逻辑说明

1. 问好(无需检索)

input_message = "hello"
for step in graph.stream(
    {"messages": [{"role": "user", "content": input_message}]},
    stream_mode="values",
):
    step["messages"][-1].pretty_print()

输出:
在这里插入图片描述

LLM直接回复,无需检索文档。

2. 知识性提问(自动检索+生成答案)

input_message = "What is Task Decomposition?"

for step in graph.stream(
    {"messages": [{"role": "user", "content": input_message}]},
    stream_mode="values",
):
    step["messages"][-1].pretty_print()

输出流程:

  • Human Message: What is Task Decomposition?
  • AI Message: 生成了工具调用请求(Tool Calls)
  • Tool Message: 检索到相关文档内容
  • AI Message: 综合检索结果给出简明专业解释
    在这里插入图片描述

九、总结与工程师Tips

  1. 多轮记忆靠消息序列封装,每条消息都是历史的“存档”
  2. 检索工具可灵活扩展,满足复杂知识库场景
  3. LangGraph让流程管理更清晰,易于维护和扩展
  4. 代码每步都可流式输出,便于调试和追踪每一个对话节点

十、下一步展望

  • 如何进一步提升RAG对话体验,如引入更智能的历史裁剪、支持多文档源、引入外部工具(如知识图谱、代码执行等)?
  • 如何结合LangSmith等工具对RAG流程进行可观测性跟踪和质量评测?

敬请期待下一篇深度剖析!你的RAG助手,从此更懂你、更懂知识库!


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Python测试之道

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值