如何在对话应用中添加聊天历史

在许多问答应用中,我们希望允许用户进行一系列的对话,这就需要应用程序有某种“记忆”来记住过去的问题和答案,并且将这些信息融入到当前的思考中。在本文中,我们重点讨论如何在对话中融入历史消息的逻辑。这实际上是《Conversational RAG教程》的精简版。我们将介绍两种实现方法:

  1. 使用 Chains:这种方法总是执行一个检索步骤。
  2. 使用 Agents:这种方法允许大型语言模型 (LLM) 自行决定是否以及如何执行一个或多个检索步骤。

我们将使用《LLM Powered Autonomous Agents》这篇博客文章作为外部知识源,这也是在 RAG 教程中使用的实例。

环境设置

依赖项

在本次演示中,我们将使用 OpenAI 的嵌入和 Chroma 向量存储,但这里展示的一切同样适用于其他任何 Embeddings、VectorStore 或 Retriever。我们将使用以下软件包:

%%capture --no-stderr
%pip install --upgrade --quiet langchain langchain-community langchain-chroma bs4

我们需要设置环境变量 OPENAI_API_KEY,可以直接设置或从 .env 文件中加载,如下所示:

import getpass
import os

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

# import dotenv
# dotenv.load_dotenv()
LangSmith

许多使用 LangChain 构建的应用程序会包含多个步骤,多次调用 LLM。随着应用程序越来越复杂,能够检查链或代理内部发生的事情变得至关重要。使用 LangSmith 是一个很好的方法。值得注意的是,LangSmith 不是必要的,但它非常有帮助。如果你想使用 LangSmith,在注册后,确保设置环境变量以开始记录踪迹:

os.environ["LANGCHAIN_TRACING_V2"] = "true"
if not os.environ.get("LANGCHAIN_API_KEY"):
    os.environ["LANGCHAIN_API_KEY"] = getpass.getpass()

Chains

在会话型 RAG 应用中,发给检索器的查询应该考虑会话的上下文。LangChain 提供了一个 create_history_aware_retriever 构造器来简化这个过程。它构建了一个链,该链接收 inputchat_history 作为输入,并具有与检索器相同的输出模式。create_history_aware_retriever 需要以下输入:

  • LLM;
  • Retriever;
  • Prompt。

首先,我们获取这些对象。

LLM

我们可以使用任何支持的聊天模型,例如 OpenAI、Anthropic、Azure 等。

pip install -qU langchain-openai

import getpass
import os

os.environ["OPENAI_API_KEY"] = getpass.getpass()

from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4o-mini")
Retriever

对于检索器,我们将使用 WebBaseLoader 来加载网页内容。这里我们实例化一个 Chroma 向量存储,然后使用其 .as_retriever 方法来构建一个可以被包含在 LCEL 链中的检索器。

import bs4
from langchain_chains import create_retrieval_chain
from langchain_chains.combine_documents import create_stuff_documents_chain
from langchain_chroma import Chroma
from langchain_community.document_loaders import WebBaseLoader
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter

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)
splits = text_splitter.split_documents(docs)
vectorstore = Chroma.from_documents(documents=splits, embedding=OpenAIEmbeddings())
retriever = vectorstore.as_retriever()
Prompt

我们将使用一个包含 MessagesPlaceholder 变量的提示,其名称为 “chat_history”。这允许我们使用 “chat_history” 输入键将一系列消息传递给提示,这些消息将插入到系统消息之后,最新用户问题之前。

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

contextualize_q_system_prompt = (
    "Given a chat history and the latest user question "
    "which might reference context in the chat history, "
    "formulate a standalone question which 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}"),
    ]
)

组装链

我们可以然后实例化历史感知检索器:

history_aware_retriever = create_history_aware_retriever(
    llm, retriever, contextualize_q_prompt
)

这个链将在我们的检索器之前添加一个重新措辞的输入查询,以便检索能够考虑会话的上下文。

接下来,我们像在 RAG 教程中一样使用 create_stuff_documents_chain 来生成一个 question_answer_chain,该链接受检索到的上下文、会话历史和查询来生成答案。

我们用 create_retrieval_chain 构建我们的最终 RAG 链。该链按顺序应用 history_aware_retrieverquestion_answer_chain,保留中间输出(如检索的上下文)以便于使用。它具有 inputchat_history 的输入键,并包括 input, chat_history, context, 和 answer 的输出。

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}"
)
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)

添加聊天历史

为了管理聊天历史,我们需要:

  1. 一个用于存储聊天历史的对象;
  2. 一个包装我们的链并管理聊天历史更新的对象。

我们将使用 BaseChatMessageHistoryRunnableWithMessageHistory。后者是 LCEL 链和 BaseChatMessageHistory 的包装器,用于在每次调用后处理聊天历史的注入和更新。

下面,我们实现了第二种选择的简单示例,其中聊天历史记录存储在一个简单的字典中。LangChain 可以通过 Redis 和其他技术进行更强大的持久化集成。

RunnableWithMessageHistory 实例为你管理聊天历史。它们接受一个配置,其中的键(默认是 “session_id”)指定要提取并附加到输入的会话历史,并将输出附加到同一会话历史。以下是一个示例:

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

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",
)

conversational_rag_chain.invoke(
    {"input": "What is Task Decomposition?"},
    config={
        "configurable": {"session_id": "abc123"}
    },  # constructs a key "abc123" in `store`.
)["answer"]

检查会话历史

你可以在存储字典中检查对话历史:

from langchain_core.messages import AIMessage

for message in store["abc123"].messages:
    if isinstance(message, AIMessage):
        prefix = "AI"
    else:
        prefix = "User"

    print(f"{prefix}: {message.content}\n")

以上的代码演示了如何在对话应用中管理聊天历史,并且展示了如何将过往对话内容与当前问题结合以保持上下文的一致性。通过这样的管理,开发者能够构建出更具互动性的会话应用。

今天的技术分享就到这里,希望对大家有帮助。开发过程中遇到问题也可以在评论区交流~

—END—

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值