LangChain教程 - 构建对话式检索增强生成(Conversational RAG)

系列文章索引
LangChain教程 - 系列文章

在许多问答应用中,我们希望允许用户进行多轮对话,这需要应用具备某种形式的“记忆”,以便能够在当前回答中整合过去的问题和答案。本教程将介绍如何通过两种方法实现这一目标:

  1. Chains(链式方法):每次执行检索步骤。
  2. Agents(智能体方法):赋予大语言模型(LLM)判断力,让它决定是否执行检索步骤或多个步骤。

前提条件

本指南假定您已经熟悉以下概念:

  • 聊天历史
  • 聊天模型
  • 嵌入向量(Embeddings)
  • 向量存储
  • 检索增强生成(RAG)
  • 工具和智能体

环境设置

我们将使用 OpenAI 嵌入向量和 Chroma 向量存储库。以下是所需的依赖包:

pip install --upgrade langchain langchain-community langchainhub langchain-chroma beautifulsoup4
设置 API 密钥

我们需要设置 OPENAI_API_KEY 环境变量,可以直接手动输入或者从 .env 文件中加载:

import getpass
import os

if not os.environ.get("OPENAI_API_KEY"):
    os.environ["OPENAI_API_KEY"] = getpass.getpass()

基础流程:构建检索增强生成(RAG)

在本节中,我们将构建一个基础的 RAG 系统,它从外部博客文章中检索信息并基于检索结果生成答案。

加载文档并构建检索器

首先,我们需要加载文档并将其拆分成较小的片段,以便可以在需要时进行高效检索。然后,我们将使用 OpenAI 嵌入向量和 Chroma 向量存储库创建一个检索器。

from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import WebBaseLoader

import os

