LangChain篇-消息管理与聊天历史存储

一、消息存储在内存

下面我们展示一个简单的示例,其中聊天历史保存在内存中,此处通过全局 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_idconversation_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='下午你在打篮球。'})]

请注意,再次调用链式模型会生成一个新的摘要,该摘要包括初始摘要以及新的消息等。您还可以设计一种混合方法,其中一定数量的消息保留在聊天历史记录中,而其他消息则被摘要。

相关推荐
青小莫10 分钟前
如何使用deepseek满血版
人工智能
GaolBB42 分钟前
博客十二:基本框架概述(上)
人工智能
强盛小灵通专卖员1 小时前
目标检测中F1-Score指标的详细解析:深度理解,避免误区
人工智能·目标检测·机器学习·视觉检测·rt-detr
用户711283928471 小时前
什么?大模型删库跑路了?
langchain·llm
SuperHeroWu71 小时前
【AI大模型入门指南】概念与专有名词详解 (一)
人工智能·ai·大模型·入门·概念
love530love1 小时前
【笔记】NVIDIA AI Workbench 中安装 cuDNN 9.10.2
linux·人工智能·windows·笔记·python·深度学习
后端小肥肠2 小时前
【效率核爆2.0】爆款短视频拆解进入流水线时代!Coze+飞书字段捷径自动生成结构化拆解报告
人工智能·aigc·coze
奇舞精选2 小时前
前端开发中AI的进阶之路:从思维重构到工程落地
前端·人工智能
创小匠2 小时前
《创始人IP打造:知识变现的高效路径》
人工智能·网络协议·tcp/ip
anyup2 小时前
震惊了!中石化将开源组件二次封装申请专利,这波操作你怎么看?
前端·程序员