在上一篇中,我们实现了最基础的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
- 多轮记忆靠消息序列封装,每条消息都是历史的“存档”
- 检索工具可灵活扩展,满足复杂知识库场景
- LangGraph让流程管理更清晰,易于维护和扩展
- 代码每步都可流式输出,便于调试和追踪每一个对话节点
十、下一步展望
- 如何进一步提升RAG对话体验,如引入更智能的历史裁剪、支持多文档源、引入外部工具(如知识图谱、代码执行等)?
- 如何结合LangSmith等工具对RAG流程进行可观测性跟踪和质量评测?
敬请期待下一篇深度剖析!你的RAG助手,从此更懂你、更懂知识库!