os.environ['USER_AGENT'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'

# 1. 加载博客文章
loader = WebBaseLoader(
    web_paths=("https://lilianweng.github.io/posts/2023-06-23-agent/",),
)
docs = loader.load()

# 2. 文本拆分
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
splits = text_splitter.split_documents(docs)

# 3. 构建向量存储库并创建检索器
vectorstore = Chroma.from_documents(documents=splits, embedding=OpenAIEmbeddings())
retriever = vectorstore.as_retriever()
构建问答链

接下来,我们将构建一个基于检索结果的问答链。该问答链使用预定义的系统提示,基于检索的上下文回答用户的问题。

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain.chains import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain

# 4. 使用 GPT-4 模型生成回答
llm = ChatOpenAI(model="gpt-4")

# 5. 定义系统提示
system_prompt = (
    "你是一个帮助用户回答问题的助手。使用以下检索到的内容作为答案的依据。如果你不知道答案,就说你不知道。请用三句话简洁作答。"
    "\n\n"
    "{context}"
)

prompt = ChatPromptTemplate.from_messages(
    [
        ("system", system_prompt),
        ("human", "{input}"),
    ]
)

# 6. 构建问答链
question_answer_chain = create_stuff_documents_chain(llm, prompt)
rag_chain = create_retrieval_chain(retriever, question_answer_chain)

现在,我们有了一个基础的 RAG 系统,它可以从外部文档中检索信息并生成答案。下一步是增加对话历史支持。

增加聊天历史支持

在多轮对话中,用户的后续问题可能依赖于先前的上下文。为了让系统能够更好地理解这些问题,我们需要在问答链中整合聊天历史。

创建支持历史的检索器

为了支持聊天历史,我们需要创建一个特殊的检索器,它能将历史消息和当前用户问题结合起来,重新表述问题,并根据这个重构后的问题进行检索。我们使用 create_history_aware_retriever 来实现这一点。

from langchain.chains import create_history_aware_retriever
from langchain_core.prompts import MessagesPlaceholder

# 7. 定义用于重构问题的提示
contextualize_q_system_prompt = (
    "根据聊天历史和最新的用户问题,如果问题涉及历史信息,请将其重新表述成独立的问题。"
)

contextualize_q_prompt = ChatPromptTemplate.from_messages(
    [
        ("system", contextualize_q_system_prompt),
        MessagesPlaceholder("chat_history"),
        ("human", "{input}"),
    ]
)

# 8. 创建历史感知检索器
history_aware_retriever = create_history_aware_retriever(
    llm, retriever, contextualize_q_prompt
)
构建包含聊天历史支持的问答链

在此步骤中,我们将使用 history_aware_retriever 替换原来的 retriever,这样系统就能够利用聊天历史生成更加精准的回答。

# 9. 构建问答链
rag_chain = create_retrieval_chain(history_aware_retriever, question_answer_chain)

现在,我们的问答链能够利用聊天历史重新表述问题,从而更好地理解用户的多轮对话。

状态化管理聊天历史

在实际应用中,您需要一种方式来管理聊天历史。我们可以使用 LangChain 的 RunnableWithMessageHistory 类,帮助自动更新聊天历史并在每次调用时传递给模型。

使用 RunnableWithMessageHistory 管理聊天历史

我们通过 ChatMessageHistory 来存储聊天历史,并使用 RunnableWithMessageHistory 自动管理和更新它。

from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory

# 10. 创建一个字典来存储聊天历史
store = {}

def get_session_history(session_id: str) -> BaseChatMessageHistory:
    if session_id not in store:
        store[session_id] = ChatMessageHistory()
    return store[session_id]

# 11. 构建支持聊天历史的问答链
conversational_rag_chain = RunnableWithMessageHistory(
    rag_chain,
    get_session_history,
    input_messages_key="input",
    history_messages_key="chat_history",
    output_messages_key="answer",
)

通过这种方式,系统会自动管理聊天历史,并在每次生成回答时传递相关的历史记录。

测试多轮对话功能

最后,我们可以测试整个系统。在以下示例中,系统会先回答一个问题,然后基于聊天历史回答后续问题。

# 定义会话历史
chat_history = []

# 第一个问题
question = "什么是任务分解?"
ai_msg_1 = conversational_rag_chain.invoke(
    {"input": question, "chat_history": chat_history},
    config={"configurable": {"session_id": "session_abc123"}}  # 指定 session_id
)
chat_history.extend(
    [
        HumanMessage(content=question),
        AIMessage(content=ai_msg_1["answer"]),
    ]
)

# 第二个问题
second_question = "有哪些常见的分解方法?"
ai_msg_2 = conversational_rag_chain.invoke(
    {"input": second_question, "chat_history": chat_history},
    config={"configurable": {"session_id": "session_abc123"}}  # 指定相同的 session_id
)

print(ai_msg_2["answer"])

完整代码实例

import os
import getpass
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_chroma import Chroma
from langchain_community.document_loaders import WebBaseLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.chains import create_retrieval_chain, create_history_aware_retriever
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.messages import AIMessage, HumanMessage

# 设置 API 密钥
os.environ["OPENAI_API_KEY"] = getpass.getpass("请输入您的 OpenAI API 密钥: ")

# Step 1: 构建检索器
# 加载博客文章并分割为可检索的块
loader = WebBaseLoader(
    web_paths=("https://lilianweng.github.io/posts/2023-06-23-agent/",)
)
docs = loader.load()

# 分割文章内容,创建向量存储
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
splits = text_splitter.split_documents(docs)
vectorstore = Chroma.from_documents(documents=splits, embedding=OpenAIEmbeddings())
retriever = vectorstore.as_retriever()

# Step 2: 创建包含聊天历史支持的检索器
contextualize_q_system_prompt = (
    "根据聊天历史和最新的用户问题,如果问题涉及历史信息,请将其重新表述成独立的问题。"
)

contextualize_q_prompt = ChatPromptTemplate.from_messages(
    [
        ("system", contextualize_q_system_prompt),
        MessagesPlaceholder("chat_history"),
        ("human", "{input}"),
    ]
)
llm = ChatOpenAI(model="gpt-4")

history_aware_retriever = create_history_aware_retriever(
    llm, retriever, contextualize_q_prompt
)

# Step 3: 创建问答链
system_prompt = (
    "你是一个帮助用户回答问题的助手。使用以下检索到的内容作为答案的依据。"
    "如果你不知道答案,就说你不知道。请用三句话简洁作答。"
    "\n\n"
    "{context}"
)

qa_prompt = ChatPromptTemplate.from_messages(
    [
        ("system", system_prompt),
        MessagesPlaceholder("chat_history"),
        ("human", "{input}"),
    ]
)

question_answer_chain = create_stuff_documents_chain(llm, qa_prompt)

rag_chain = create_retrieval_chain(history_aware_retriever, question_answer_chain)

# Step 4: 管理聊天历史
store = {}

def get_session_history(session_id: str) -> BaseChatMessageHistory:
    if session_id not in store:
        store[session_id] = ChatMessageHistory()
    return store[session_id]

conversational_rag_chain = RunnableWithMessageHistory(
    rag_chain,
    get_session_history,
    input_messages_key="input",
    history_messages_key="chat_history",
    output_messages_key="answer",
)

# Step 5: 模拟对话并添加聊天历史
session_id = "abc123"
chat_history = get_session_history(session_id)

# 问题 1
question = "什么是任务分解?"
ai_msg_1 = conversational_rag_chain.invoke(
    {"input": question},
    config={"configurable": {"session_id": "abc123"}}  # 指定 session_id
)
print(f"AI: {ai_msg_1['answer']}")

# 问题 2(基于上下文的后续问题)
second_question = "有哪些常见的分解方法?"
ai_msg_2 = conversational_rag_chain.invoke(
    {"input": second_question},
    config={"configurable": {"session_id": "abc123"}}  # 指定 session_id
)
print(f"AI: {ai_msg_2['answer']}")

# Step 6: 查看整个聊天历史
print("\n聊天历史:")
for message in chat_history.messages:
    if isinstance(message, AIMessage):
        prefix = "AI"
    else:
        prefix = "User"
    print(f"{prefix}: {message.content}\n")

总结

通过本教程,您学习了如何使用 LangChain 构建一个对话式的检索增强生成系统。我们详细介绍了如何加载文档、创建检索器、构建问答链,并在系统中加入聊天历史支持。最后,我们使用 RunnableWithMessageHistory 实现了对聊天历史的状态化管理,使系统能够自动处理多轮对话。

通过这些步骤,您可以创建一个支持复杂对话和上下文处理的智能系统,用于回答用户问题并提供精准的内容检索和生成功能。

附1:代码实例 LLM 调用解析

在构建支持聊天历史的 Conversational RAG 系统中,LLM(大语言模型)的调用贯穿了问题的重构和答案的生成过程。为了清楚理解 LLM 的调用时机及作用,我们可以将整个过程分为以下四个步骤,详细解析每次 LLM 的调用。

1. 第一次调用:格式化第一个问题

当用户提出第一个问题(例如:"什么是任务分解?")时,系统首先会调用 history_aware_retriever 进行问题的重新格式化。这是为了确保当前问题在检索前是完整的、独立的问题,即使没有任何历史消息也可以理解。

位置

history_aware_retriever = create_history_aware_retriever(
    llm, retriever, contextualize_q_prompt
)

在此过程中,LLM 被调用来基于当前的聊天历史生成完整的问题格式。这是整个流程中的第一次 LLM 调用

2. 第二次调用:生成第一个问题的答案

在第一个问题被重新格式化并提交给检索器后,系统会调用 LLM 来基于检索到的相关文档生成答案。LLM 根据系统提示,结合检索到的内容,为第一个问题生成简洁的回答。

位置

ai_msg_1 = conversational_rag_chain.invoke({"input": question, "chat_history": chat_history})

这一步是 LLM 的第二次调用,负责根据检索结果为第一个问题生成具体的答案。

3. 第三次调用:格式化第二个问题

当用户提出第二个问题(例如:"有哪些常见的分解方法?")时,系统会再次调用 history_aware_retriever。这次,LLM 会根据历史消息和当前用户的问题,重新格式化这个问题,使其在检索时可以被正确理解。例如,这个问题的“它”可能指代前面的“任务分解”。

位置

history_aware_retriever = create_history_aware_retriever(
    llm, retriever, contextualize_q_prompt
)

这是 LLM 的第三次调用,它帮助将第二个问题重新构造成完整、独立的问题,避免历史上下文的混淆。

4. 第四次调用:生成第二个问题的答案

在第二个问题被重新格式化并提交检索后,系统再次调用 LLM,基于检索结果生成针对该问题的答案。LLM 会根据系统的提示和检索到的上下文,为第二个问题提供具体的回答。

位置

ai_msg_2 = conversational_rag_chain.invoke({"input": second_question, "chat_history": chat_history})

这是 LLM 的第四次调用,负责生成第二个问题的答案。

LLM 调用总结

在这个支持聊天历史的 Conversational RAG 系统中,LLM 总共被调用了 4 次

  1. 第一次 LLM 调用:通过 history_aware_retriever 格式化第一个问题,确保其独立于聊天历史。
  2. 第二次 LLM 调用:生成第一个问题的答案,基于检索结果返回信息。
  3. 第三次 LLM 调用:通过 history_aware_retriever 格式化第二个问题,结合历史上下文重新构建问题。
  4. 第四次 LLM 调用:生成第二个问题的答案,基于检索到的内容提供解答。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值