一、消息存储在内存
下面我们展示一个简单的示例,其中聊天历史保存在内存中,此处通过全局 Python 字典实现。
我们构建一个名为 get_session_history
的可调用对象,引用此字典以返回 ChatMessageHistory
实例。通过在运行时向 RunnableWithMessageHistory
传递配置,可以指定可调用对象的参数。默认情况下,期望配置参数是一个字符串 session_id
。可以通过 history_factory_config
关键字参数进行调整。
使用单参数默认值:
ini
#chat_history_memory.pyfrom 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]
with_message_history = RunnableWithMessageHistory(
runnable,
get_session_history,
input_messages_key="input",
history_messages_key="history",
)
请注意,我们已指定了 input_messages_key
(要视为最新输入消息的键)和 history_messages_key
(要添加历史消息的键)。
在调用此新 Runnable 时,我们通过配置参数指定相应的聊天历史:
arduino
with_message_history.invoke(
{"ability": "math", "input": "余弦是什么意思?"},
config={"configurable": {"session_id": "abc123"}},
)
python
content='余弦是一个数学函数,通常在三角学中使用,表示直角三角形的邻边和斜边的比例。' response_metadata={'token_usage': {'completion_tokens': 38, 'prompt_tokens': 38, 'total_tokens': 76}, 'model_name': 'gpt-4-0613', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None} id='run-9aa23716-3959-476d-9386-6d433266e060-0' usage_metadata={'input_tokens': 38, 'output_tokens': 38, 'total_tokens': 76}
arduino
# 记住
with_message_history.invoke(
{"ability": "math", "input": "什么?"},
config={"configurable": {"session_id": "abc123"}},
)
python
content='余弦是一个数学术语,用于描述直角三角形中的角度关系。' response_metadata={'token_usage': {'completion_tokens': 26, 'prompt_tokens': 88, 'total_tokens': 114}, 'model_name': 'gpt-4-0613', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None} id='run-f77baf90-6a13-4f48-991a-28e60ece84e8-0' usage_metadata={'input_tokens': 88, 'output_tokens': 26, 'total_tokens': 114}
arduino
# 新的 session_id --> 不记得了。
with_message_history.invoke(
{"ability": "math", "input": "什么?"},
config={"configurable": {"session_id": "def234"}},
)
python
content='对不起,我没明白你的问题。你能再详细一点吗?我很擅长数学。' response_metadata={'token_usage': {'completion_tokens': 34, 'prompt_tokens': 32, 'total_tokens': 66}, 'model_name': 'gpt-4-0613', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None} id='run-3f69d281-a850-452f-8055-df70d4936630-0' usage_metadata={'input_tokens': 32, 'output_tokens': 34, 'total_tokens': 66}
二、配置会话唯一键
我们可以通过向 history_factory_config
参数传递一个 ConfigurableFieldSpec
对象列表来自定义跟踪消息历史的配置参数。下面我们使用了两个参数:user_id
和 conversation_id
。
配置 user_id 和 conversation_id 作为会话唯一键:
python
from langchain_core.runnables import ConfigurableFieldSpec
store = {}
def get_session_history(user_id: str, conversation_id: str) -> BaseChatMessageHistory:if (user_id, conversation_id) not in store:
store[(user_id, conversation_id)] = ChatMessageHistory()return store[(user_id, conversation_id)]
with_message_history = RunnableWithMessageHistory(
runnable,
get_session_history,
input_messages_key="input",
history_messages_key="history",
history_factory_config=[
ConfigurableFieldSpec(id="user_id",
annotation=str,
name="User ID",
description="用户的唯一标识符。",
default="",
is_shared=True,
),
ConfigurableFieldSpec(id="conversation_id",
annotation=str,
name="Conversation ID",
description="对话的唯一标识符。",
default="",
is_shared=True,
),
],
)
with_message_history.invoke(
{"ability": "math", "input": "余弦是什么意思?"},
config={"configurable": {"user_id": "123", "conversation_id": "1"}},
)
python
content='对不起,你能提供一些更详细的信息吗?我会很高兴帮助你解决数学问题。' response_metadata={'token_usage': {'completion_tokens': 38, 'prompt_tokens': 32, 'total_tokens': 70}, 'model_name': 'gpt-4-0613', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None} id='run-02030348-7bbb-4f76-8c68-61785d012c26-0' usage_metadata={'input_tokens': 32, 'output_tokens': 38, 'total_tokens': 70}
在许多情况下,持久化对话历史是可取的。RunnableWithMessageHistory
对于 get_session_history
可调用如何检索其聊天消息历史是中立的。请参见这里 ,这是一个使用本地文件系统的示例。下面我们演示如何使用 Redis。请查看内存集成页面,以获取使用其他提供程序的聊天消息历史的实现。
三、消息持久化
请查看 memory integrations 页面,了解使用 Redis 和其他提供程序实现聊天消息历史的方法。这里我们演示使用内存中的 ChatMessageHistory
以及使用 RedisChatMessageHistory
进行更持久存储。
配置 Redis 环境
如果尚未安装 Redis,我们需要安装它:
css
%pip install --upgrade --quiet redis
如果我们没有现有的 Redis 部署可以连接,可以启动本地 Redis Stack 服务器:
arduino
docker run -d -p 6379:6379 -p 8001:8001 redis/redis-stack:latest
ini
REDIS_URL = "redis://localhost:6379/0"
调用聊天接口,看 Redis 是否存储历史记录
更新消息历史实现只需要我们定义一个新的可调用对象,这次返回一个 RedisChatMessageHistory
实例:
ini
from langchain_community.chat_message_histories import RedisChatMessageHistory
def get_message_history(session_id: str) -> RedisChatMessageHistory:return RedisChatMessageHistory(session_id, url=REDIS_URL)
with_message_history = RunnableWithMessageHistory(
runnable,
get_message_history,
input_messages_key="input",
history_messages_key="history",
)
我们可以像以前一样调用:
arduino
with_message_history.invoke(
{"ability": "math", "input": "余弦是什么意思?"},
config={"configurable": {"session_id": "foobar"}},
)
python
content='余弦是一个三角函数,它表示直角三角形的邻边长度和斜边长度的比值。' response_metadata={'token_usage': {'completion_tokens': 33, 'prompt_tokens': 38, 'total_tokens': 71}, 'model_name': 'gpt-4-0613', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None} id='run-2d1eba02-4709-4db5-ab6b-0fd03ab4c68a-0' usage_metadata={'input_tokens': 38, 'output_tokens': 33, 'total_tokens': 71}
arduino
with_message_history.invoke(
{"ability": "math", "input": "什么?"},
config={"configurable": {"session_id": "foobar"}},
)
python
content='余弦是一个数学术语,代表在一个角度下的邻边和斜边的比例。' response_metadata={'token_usage': {'completion_tokens': 32, 'prompt_tokens': 83, 'total_tokens': 115}, 'model_name': 'gpt-4-0613', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None} id='run-99368d03-c2ed-4dda-a32f-677c036ad676-0' usage_metadata={'input_tokens': 83, 'output_tokens': 32, 'total_tokens': 115}
redis 历史记录查询:
四、修改聊天历史
修改存储的聊天消息可以帮助您的聊天机器人处理各种情况。以下是一些示例:
裁剪消息
LLM 和聊天模型有限的上下文窗口,即使您没有直接达到限制,您可能也希望限制模型处理的干扰量。一种解决方案是只加载和存储最近的 n
条消息。让我们使用一个带有一些预加载消息的示例历史记录:
makefile
temp_chat_history = ChatMessageHistory()
temp_chat_history.add_user_message("我叫Jack,你好")
#chatbot_clear_history.py
temp_chat_history.add_ai_message("你好")
temp_chat_history.add_user_message("我今天心情挺开心")
temp_chat_history.add_ai_message("你今天心情怎么样")
temp_chat_history.add_user_message("我下午在打篮球")
temp_chat_history.add_ai_message("你下午在做什么")
temp_chat_history.messages
css
[HumanMessage(content='我叫Jack,你好'), AIMessage(content='你好'), HumanMessage(content='我今天心情挺开心'), AIMessage(content='你今天心情怎么样'), HumanMessage(content='我下午在打篮球'), AIMessage(content='你下午在做什么'), HumanMessage(content='我今天心情如何?'), AIMessage(content='你今天的心情很开心。')]
让我们将这个消息历史与上面声明的 RunnableWithMessageHistory
链条一起使用:
css
prompt = ChatPromptTemplate.from_messages(
[ ("system","你是一个乐于助人的助手。尽力回答所有问题。提供的聊天历史包括与您交谈的用户的事实。", ), MessagesPlaceholder(variable_name="chat_history"), ("human", "{input}"), ]
)
chain = prompt | chat
chain_with_message_history = RunnableWithMessageHistory(
chain,lambda session_id: temp_chat_history,
input_messages_key="input",
history_messages_key="chat_history",
)
chain_with_message_history.invoke(
{"input": "我今天心情如何?"},
{"configurable": {"session_id": "unused"}},
)
ini
content='你今天的心情很开心。'
我们可以看到链条记住了预加载的名字。
但是假设我们有一个非常小的上下文窗口,并且我们想要将传递给链的消息数量减少到最近的2条。我们可以使用 clear
方法来删除消息并重新将它们添加到历史记录中。我们不一定要这样做,但让我们将这个方法放在链的最前面,以确保它总是被调用:
python
from langchain_core.runnables import RunnablePassthrough
def trim_messages(chain_input):
stored_messages = temp_chat_history.messagesif len(stored_messages) <= 2:return False
temp_chat_history.clear()for message in stored_messages[-2:]:
temp_chat_history.add_message(message)return True
chain_with_trimming = (
RunnablePassthrough.assign(messages_trimmed=trim_messages)
| chain_with_message_history
)
让我们调用这个新链并检查消息:
arduino
chain_with_trimming.invoke(
{"input": "我下午在做什么?"},
{"configurable": {"session_id": "unused"}},
)
根据您之前的信息,您下午在打篮球。
temp_chat_history.messages
css
[HumanMessage(content='我下午在打篮球'), AIMessage(content='你下午在做什么'), HumanMessage(content='我下午在做什么?'), AIMessage(content='根据您之前的信息,您下午在打篮球。')]
我们可以看到我们的历史记录已经删除了两条最旧的消息,同时在末尾添加了最近的对话。下次调用链时,trim_messages
将再次被调用,只有最近的两条消息将被传递给模型。在这种情况下,这意味着下次调用时模型将忘记我们给它的名字:
arduino
chain_with_trimming.invoke(
{"input": "我叫什么名字?"},
{"configurable": {"session_id": "unused"}},
)
对不起,我无法获取这个信息,因为你还没有告诉我你的名字。
temp_chat_history.messages
css
[HumanMessage(content='我下午在打篮球'), AIMessage(content='你下午在做什么'), HumanMessage(content='我叫什么名字?'), AIMessage(content='对不起,我无法获取这个信息,因为你还没有告诉我你的名字。')]
总结记忆
我们也可以以其他方式使用相同的模式。例如,我们可以使用额外的 LLM 调用来在调用链之前生成对话摘要。让我们重新创建我们的聊天历史和聊天机器人链:
scss
temp_chat_history = ChatMessageHistory()
temp_chat_history.add_user_message("我叫Jack,你好")
temp_chat_history.add_ai_message("你好")
temp_chat_history.add_user_message("我今天心情挺开心")
temp_chat_history.add_ai_message("你今天心情怎么样")
temp_chat_history.add_user_message("我下午在打篮球")
temp_chat_history.add_ai_message("你下午在做什么")
temp_chat_history.messages
css
[HumanMessage(content='我叫Jack,你好'), AIMessage(content='你好'), HumanMessage(content='我今天心情挺开心'), AIMessage(content='你今天心情怎么样'), HumanMessage(content='我下午在打篮球'), AIMessage(content='你下午在做什么'), HumanMessage(content='我今天心情如何?'), AIMessage(content='作为一个人工智能,我无法知道你的心情。你可以告诉我你今天感觉如何,我会尽我所能提供帮助。')]
我们将稍微修改提示,让 LLM 意识到它将收到一个简短摘要而不是聊天历史:
ini
prompt = ChatPromptTemplate.from_messages(
[
("system","你是一个乐于助人的助手。尽力回答所有问题。提供的聊天历史包括与您交谈的用户的事实。",
),
MessagesPlaceholder(variable_name="chat_history"),
("user", "{input}"),
]
)
chain = prompt | chat
chain_with_message_history = RunnableWithMessageHistory(
chain,lambda session_id: temp_chat_history,
input_messages_key="input",
history_messages_key="chat_history",
)
现在,让我们创建一个函数,将之前的交互总结为摘要。我们也可以将这个函数添加到链的最前面:
ini
def summarize_messages(chain_input):
stored_messages = temp_chat_history.messagesif len(stored_messages) == 0:return False
summarization_prompt = ChatPromptTemplate.from_messages(
[
MessagesPlaceholder(variable_name="chat_history"),
("user","将上述聊天消息浓缩成一条摘要消息。尽可能包含多个具体细节。",
),
]
)
summarization_chain = summarization_prompt | chat
summary_message = summarization_chain.invoke({"chat_history": stored_messages})
temp_chat_history.clear()
temp_chat_history.add_message(summary_message)return True
chain_with_summarization = (
RunnablePassthrough.assign(messages_summarized=summarize_messages)
| chain_with_message_history
)
让我们看看它是否记得我们给它起的名字:
arduino
chain_with_summarization.invoke(
{"input": "我下午在干嘛"},
{"configurable": {"session_id": "unused"}},
)
输出结果为:
下午你在打篮球。
查看聊天历史记录:
temp_chat_history.messages
输出结果为:
python
[AIMessage(content='用户Jack今天心情很好,他下午打了篮球。', response_metadata={'token_usage': {'completion_tokens': 20, 'prompt_tokens': 108, 'total_tokens': 128}, 'model_name': 'gpt-4-0613', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-3ece2bde-b763-4ca0-84f9-43cfdf5c2e5e-0', usage_metadata={'input_tokens': 108, 'output_tokens': 20, 'total_tokens': 128}), HumanMessage(content='我下午在干嘛'), AIMessage(content='下午你在打篮球。'})]
请注意,再次调用链式模型会生成一个新的摘要,该摘要包括初始摘要以及新的消息等。您还可以设计一种混合方法,其中一定数量的消息保留在聊天历史记录中,而其他消息则被摘要。