为什么需要对话式RAG?
在langchain03——用langchain构建RAG应用中,我们学习了如何基于langchain构建一个RAG,此时,我们构建的RAG被称为非对话式的RAG。非对话式RAG主要用于单轮问答或简单的信息检索场景,在需要连贯、动态和个性化对话的场景中,其局限性就显得尤为突出:
- 缺乏上下文连贯性:在多轮对话中,模型无法记住之前的对话内容,导致生成的回答可能与之前的对话不连贯。
- 无法利用历史信息:无法有效地利用对话历史中的信息来增强当前回答的相关性和准确性。
- 缺乏反思能力:无法根据之前的回答效果进行自我反思和改进,难以提供更高质量的对话体验。
- 重复信息:可能会重复生成已经提供过的信息,导致用户体验不佳。
这些局限引出了对话式的RAG。对话式 RAG之所以必要,是因为它能显著提升对话系统的性能和用户体验。
在对话场景中,用户的问题往往具有连贯性和上下文依赖性,对话式 RAG 通过结合检索和生成技术,使得模型能够有效地利用外部信息源。这不仅提高了回答的准确性和相关性,还增强了对话的连贯性和一致性,从而为用户提供足够的背景信息,以便更好地理解和回应后续的提问。
1. 构建一个非对话式的RAG
由于对话式RAG是在非对话式的RAG基础上构建的,所以我们先构建非对话式RAG:
(1)加载数据
首先使用 WebBaseLoader
从网页加载数据。WebBaseLoader
的原理就是利用urllib加载html页面,然后通过BeautifulSoup进行Html解析,提取出其中的内容
# 1. 加载数据
from langchain_community.document_loaders import WebBaseLoader
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()
在 WebBaseLoader
中,bs_kwargs
是一个参数,用于向 BeautifulSoup 提供额外的关键字参数,自定义网页解析的行为。
parse_only
是 BeautifulSoup 的一个参数,用来指定一个SoupStrainer
对象。bs4.SoupStrainer
是 BeautifulSoup 中的一个类,用于创建一个过滤器,指定在解析 HTML
时只关注某些特定的元素。它可以帮助我们限制解析的范围,只提取网页中我们感兴趣的部分,从而提高效率并减少不必要的数据处理。- 在
SoupStrainer
的构造函数中,class_=("post-content", "post-title", "post-header")
意味着 BeautifulSoup 在解析网页时,只会处理网页中标记文章内容、标题和页眉的标识符。
通过指定 bs_kwargs
参数,仅解析具有特定类名的 HTML 元素(如文章内容、标题、页眉),过滤掉无关信息,提高加载效率和相关性。
(2)分割数据
利用 RecursiveCharacterTextSplitter
将加载的长文本分割成较小的文本块(chunk)。设置 chunk_size=1000
表示每个文本块大约包含 1000 个字符,chunk_overlap=200
允许相邻文本块之间有 200 个字符的重叠,确保信息的连贯性和完整性。
# 2. 分割数据
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
splits = text_splitter.split_documents(docs)
(3)嵌入数据
使用 OllamaEmbeddings
将文本块转化为高维向量。Ollama 是一个本地部署的模型服务平台,这里使用 bge-m3
模型生成嵌入向量。
如果没有下载bge-m3
模型,先在cmd中执行下方命令下载模型。
ollama pull bge-m3
将嵌入后的文本块存储到 Chroma 向量数据库中,便于后续的快速相似性搜索。
from langchain_ollama import OllamaEmbeddings
from langchain_community.vectorstores import Chroma
embedding = OllamaEmbeddings(
base_url="http://localhost:11434",
model="bge-m3"
)
vectorstore = Chroma.from_documents(documents=splits, embedding=embedding)
(4)检索
创建检索器 retriever,用于根据用户问题从向量数据库中检索出最相关的文本块。检索器利用嵌入向量计算用户问题与文本块之间的相似度,快速定位相关上下文。
# 4. 检索
retriever = vectorstore.as_retriever()
(5)生成
初始化 ChatOllama 作为语言模型(LLM),使用 deepseek-r1:8b
模型进行文本生成。
定义 prompt 模板,明确指示 LLM 使用检索到的上下文来回答问题,限制回答长度(最多三句话),确保回答简洁且基于检索到的上下文。
# 5. 生成
from langchain_ollama import ChatOllama
# 修改 LLM 初始化
llm = ChatOllama(
model="deepseek-r1:8b",
base_url="http://localhost:11434",
)
# 定义prompt
prompt_template = """
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.
context: {context}
question: {input}
answer:"""
from langchain_core.prompts import PromptTemplate
prompt = PromptTemplate(
template=prompt_template,
input_variables=["context", "input"]
)
(6)构建 RAG
构建RAG中,有两种方式:使用 ICEL 链和使用函数构建,下面分别介绍这两种实现方式:
使用 ICEL 链构建 RAG:
format_docs
函数将检索到的文档对象列表转换为字符串格式,每个文档的内容之间用两个换行符分隔。
{"context": retriever | format_docs, "input": RunnablePassthrough()}
定义一个字典,其中 context
键对应的值是通过检索器 retriever
检索到的文档,并经过 format_docs
函数格式化;input
键对应的值通过 RunnablePassthrough
传递,不进行任何处理。
| prompt
将前面生成的字典传递给提示模板 prompt,生成最终的提示内容。
| llm
将生成的提示内容传递给语言模型 llm,生成回答。
| StrOutputParser()
将语言模型的输出解析为字符串。
最后,通过 rag_chain.stream
方法执行查询,并实时打印生成的回答内容。
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
# 创建 RAG 链
from langchain.schema.runnable import RunnablePassthrough
from langchain.schema.output_parser import StrOutputParser
rag_chain = (
{
"context": retriever | format_docs,
"input": RunnablePassthrough()
}
| prompt
| llm
| StrOutputParser()
)
# 执行查询
for chunk in rag_chain.stream("What is Task Decomposition?"):
print(chunk, end="", flush=True)
使用函数构建 RAG:
create_stuff_documents_chain
:定义如何将检索到的文档块与 LLM 的 prompt 结合起来。
create_retrieval_chain
:将检索器与文档处理链组合成一个完整的 RAG 流程。
调用 rag_chain.invoke
方法,传入用户问题,触发整个 RAG 流程:
- 检索器根据问题检索相关文档。
- 将检索到的文档与问题合并到 prompt 中。
- LLM 根据 prompt 生成回答。
from langchain.chains import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
question_answer_chain = create_stuff_documents_chain(llm, prompt)
rag_chain = create_retrieval_chain(retriever, question_answer_chain)
response = rag_chain.invoke({"input": "What is Task Decomposition?"})
print(response["answer"])
非对话式RAG完整代码
可以用ICEL链来生成回答或者使用便捷的内置链函数,这里同时实现了两种方法,使用ICEL链的方法注释了,可以自行解除注释来运行
# 1. 加载数据
from langchain_community.document_loaders import WebBaseLoader
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()
# 2. 分割数据
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
splits = text_splitter.split_documents(docs)
# 3. 嵌入数据
from langchain_ollama import OllamaEmbeddings
from langchain_community.vectorstores import Chroma
embedding = OllamaEmbeddings(
base_url="http://localhost:11434",
model="bge-m3"
)
vectorstore = Chroma.from_documents(documents=splits, embedding=embedding)
# 4. 检索
retriever = vectorstore.as_retriever()
# 5. 生成
from langchain_ollama import ChatOllama
# 修改 LLM 初始化
llm = ChatOllama(
model="deepseek-r1:8b",
base_url="http://localhost:11434",
)
# 定义prompt
prompt_template = """
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.
context: {context}
question: {input}
answer:"""
from langchain_core.prompts import PromptTemplate
prompt = PromptTemplate(
template=prompt_template,
input_variables=["context", "input"]
)
# def format_docs(docs):
# return "\n\n".join(doc.page_content for doc in docs)
# # 创建 RAG 链
# from langchain.schema.runnable import RunnablePassthrough
# from langchain.schema.output_parser import StrOutputParser
# rag_chain = (
# {"context": retriever | format_docs, "input": RunnablePassthrough()}
# | prompt
# | llm
# | StrOutputParser()
# )
# # 执行查询
# for chunk in rag_chain.stream("What is Task Decomposition?"):
# print(chunk, end="", flush=True)
from langchain.chains import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
question_answer_chain = create_stuff_documents_chain(llm, prompt)
rag_chain = create_retrieval_chain(retriever, question_answer_chain)
response = rag_chain.invoke({"input": "What is Task Decomposition?"})
print(response["answer"])
2.构建一个对话式RAG
接下来,我们将介绍构建对话式RAG的两种方法:
- 链接:检索步骤是固定执行的,每次生成回答时都会先进行检索,然后将检索结果作为上下文的一部分传递给模型。模型在这种情况下没有自主决定是否进行检索的灵活性。
- 代理:智能体可以根据对话的具体情况自主决定是否进行检索,这种灵活性使得代理方法更适合复杂的对话场景,能够更有效地利用检索工具来增强对话质量。
构建对话式RAG时,加载数据、分割数据、嵌入数据和生成模型定义与构建部分和构建非对话式RAG一致,这里不再赘述。
链接
上下文感知的检索
ChatPromptTemplate
是用于构建对话式 Prompt 模板的工具。它允许开发者定义一个包含多种消息类型的模板,从而形成一个完整的对话上下文,用以指导语言模型生成合适的回答。在构建对话系统时,它非常有用,因为它可以管理多轮对话的上下文,并基于历史对话生成连贯的回答。
在 ChatPromptTemplate
中可以使用 SystemMessage
来定义系统消息,它为 AI 助手提供上下文信息和行为指导。例如,可以通过 SystemMessage
设定 AI 助手的角色、行为准则以及回答风格,帮助 AI 更好地理解问题和生成合适的回应。
而 HumanMessage
则用于插入用户在对话中的输入。这部分表示用户的问题或发言,提示 AI 助手需要对此进行回应。
MessagesPlaceholder
作为一个占位符,用于在模板中预留位置以插入对话历史。这在多轮对话中非常重要,因为它能够保持上下文的连贯性。每次生成新的 Prompt 时,都可以动态地将之前的对话历史填充到这个占位符中,让 AI 助手能够参考之前的对话内容,从而生成更连贯的回答。
variable_name="chat_history"
是在MessagesPlaceholder
中指定的一个变量名,用于存储和引用对话历史。这个变量保存了多轮对话中的消息记录,能让AI助手了解之前的对话内容,从而生成连贯的回答。chat_history
的格式通常是一个消息列表,列表中的每个元素代表一条消息,可以是用户消息HumanMessage
、AI消息AIMessage
等。
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
# 创建上下文感知问题重构提示
contextualize_q_system_prompt = (
"Given the chat history and the latest user question "
"(which might reference context from the chat history), "
"formulate a standalone question that can be understood "
"without the chat history. Do NOT answer the question, "
"just reformulate it if needed and otherwise return it as is."
)
contextualize_q_prompt = ChatPromptTemplate.from_messages([
SystemMessage(content= contextualize_q_system_prompt),
MessagesPlaceholder(variable_name="chat_history"),
HumanMessage(content="{input}"),
])
create_history_aware_retriever
是 LangChain 中的一个函数,用于创建一个上下文感知检索器。这个检索器在检索相关文档之前,会先利用语言模型根据对话历史对用户的问题进行重新构建,使其能够脱离对话历史独立存在。这有助于提高检索的准确性和相关性,因为重新构建后的问题更加明确和完整。
上面构建的contextualize_q_prompt
是用于指导语言模型重新构建问题的提示模板。这个提示模板通常包含对话历史的占位符和用户输入的占位符。
当用户提出一个问题时,create_history_aware_retriever
会先使用提供的语言模型和提示模板,根据对话历史重新构建问题。这一步的目的是生成一个能够脱离对话历史独立理解的问题。然后,使用基础检索器retriever
根据重新构建后的问题检索相关文档。最后,检索器返回与问题相关的文档,这些文档将用于后续的回答生成。
# 创建上下文感知检索器
from langchain.chains import create_history_aware_retriever
history_aware_retriever = create_history_aware_retriever(
llm, retriever, contextualize_q_prompt
)
然后,构建回答问题的提示模板
# 创建回答问题的提示模板
qa_system_prompt = (
"You are an assistant for question-answering tasks. "
"Use the following retrieved context to answer the question. "
"If you don't know the answer, just say you don't know. "
"Use three sentences maximum and keep the answer concise."
"\n\n"
"{context}"
)
qa_prompt = ChatPromptTemplate.from_messages([
("system", qa_system_prompt),
MessagesPlaceholder("chat_history"),
("human", "{input}"),
])
create_stuff_documents_chain
创建了一个简单的链式结构,将所有检索到的文档组合在一起,并将其与用户的问题一起传递给语言模型,以生成回答。它适用于文档数量较少的情况,可以将所有文档作为一个整体进行处理。
create_retrieval_chain
是构建 RAG 系统的关键工具,能够将检索和生成结合起来,形成一个完整的问答流程:先检索相关文档,再基于这些文档生成回答。
流程:
- 文档检索:使用提供的检索器根据用户问题检索相关文档。
- 文档处理:将检索到的文档传递给文档链。
- 回答生成:文档链结合文档和问题生成回答。
# 创建检索问答链
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain.chains import create_retrieval_chain
qa_chain = create_stuff_documents_chain(llm, qa_prompt)
rag_chain = create_retrieval_chain(history_aware_retriever, qa_chain)
get_session_history
函数用于获取指定 session_id
的对话历史。如果 session_id
不在 store
中,就在 store
中为该 session_id
创建一个新的 ChatMessageHistory
对象。最后返回与 session_id
对应的 ChatMessageHistory
对象。
最后创建带会话历史的RAG链conversational_rag_chain
:
rag_chain
:之前创建的检索增强生成链,用于根据检索到的文档和问题生成回答。get_session_history
:用于获取会话历史的函数,使得链能够访问和更新会话历史。input_messages_key
、history_messages_key
和output_messages_key
参数分别指定了输入消息、历史消息和输出消息在链中的键名,以便链能够在处理过程中正确地识别和使用这些消息。
# 创建会话存储
store = {}
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.chat_history import BaseChatMessageHistory
def get_session_history(session_id: str) -> BaseChatMessageHistory:
if session_id not in store:
store[session_id] = ChatMessageHistory()
return store[session_id]
# 创建带会话历史的RAG链
from langchain_core.runnables.history import RunnableWithMessageHistory
conversational_rag_chain = RunnableWithMessageHistory(
rag_chain,
get_session_history,
input_messages_key="input",
history_messages_key="chat_history",
output_messages_key="answer",
)
代理
用MemorySaver
在会话线程中存储对话的状态。在后台,LangGraph 每一步都会保存检查点。这些检查点与模拟会话的线程 ID 相连。它在工作流中作为中间件介入,捕获并加工数据流中的数据,对用户透明地进行数据的保存和恢复
# 初始化内存
from langgraph.checkpoint.memory import MemorySaver
memory = MemorySaver()
用create_retriever_tool
函数将检索器封装为一个工具,以便在对话系统或其他应用中使用。这个工具可以被Agent调用,以检索与用户问题相关的文档或信息。
在创建工具后,可以将工具列表传递给Agent,使其能够调用这些工具来解决用户的问题。
from langgraph.prebuilt import create_react_agent
from langchain.tools.retriever import create_retriever_tool
# 创建检索工具
retriever_tool = create_retriever_tool(
retriever,
"blog_post_retriever",
"Searches and returns excerpts from the Autonomous Agents blog post.",
)
tools = [retriever_tool]
用ChatPromptTemplate
创建prompt:
# 定义自定义 system prompt
system_prompt = (
"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"
"{context}"
)
# 创建代理prompt
agent_prompt = ChatPromptTemplate.from_messages([
("system", system_prompt),
("human", "{input}"),
])
最后,用create_react_agent
创建能够结合推理和行动的智能代理。它在需要调用外部工具并进行多步推理的任务中特别有用。
流程:
- 推理和行动:智能代理根据用户的问题生成推理步骤和行动计划。
- 工具调用:根据行动计划调用相应的工具,获取中间结果。
- 状态更新:使用
checkpointer
保存智能代理的状态,以便在后续步骤中恢复和继续推理。 - 结果生成:结合推理步骤和工具结果生成最终的回答。
# 创建 ReAct 代理
agent_executor = create_react_agent(
llm,
tools,
prompt=agent_prompt,
checkpointer=memory
)
完整代码
# 1. 加载数据
from langchain_community.document_loaders import WebBaseLoader
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()
# 2. 分割数据
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
splits = text_splitter.split_documents(docs)
# 3. 嵌入数据
from langchain_ollama import OllamaEmbeddings
from langchain_community.vectorstores import Chroma
embedding = OllamaEmbeddings(
base_url="http://localhost:11434",
model="bge-m3"
)
vectorstore = Chroma.from_documents(documents=splits, embedding=embedding)
# 4. 检索
retriever = vectorstore.as_retriever()
# 5. 生成
from langchain_ollama import ChatOllama
# 初始化 LLM
llm = ChatOllama(
model="deepseek-r1:8b",
base_url="http://localhost:11434",
)
# 6. 构建对话式RAG - 方法一:链接方式 (使用上下文感知检索器)
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
# 创建上下文感知问题重构提示
contextualize_q_system_prompt = (
"Given the chat history and the latest user question "
"(which might reference context from the chat history), "
"formulate a standalone question that can be understood "
"without the chat history. Do NOT answer the question, "
"just reformulate it if needed and otherwise return it as is."
)
contextualize_q_prompt = ChatPromptTemplate.from_messages([
("system", contextualize_q_system_prompt),
MessagesPlaceholder("chat_history"),
("human", "{input}"),
])
from langchain.chains import create_history_aware_retriever
# 创建上下文感知检索器
history_aware_retriever = create_history_aware_retriever(
llm, retriever, contextualize_q_prompt
)
# 创建回答问题的提示模板
qa_system_prompt = (
"You are an assistant for question-answering tasks. "
"Use the following retrieved context to answer the question. "
"If you don't know the answer, just say you don't know. "
"Use three sentences maximum and keep the answer concise."
"\n\n"
"{context}"
)
qa_prompt = ChatPromptTemplate.from_messages([
("system", qa_system_prompt),
MessagesPlaceholder("chat_history"),
("human", "{input}"),
])
# 创建问答链
from langchain.chains.combine_documents import create_stuff_documents_chain
qa_chain = create_stuff_documents_chain(llm, qa_prompt)
# 创建检索链
from langchain.chains import create_retrieval_chain
rag_chain = create_retrieval_chain(history_aware_retriever, qa_chain)
# 创建会话存储
store = {}
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.chat_history import BaseChatMessageHistory
def get_session_history(session_id: str) -> BaseChatMessageHistory:
if session_id not in store:
store[session_id] = ChatMessageHistory()
return store[session_id]
# 创建带会话历史的RAG链
from langchain_core.runnables.history import RunnableWithMessageHistory
conversational_rag_chain = RunnableWithMessageHistory(
rag_chain,
get_session_history,
input_messages_key="input",
history_messages_key="chat_history",
output_messages_key="answer",
)
# 7. 构建对话式RAG - 方法二:代理方式
from langgraph.prebuilt import create_react_agent
from langchain.tools.retriever import create_retriever_tool
# 创建检索工具
retriever_tool = create_retriever_tool(
retriever,
"blog_post_retriever",
"Searches and returns excerpts from the Autonomous Agents blog post.",
)
tools = [retriever_tool]
# 定义自定义 system prompt
system_prompt = (
"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"
"{context}"
)
# 创建代理prompt
agent_prompt = ChatPromptTemplate.from_messages([
("system", system_prompt),
("human", "{input}"),
])
# 初始化内存
from langgraph.checkpoint.memory import MemorySaver
memory = MemorySaver()
# 创建 ReAct 代理
agent_executor = create_react_agent(
llm,
tools,
prompt=agent_prompt,
checkpointer=memory
)
# 8. 测试对话式RAG
def rag_chain():
print("\n=== Conversational RAG using Chain Method with Context-Aware Retriever ===")
session_id = "user-123"
response = conversational_rag_chain.invoke(
{"input": "What is Task Decomposition?"},
config={"configurable": {"session_id": session_id}}
)
print("Question: What is Task Decomposition?")
print("Answer:", response["answer"])
response = conversational_rag_chain.invoke(
{"input": "What is its relationship with ReAct?"},
config={"configurable": {"session_id": session_id}}
)
print("\nQuestion: What is its relationship with ReAct?")
print("Answer:", response["answer"])
def rag_agent():
print("\n=== Conversational RAG using Agent Method ===")
response = agent_executor.invoke({
"input": "What is Task Decomposition?"
})
print("Question: What is Task Decomposition?")
print("Answer:", response["output"])
response = agent_executor.invoke({
"input": "What is its relationship with ReAct?"
})
print("\nQuestion: What is its relationship with ReAct?")
print("Answer:", response["output"])
# Demo runner
if __name__ == "__main__":
print("Select demo method:")
print("1. Chain Method")
print("2. Agent Method")
print("3. Both Methods")
choice = input("Enter your choice (1/2/3): ")
if choice == "1":
rag_chain()
elif choice == "2":
rag_agent()
elif choice == "3":
rag_chain()
rag_agent()
else:
print("Invalid choice, defaulting to Chain Method")
rag_chain()