在许多问答应用中,我们希望允许用户进行一系列的对话,这就需要应用程序有某种“记忆”来记住过去的问题和答案,并且将这些信息融入到当前的思考中。在本文中,我们重点讨论如何在对话中融入历史消息的逻辑。这实际上是《Conversational RAG教程》的精简版。我们将介绍两种实现方法:
- 使用 Chains:这种方法总是执行一个检索步骤。
- 使用 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
构造器来简化这个过程。它构建了一个链,该链接收 input
和 chat_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_retriever
和 question_answer_chain
,保留中间输出(如检索的上下文)以便于使用。它具有 input
和 chat_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)
添加聊天历史
为了管理聊天历史,我们需要:
- 一个用于存储聊天历史的对象;
- 一个包装我们的链并管理聊天历史更新的对象。
我们将使用 BaseChatMessageHistory
和 RunnableWithMessageHistory
。后者是 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—