LangChain 系列文章目录

第一章 LangChain 简介和核心包;
第二章 LangChain 提示词工程;
第三章 LangChain 工作流;
第四章 LangChain服务部署与链路监控;
第五章 LangChain 消息管理与聊天历史存储



前言

本文主要整理 LangChain 消息管理与聊天历史存储相关内容,包括内存消息存储、会话唯一键配置、Redis 持久化、多种消息存储方式、消息裁剪以及历史对话总结记忆等。


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

一、消息存储在内存

1.作用:将该历史对话通过session_id保存到内存中。

2.构建消息存储方法一

  • 自建一个get_session_history()方法,用于将对话记录保存到自定义的store[session_id]中
    • 输入参数为session_id,用于保存到指定会话中。
    • 输出为抽象基类BaseChatMessageHistory的子类ChatMessageHistory实例,LangChain提供的保存对象。
  • 创建带会话历史记录的Runnable(链条)
    • 使用RunnableWithMessageHistory()方法创建一个可以输入历史记录的链条(Runnable)对象。
    • 参数:
      • runnable:(必需) 要包装的可运行对象,可以是Runnable接口的LangChain组件,也可以是链。
      • get_session_history:(必需) 工厂函数,接收session_id并返回对应的聊天消息历史存储对象的函数名称
      • 工厂函数:不直接实例化类,而是使用一个函数来生产对象
      • input_messages_key: (可选) 指定用户消息在输入字典中的键名
      • history_messages_key:(可选) 指定历史消息在输入字典中的键名
  • 执行带会话历史记录的Runnable(链条)
    • 使用invoke()执行链条。
    • 参数
      • input为输入的内容字典,字典的键为对应的提示词模板的模板变量
      • config为配置项字典。
      • 外层结构:包含一个configurable键
      • 内层结构:session_id对话的唯一标识符。不同的session_id代表不同的会话,历史也会不一样。

代码

from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_openai.chat_models import ChatOpenAI
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory

# 可以尝试打印prompt,不过会内容会比较乱。非必要。
from langchain_core.callbacks import StdOutCallbackHandler
from langchain_core.messages import BaseMessage
# 1. 创建一个打印prompt的callback
class PrintPromptCallback(StdOutCallbackHandler):
    def on_chat_model_start(self, serialized, messages, **kwargs):
        # messages就是最终送进模型的完整prompt(list[BaseMessage])
        print("=== 第二次真正送进模型的 prompt ===")
        for m in messages[0]:        # messages是List[List[BaseMessage]]
            print(f"{m.type}: {m.content}")

# 创建一个聊天提示词模版
prompt = ChatPromptTemplate.from_messages([
        ("system", "You're an assistant who's good at {ability}."),
        MessagesPlaceholder(variable_name="history"),
        ("human", "{inputx}"),
])

model = ChatOpenAI(
    model="Pro/deepseek-ai/DeepSeek-V3",
    openai_api_key="************",
    openai_api_base="https://api.siliconflow.cn/v1",
    temperature=0.7,
)

runnable = prompt | model

# 用来存储会话历史记录
store = {}

# 定义一个获取会话历史的函数,入参是session_id,返回一个会话历史记录
def get_session_history(session_id: str) -> BaseChatMessageHistory:
    if session_id not in store:
        store[session_id] = ChatMessageHistory()
    return store[session_id]

# 创建一个带会话历史记录的Runnable
with_message_history = RunnableWithMessageHistory(
    runnable=runnable,
    get_session_history=get_session_history,
    history_messages_key="history",
)

# 调用带会话历史记录的Runnable
response = with_message_history.invoke(
    input={"ability": "math", "inputx": "余弦是什么意思?"},
    config={"configurable": {"session_id": "abc123"}},
)
print(response)
print("=======================")

# 相同session_id --> 记住了
response = with_message_history.invoke(
    input={"ability": "math", "inputx": "什么?"},
    config={
        "configurable": {"session_id": "abc123"},
        "callbacks": [PrintPromptCallback()]  # 增加一个callback函数打印prompt
    },
)
print(response)
print("=======================")

# 新session_id --> 不记得了
response = with_message_history.invoke(
    {"ability": "math", "inputx": "什么?"},
    config={"configurable": {"session_id": "def234"}},
)
print(response)

结果

content = "余弦是三角函数之一,表示邻边与斜边的比值。"
additional_kwargs = {"refusal": None}
response_metadata = {
    "token_usage": {
        "completion_tokens": 13,
        "prompt_tokens": 23,
        "total_tokens": 36,
        "completion_tokens_details": None,
        "prompt_tokens_details": None,
    },
    "model_name": "Pro/deepseek-ai/DeepSeek-V3",
    "system_fingerprint": "",
    "id": "0197065649d0cd9f34ea935878f474de",
    "service_tier": None,
    "finish_reason": "stop",
    "logprobs": None,
}
id = "run--e4e6ee75-4d75-41c2-a095-c123a2a93b66-0"
usage_metadata = {
    "input_tokens": 23,
    "output_tokens": 13,
    "total_tokens": 36,
    "input_token_details": {},
    "output_token_details": {},
}
=======================
...
=== 第二次真正送进模型的 prompt ===
system: You're an assistant who's good at math.
human: 余弦是什么意思?
ai: 余弦是三角函数之一,表示邻边与斜边的比值。
human: 什么?
...

content = "邻边比斜边,记作cos。"
additional_kwargs = {"refusal": None}
response_metadata = {
    ...
}
id = "run--4aee0646-9a23-4906-9971-8df9f8849d74-0"
usage_metadata = {
    ...
}
=======================
content = "有什么可以帮您?"
additional_kwargs = {"refusal": None}
response_metadata = {
    "token_usage": {
        "completion_tokens": 5,
        "prompt_tokens": 22,
        "total_tokens": 27,
        "completion_tokens_details": None,
        "prompt_tokens_details": None,
    },
    "model_name": "Pro/deepseek-ai/DeepSeek-V3",
    "system_fingerprint": "",
    "id": "0197065657872ab0cd92d3e280c480ec",
    "service_tier": None,
    "finish_reason": "stop",
    "logprobs": None,
}
id = "run--b7965209-2a85-4b84-a890-0ca93618253d-0"
usage_metadata = {
    "input_tokens": 22,
    "output_tokens": 5,
    "total_tokens": 27,
    "input_token_details": {},
    "output_token_details": {},
}

3.构建消息存储方法二

  • 自建一个get_session_history()方法,用于将对话记录保存到自定义的store[session_id]中
    • 输入参数为session_id,用于保存到指定会话中。
    • 输出为InMemoryChatMessageHistory实例,LangChain提供的保存对象。
      • InMemoryChatMessageHistory本质是ChatMessageHistory的旧写法
  • 创建带会话历史记录的Runnable(链条)同上
    • 因为get_session_history()方法返回的是InMemoryChatMessageHistory实例,所以无需指定history_messages_key历史消息在输入字典中的键名。所以MessagesPlaceholder的variable_name参数定义什么都可以。
  • 执行带会话历史记录的Runnable(链条)同上
    • 对于invoke()方法的input输入为一个HumanMessage对象时,input_key或history_key和MessagesPlaceholder的variable_name可以随便定义

代码

from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.chat_history import (
    BaseChatMessageHistory,
    InMemoryChatMessageHistory,
)
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.messages import HumanMessage
from langchain_openai import ChatOpenAI

model = ChatOpenAI(
    model="Pro/deepseek-ai/DeepSeek-V3",
    # model="Qwen/Qwen2.5-VL-32B-Instruct",
    openai_api_key="************",
    openai_api_base="https://api.siliconflow.cn/v1",
    temperature=0.7,
)

prompt = ChatPromptTemplate.from_messages([
        ("system", "你是个智能AI助手"),
        MessagesPlaceholder(variable_name="messages"),
])

chain = prompt | model

# 存储会话
store = {}
def get_session_history(session_id: str) -> BaseChatMessageHistory:
    if session_id not in store:
        store[session_id] = InMemoryChatMessageHistory()
    return store[session_id]

with_message_history = RunnableWithMessageHistory(
    runnable=chain,
    get_session_history=get_session_history,
)

# 用固定配置模拟同一会话
config = {"configurable": {"session_id": "abc2"}}

response = with_message_history.invoke(
    input=HumanMessage(content="你好,我是张三。"),
    config=config,
)
print(response)
print("=======================")

response2 = with_message_history.invoke(
    input=HumanMessage(content="我叫什么名字?"),
    config=config,
)
print(response2)
print("=======================")
print(response2.content)

结果

content = "你好,张三!很高兴认识你。我是你的智能助手,有什么可以帮你的吗?😊"
additional_kwargs = {"refusal": None}
response_metadata = {
    "token_usage": {
        "completion_tokens": 21,
        "prompt_tokens": 13,
        "total_tokens": 34,
        "completion_tokens_details": None,
        "prompt_tokens_details": None,
    },
    "model_name": "Pro/deepseek-ai/DeepSeek-V3",
    "system_fingerprint": "",
    "id": "01981289e155932d3167941a516d9fe3",
    "service_tier": None,
    "finish_reason": "stop",
    "logprobs": None,
}
id = "run--7860cfb6-c4ca-4b54-9bca-72bb71c9e56b-0"
usage_metadata = {
    "input_tokens": 13,
    "output_tokens": 21,
    "total_tokens": 34,
    "input_token_details": {},
    "output_token_details": {},
}
=======================
content = "你刚刚告诉我你的名字是**张三**哦!需要我帮你记住这个名字吗?或者有其他问题想聊? 😄"
additional_kwargs = {"refusal": None}
response_metadata = {
    "token_usage": {
        "completion_tokens": 26,
        "prompt_tokens": 41,
        "total_tokens": 67,
        "completion_tokens_details": None,
        "prompt_tokens_details": None,
    },
    "model_name": "Pro/deepseek-ai/DeepSeek-V3",
    "system_fingerprint": "",
    "id": "01981289ea33521239e7037c4626328d",
    "service_tier": None,
    "finish_reason": "stop",
    "logprobs": None,
}
id = "run--fa7b0be9-f01e-4021-805b-f8c72055a682-0"
usage_metadata = {
    "input_tokens": 41,
    "output_tokens": 26,
    "total_tokens": 67,
    "input_token_details": {},
    "output_token_details": {},
}
=======================
你刚刚告诉我你的名字是**张三**哦!需要我帮你记住这个名字吗?或者有其他问题想聊? 😄

二、配置会话唯一键

1.作用:将该历史对话通过user_id和conversation_id保存到内存中。

2.构建消息存储方法

  • 构建一个get_session_history()方法,用于将对话记录保存到自定义的store[(user_id, conversation_id)]中
  • 参数
    • user_id,用于将记录保存到指定的用户id中。
    • conversation_id:用于保存用户下的每次对话的id
  • 返回为BaseChatMessageHistory实例,LangChain提供的基础保存对象。

3.创建一个带会话历史记录的Runnable(链条)

  • 使用RunnableWithMessageHistory()方法,创建一个可以输入历史记录的链条(Runnable)对象。
  • 参数:
    • runnable:(必需) 要包装的基础可运行对象,可以是任何实现了 Runnable 接口的 LangChain 组件
    • get_session_history:(必需) 工厂函数,接收 session_id 并返回对应的聊天消息历史存储对象的函数名称
    • input_messages_key:(可选) 指定输入字典中用户消息的键名
    • history_messages_key:(可选) 指定历史消息在输入字典中的键名
    • history_factory_config:配置传递给聊天历史工厂(chat history factory)的字段参数。
      • history_factory_config是一个ConfigurableFieldSpec对象的列表,用于定义需要传递给get_session_history函数的配置字段。
      • 如果不指定history_factory_config,系统会使用默认的配置,即只包含一个session_id字段
  • ConfigurableFieldSpec对象包含内容
    • id:字段的唯一标识符,用于在配置中引用该字段。如:“user_id”、“conversation_id”
    • annotation:字段的类型注解,用于指定该字段接受的数据类型。
    • name:字段的显示名称,提供更友好的字段名称显示。如:“User ID”、“Conversation ID”
    • description:字段的描述信息,用于解释该字段的用途。如:“用户的唯一标识符。”
    • default:字段的默认值,当没有提供值时使用的默认值。如:“” 空字符串
    • is_shared:字段是否共享,默认False

4.执行Runnable(链条)

  • 使用invoke()执行链条。
  • 参数
    • input为输入的内容字典,字典的键为对应的提示词模板的模板变量
    • config为配置项字典。
      • 外层结构:包含一个configurable键
      • 内层结构:user_id用户的唯一标识符,conversation_id对话的唯一标识符。键值对与之前定义history_factory_config中的ConfigurableFieldSpec配置相对应
  • config作用
    • 实际应用:
      • 这些参数会被传递给get_session_history 函数
      • 用于获取或创建特定用户和对话的聊天历史
      • 在示例代码中,当user_id和conversation_id相同时(如"123"和"1"),会使用相同的聊天历史
      • 当 conversation_id改变时(如改为 “2”),会创建新的聊天历史
    • 作用效果:
      • 从代码运行结果可以看到,当使用相同的user_id和conversation_id时,AI能够记住之前的对话内容
      • 当改变conversation_id时,AI 就无法获取之前的对话内容,表现为不记得之前的对话

代码

from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_openai.chat_models import ChatOpenAI
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.runnables import ConfigurableFieldSpec

prompt = ChatPromptTemplate.from_messages([
        ("system", "You're an assistant who's good at {ability}."),
        MessagesPlaceholder(variable_name="history"),  # 历史消息占位符
        ("human", "{input}"),
])

model = ChatOpenAI(
    model="Pro/deepseek-ai/DeepSeek-V3",
    openai_api_key="************",
    openai_api_base="https://api.siliconflow.cn/v1",
    temperature=0.7,
)
runnable = prompt | model
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=runnable,
    get_session_history=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,
        ),
    ],
)

response = with_message_history.invoke(
    input={"ability": "math", "input": "余弦是什么意思?"},
    config={"configurable": {"user_id": "123", "conversation_id": "1"}},
)
print(response)

# 记住
response = with_message_history.invoke(
    input={"ability": "math", "input": "什么?"},
    config={"configurable": {"user_id": "123", "conversation_id": "1"}},
)
print(response)

# 同一用户,不同对话,记不住
response = with_message_history.invoke(
    input={"ability": "math", "input": "什么?"},
    config={"configurable": {"user_id": "123", "conversation_id": "2"}},
)
print(response)

结果

content = "余弦是三角函数之一,表示邻边与斜边的比值。"
additional_kwargs = {"refusal": None}
response_metadata = {
    ...
}
id = "run--ff7be465-ba6b-4186-954d-6467862ef582-0"
usage_metadata = {
    ...
}

content = "余弦是直角三角形中邻边比斜边的比值。"
additional_kwargs = {"refusal": None}
response_metadata = {
    ...
}
id = "run--088ecf83-3e2d-4b5b-8cc9-203a069787cb-0"
usage_metadata = {
    ...
}

content = "啥?"
additional_kwargs = {"refusal": None}
response_metadata = {
    ...
}
id = "run--1565a8fd-0585-4024-a593-78a210f33207-0"
usage_metadata = {
    ...
}

三、消息持久化

1.作用:将该历史对话通过user_id保存到Redis中。

2.配置Redis环境

  • 外部需要有redis服务。
  • 内部需要安装redis的python操作库:pip install redis

3.构建消息存储方法

  • 构建一个get_session_history()方法,用于将对话记录保存到自定义的store[session_id]中
  • 输入参数session_id,用于指定需要保存的session_id。
  • 返回为RedisChatMessageHistory(session_id, url=REDIS_URL)实例,LangChain提供的保存对象。
    • url参数为Redis的地址字符串

4.创建一个带会话历史记录的Runnable(链条)

  • 使用RunnableWithMessageHistory()方法创建一个可以输入历史记录的链条(Runnable)对象。
  • 参数:
    • runnable:(必需) 要包装的基础可运行对象,可以是任何实现了Runnable接口的LangChain组件
    • get_session_history:(必需) 工厂函数,接收 session_id 并返回对应的聊天消息历史存储对象的函数名称
    • input_messages_key:(可选) 指定输入字典中用户消息的键名
    • history_messages_key:(可选) 指定历史消息在输入字典中的键名

5.执行Runnable(链条)

  • 使用invoke()执行链条。
  • 参数
    • input为输入的内容字典,字典的键为对应的提示词模板的模板变量
    • config为配置项字典。
      • 外层结构:包含一个configurable键
      • 内层结构:session_id对话的唯一标识符。不同的session_id代表不同的会话,历史也会不一样。

代码

# 引入redis聊天消息存储类
from langchain_community.chat_message_histories import RedisChatMessageHistory
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_openai.chat_models import ChatOpenAI

prompt = ChatPromptTemplate.from_messages([
        ("system", "You're an assistant who's good at {ability}."),
        MessagesPlaceholder(variable_name="history"),  # 历史消息占位符
        ("human", "{input}"),
])
model = ChatOpenAI(
    model="Pro/deepseek-ai/DeepSeek-V3",
    openai_api_key="************",
    openai_api_base="https://api.siliconflow.cn/v1",
    temperature=0.7,
    max_tokens=8000,
)
runnable = prompt | model
REDIS_URL = "redis://localhost:6379/0"

def get_message_history(session_id: str) -> RedisChatMessageHistory:
    res = RedisChatMessageHistory(session_id, url=REDIS_URL)
    return res

with_message_history = RunnableWithMessageHistory(
    runnable=runnable,
    get_session_history=get_message_history,
    input_messages_key="input",
    history_messages_key="history",
)
response = with_message_history.invoke(
    input={"ability": "math", "input": "余弦是什么意思?"},
    config={"configurable": {"session_id": "abc123"}},
)
print(response)

# 记住
response = with_message_history.invoke(
    input={"ability": "math", "input": "什么?"},
    config={"configurable": {"session_id": "abc123"}},
)
print(response)

# 新session_id --> 不记得了。
response = with_message_history.invoke(
    input={"ability": "math", "input": "什么?"},
    config={"configurable": {"session_id": "def234"}},
)
print(response)

结果

content = "余弦是三角函数之一,表示邻边与斜边的比值,记作cos。"
additional_kwargs = {"refusal": None}
response_metadata = {
    ...
}
id = "run--cf611be7-b56f-4451-890b-df2ef48a49cb-0"
usage_metadata = {
    ...
}

content = "余弦(cos)是三角函数,指直角三角形中邻边与斜边的比值。"
additional_kwargs = {"refusal": None}
response_metadata = {
    ...
}
id = "run--3fcc61b8-7ede-4923-8aa1-5b929222281a-0"
usage_metadata = {
    ...
}

content = "不清楚您的问题,请再详细说明。"
additional_kwargs = {"refusal": None}
response_metadata = {
    ...
}
id = "run--0ee42bbe-d6a1-4d53-8c4e-d3bfd8b70da6-0"
usage_metadata = {
    ...
}

6.Redis内有数据

在这里插入图片描述

7.简易使用user_id和conversation_id定义对话

  • 其实就是使用user_id和conversation_id拼接成一个字符串,类似session_id输入到redis
  • 在get_message_history()方法中增加user_id和conversation_id,并且将其拼接成一个字符串。
  • 设置RunnableWithMessageHistory()方法的参数history_factory_config定义两个id:user_id和conversation_id

代码

from langchain_community.chat_message_histories import RedisChatMessageHistory
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_openai.chat_models import ChatOpenAI
from langchain_core.runnables import ConfigurableFieldSpec

prompt = ChatPromptTemplate.from_messages([
        ("system", "You're an assistant who's good at {ability}."),
        MessagesPlaceholder(variable_name="history"),  # 历史消息占位符
        ("human", "{input}"),
])
model = ChatOpenAI(
    model="doubao-seed-1-6-flash-250615",
    openai_api_key="************",
    openai_api_base="https://ark.cn-beijing.volces.com/api/v3",
)
runnable = prompt | model

REDIS_URL = "redis://localhost:6379/0"
def get_message_history(user_id: str, conversation_id: str) -> RedisChatMessageHistory:
    all_id = user_id+"_"+conversation_id           # 拼接字符串
    res = RedisChatMessageHistory(all_id, url=REDIS_URL)
    return res

with_message_history = RunnableWithMessageHistory(
    runnable=runnable,
    get_session_history=get_message_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,
        ),
    ],
)

response = with_message_history.invoke(
    input={"ability": "math", "input": "那么正弦呢"},
    config={"configurable": {"user_id": "abc123","conversation_id": "xxx123"}},
)
print(response)

# 记住
response = with_message_history.invoke(
    input={"ability": "math", "input": "第二个问了什么问题?"},
    config={"configurable": {"user_id": "abc123","conversation_id": "xxx123"}},
)
print(response)

8.使用user_id和conversation_id定义对话

  • 其实就是使用user_id和conversation_id拼接成一个字符串,类似session_id输入到redis
    • user_id和conversation_id使用:拼接时,redis会自动生成一个user_id文件夹来保存数据,用于区分不同用户。
    • 最终redis的key为user_id:conversation_id
  • 继承RedisChatMessageHistory类并修改:
    • 修改session_id为user_id:conversation_id
    • 修改add_message()用于设置最大保存对话记录数量
  • 在get_message_history()方法中增加user_id和conversation_id参数
  • 设置RunnableWithMessageHistory()方法的参数history_factory_config定义两个id:user_id和conversation_id

代码

from langchain_community.chat_message_histories import RedisChatMessageHistory
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.runnables import ConfigurableFieldSpec
from langchain_openai.chat_models import ChatOpenAI

# ----------------- 基础配置 -----------------
prompt = ChatPromptTemplate.from_messages([
        ("system", "You're an assistant who's good at {ability}."),
        MessagesPlaceholder(variable_name="history"),  # 历史消息占位符
        ("human", "{input}"),
])

model = ChatOpenAI(
    model="Pro/deepseek-ai/DeepSeek-V3",
    openai_api_key="************",
    openai_api_base="https://api.siliconflow.cn/v1",
    temperature=0.7,
    max_tokens=8000,
)

# ---------- 1. 自定义 Redis 历史存储,带「最多 8 轮」限制 -------------
REDIS_URL = "redis://localhost:6379/0"
class RedisChatMessageHistoryWithKeys(RedisChatMessageHistory):
    MAX_ROUNDS = 8
    MAX_MSG = MAX_ROUNDS * 2                      # 8 轮 = 16 条消息

    def __init__(self, user_id: str, conversation_id: str, url: str = REDIS_URL):
        super().__init__(session_id=f"{user_id}:{conversation_id}", url=url)

    # 重写 add_message,在每次添加后做滑动窗口
    def add_message(self, message):
        super().add_message(message)             # 先正常写入 
        while len(self.messages) > self.MAX_MSG: # 如果超过上限,从左侧弹出旧消息
            self.messages.pop(0)                 # 移除最早的一条
            self.redis_client.lpop(self.key)     # 同步删除 Redis 里最左元素

# ----------------- 2. 获取会话历史 -----------------
def get_session_history(user_id: str, conversation_id: str):
    return RedisChatMessageHistoryWithKeys(user_id, conversation_id, url=REDIS_URL)

# ----------------- 3. Runnable 包装 -----------------
with_message_history = RunnableWithMessageHistory(
    runnable=prompt | model,
    get_session_history=get_session_history,
    input_messages_key="input",
    history_messages_key="history",
    history_factory_config=[
        ConfigurableFieldSpec(
            id="user_id", annotation=str, name="User ID", is_shared=True
        ),
        ConfigurableFieldSpec(
            id="conversation_id", annotation=str, name="Conversation ID", is_shared=True
        ),
    ],
)

# ----------------- 4. 简单测试:连续对话 10 轮 -----------------
for i in range(10):
    resp = with_message_history.invoke(
        {"ability": "math", "input": f"第 {i+1} 轮"},
        config={"configurable": {"user_id": "u1", "conversation_id": "c1"}},
    )
    print(f"第 {i+1} 轮回复:{resp.content}")

redis结果,只保存有8轮对话

9.多种存储方式(举例说明,不详细说明)

  • 文件存储:FileChatMessageHistory
  • SQL存储:SQLChatMessageHistory
  • MongoDB存储:MongoDBChatMessageHistory

四、裁剪消息

1.作用:LLM和聊天模型的上下文token有限,需要截取有效的长度。

2.没有截取历史消息的聊天模型链

  • 使用ChatMessageHistory()的add_user_message()和add_ai_message()来增加用户和ai历史对话记录。记录会按循序增加下去。
  • 使用ChatMessageHistory()的messages属性获取所有的记录
  • 使用匿名方程lambda session_id: temp_chat_history来输入执行链的get_session_history属性
    • 与自建一个get_session_history()方法的功能一样,用于将对话记录保存到自定义的store[session_id]中

代码

from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_openai import ChatOpenAI

# 手动增加对话记录
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("运动是一个不错选择")
print(temp_chat_history.messages)

prompt = ChatPromptTemplate.from_messages([
    ("system","你是一个乐于助人的助手。尽力回答所有问题。提供的聊天历史包括与您交谈的用户的事实。",),
    MessagesPlaceholder(variable_name="chat_history"),
    ("human", "{input}"),
])
model = ChatOpenAI(
    model="Pro/deepseek-ai/DeepSeek-V3",
    openai_api_key="************",
    openai_api_base="https://api.siliconflow.cn/v1",
)
chain = prompt | model

chain_with_message_history = RunnableWithMessageHistory(
    runnable=chain,
    get_session_history=lambda session_id: temp_chat_history,
    input_messages_key="input",
    history_messages_key="chat_history",
)
response = chain_with_message_history.invoke(
    input={"input": "我今天心情如何?"},
    config={"configurable": {"session_id": "unused"}},
)
print(response)
print(temp_chat_history.messages)

结果

[
    HumanMessage(
        content="我叫Jack,你好", additional_kwargs={}, response_metadata={}),
    AIMessage(
        content="你好", additional_kwargs={}, response_metadata={}),
    HumanMessage(
        content="我今天心情挺开心", additional_kwargs={}, response_metadata={}),
    AIMessage(
        content="很好啊!", additional_kwargs={}, response_metadata={}),
    HumanMessage(
        content="我下午在打篮球", additional_kwargs={}, response_metadata={}),
    AIMessage(
        content="运动是一个不错选择", additional_kwargs={}, response_metadata={}),
]

content = "根据之前的对话记录,你提到过今天心情挺开心,下午还去打了篮球呢!看来是个活力满满的日子~ 现在你的心情有变化吗?还是依然保 持好状态呢? 🌞"
additional_kwargs = {"refusal": None}
response_metadata = {
...
}
id = "run--adef5c58-0be0-4df4-a556-5a0514f91bf2-0"
usage_metadata = {
...
}

[
    HumanMessage(
        content="我叫Jack,你好", additional_kwargs={}, response_metadata={}),
    AIMessage(
        content="你好", additional_kwargs={}, response_metadata={}),
    HumanMessage(
        content="我今天心情挺开心", additional_kwargs={}, response_metadata={}),
    AIMessage(
        content="很好啊!", additional_kwargs={}, response_metadata={}),
    HumanMessage(
        content="我下午在打篮球", additional_kwargs={}, response_metadata={}),
    AIMessage(
        content="运动是一个不错选择", additional_kwargs={}, response_metadata={}),
    HumanMessage(
        content="我今天心情如何?", additional_kwargs={}, response_metadata={}),
    AIMessage(
        content="根据之前的对话记录,你提到过今天心情挺开心,下午还去打了篮球呢!看来是个活力满满的日子~ 现在你的心情有变化吗?还是依然保持好状态呢? 🌞",
        additional_kwargs={"refusal": None},
        response_metadata={
            "token_usage": {
                "completion_tokens": 43,
                "prompt_tokens": 59,
                "total_tokens": 102,
                "completion_tokens_details": None,
                "prompt_tokens_details": None,
            },
            "model_name": "Pro/deepseek-ai/DeepSeek-V3",
            "system_fingerprint": "",
            "id": "019707e82c23aa55d485540c671107cd",
            "service_tier": None,
            "finish_reason": "stop",
            "logprobs": None,
        },
        id="run--adef5c58-0be0-4df4-a556-5a0514f91bf2-0",
        usage_metadata={
            "input_tokens": 59,
            "output_tokens": 43,
            "total_tokens": 102,
            "input_token_details": {},
            "output_token_details": {},
        },
    ),
]

3.截取历史消息后的聊天模型链

  • 创建一个trim_messages()方法,裁剪temp_chat_history中的消息。
    • 方法必须有chain_input参数
    • 使用temp_chat_history.clear()方法,清除所有的历史对话记录
    • 使用temp_chat_history.add_message()方法,增加历史对话记录
    • 返回True为有截取,返回False为无截取。
  • 裁剪过程
    • 先使用RunnablePassthrough.assign(messages_trimmed=trim_messages)将裁剪方法导入。
      • 参数messages_trimmed为裁剪方法名称。
      • 其实就是增加一个messages_trimmed的变量到链中,值为裁剪方法名称
    • 再使用链,链接使用所有的历史对话记录
  • 问最近提及的问题,是能够回答

代码

from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_openai import ChatOpenAI
from langchain_core.runnables import RunnablePassthrough

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("运动是一个不错选择")

prompt = ChatPromptTemplate.from_messages([
    ("system","你是一个乐于助人的助手。尽力回答所有问题。提供的聊天历史包括与您交谈的用户的事实。",),
    MessagesPlaceholder(variable_name="chat_history"),
    ("human", "{input}"),
])
model = ChatOpenAI(
    model="Pro/deepseek-ai/DeepSeek-V3",
    openai_api_key="************",
    openai_api_base="https://api.siliconflow.cn/v1",
)
chain = prompt | model

def trim_messages(chain_input):
    stored_messages = temp_chat_history.messages
    if 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_message_history = RunnableWithMessageHistory(
    runnable=chain,
    get_session_history=lambda session_id: temp_chat_history,
    input_messages_key="input",
    history_messages_key="chat_history",
)
chain_with_trimming = (
    RunnablePassthrough.assign(messages_trimmed=trim_messages)
    | chain_with_message_history
)

response = chain_with_trimming.invoke(
    {"input": "我今天下午在干嘛?"},
    {"configurable": {"session_id": "unused"}},
)
print(response.content)
print(temp_chat_history.messages)

结果

根据之前的聊天记录,你提到过 **“我下午在打篮球”**。所以,你今天下午应该是在 **打篮球** 🏀。  
如果之后有其他安排或者记错了,可以告诉我,我帮你记着!😊

[
    HumanMessage(
        content="我下午在打篮球", additional_kwargs={}, response_metadata={}),
    AIMessage(
        content="运动是一个不错选择", additional_kwargs={}, response_metadata={}),
    HumanMessage(
        content="我今天下午在干嘛?", additional_kwargs={}, response_metadata={}),
    AIMessage(
        content="根据之前的聊天记录,你提到过 **“我下午在打篮球”**。所以,你今天下午应该是在 **打篮球** 🏀。  \n\n如果之后有其他安排或者记错了,可以告诉我,我帮你记着!😊",
        additional_kwargs={"refusal": None},
        response_metadata={
            "token_usage": {
                "completion_tokens": 51,
                "prompt_tokens": 42,
                "total_tokens": 93,
                "completion_tokens_details": None,
                "prompt_tokens_details": None,
            },
            "model_name": "Pro/deepseek-ai/DeepSeek-V3",
            "system_fingerprint": "",
            "id": "019707f7c262a32b1a84a7c913ebb2e4",
            "service_tier": None,
            "finish_reason": "stop",
            "logprobs": None,
        },
        id="run--2a3b9e51-b44c-4a23-8dce-047b1267502e-0",
        usage_metadata={
            "input_tokens": 42,
            "output_tokens": 51,
            "total_tokens": 93,
            "input_token_details": {},
            "output_token_details": {},
        },
    ),
]
  • 问最前提及的问题,无法知道答案

代码

...
response = chain_with_trimming.invoke(
    {"input": "我叫什么名字?"},
    {"configurable": {"session_id": "unused"}},
)
print(response.content)

结果

...
在你的对话历史中,没有提供过你的名字信息。作为AI助手,我无法知道你的真实姓名,除非你主动告诉我。如果你愿意分享,可以告诉我你的名字,我会记住并在之后 的对话中使用哦!
(根据当前对话记录,你只提到过“下午在打篮球”,没有提及任何姓名信息。)

五、总结记忆

1.作用:使用LLM来对历史对话记录做对话总结。目的减少使用token。

2.总结历史对话记录

  • 创建一个summarize_messages()方法,总结temp_chat_history中的消息。
    • 方法必须有chain_input参数
    • 方法返回True为有总结,返回False为无总结。
    • 方法内部其实就是一个LCEL链,主要操作就是总结所有的历史对话记录
    • 过程:
      • 先创建提示词模板,模板参数是所有的历史对话记录
      • 创建链条,模型使用原始模型model
      • 执行链条,输出总结内容
      • 清理之前的所有记录,换成总结内容
  • 总结过程
    • 先使用RunnablePassthrough.assign(messages_trimmed=summarize_messages)将总结方法导入。
      • 参数messages_trimmed为总结方法名称。
      • 其实就是增加一个messages_trimmed的变量到链中,值为总结方法名称
    • 再使用链,链接使用所有的历史对话记录

代码

from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_openai import ChatOpenAI
from langchain_core.runnables import RunnablePassthrough

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("运动是一个不错选择")

prompt = ChatPromptTemplate.from_messages([
    ("system","你是一个乐于助人的助手。尽力回答所有问题。提供的聊天历史包括与您交谈的用户的事实。",),
    MessagesPlaceholder(variable_name="chat_history"),
    ("human", "{input}"),
])
model = ChatOpenAI(
    model="Pro/deepseek-ai/DeepSeek-V3",
    openai_api_key="************",
    openai_api_base="https://api.siliconflow.cn/v1",
)
chain = prompt | model

chain_with_message_history = RunnableWithMessageHistory(
    runnable=chain,
    get_session_history=lambda session_id: temp_chat_history,
    input_messages_key="input",
    history_messages_key="chat_history",
)

def summarize_messages(chain_input):
    stored_messages = temp_chat_history.messages
    if len(stored_messages) == 0:
        return False
    summarization_prompt = ChatPromptTemplate.from_messages([
            MessagesPlaceholder(variable_name="chat_history"),
            ("user","将上述聊天消息浓缩成一条摘要消息。尽可能包含具体细节。",),
    ])
    summarization_chain = summarization_prompt | model
    summary_message = summarization_chain.invoke(
                        {"chat_history": stored_messages}
    )
    temp_chat_history.clear()
    temp_chat_history.add_message(summary_message) # 将总结内容以AI角色加到历史
    return True

chain_with_summarization = (
    RunnablePassthrough.assign(messages_summarized=summarize_messages)
    | chain_with_message_history
)

response = chain_with_summarization.invoke(
    {"input": "我的名字叫什么,下午在干嘛,心情如何"},
    {"configurable": {"session_id": "unused"}},
)
print(response.content)
print("-" * 80)
print(temp_chat_history.messages)

结果

你的名字是Jack,下午你去打篮球了,心情非常愉快!运动确实是个很棒的选择,继续保持好状态哦!😊🏀
--------------------------------------------------------------------------------
[
    AIMessage(
        content="【聊天摘要】  \n用户Jack表示今天心情愉快,下午进行了打篮球的活动,认为运动是个不错的选择。",
        additional_kwargs={"refusal": None},
        response_metadata={
            "token_usage": {
                "completion_tokens": 23,
                "prompt_tokens": 48,
                "total_tokens": 71,
                "completion_tokens_details": None,
                "prompt_tokens_details": None,
            },
            "model_name": "Pro/deepseek-ai/DeepSeek-V3",
            "system_fingerprint": "",
            "id": "019832d716528a1e57a72f083a1466c4",
            "service_tier": None,
            "finish_reason": "stop",
            "logprobs": None,
        },
        id="run--ed6ccf0d-bd3d-4aa6-840e-7b9e0a436991-0",
        usage_metadata={
            "input_tokens": 48,
            "output_tokens": 23,
            "total_tokens": 71,
            "input_token_details": {},
            "output_token_details": {},
        },
    ),
    HumanMessage(
        content="我的名字叫什么,下午在干嘛,心情如何",
        additional_kwargs={},
        response_metadata={},
    ),
    AIMessage(
        content="你的名字是Jack,下午你去打篮球了,心情非常愉快!运动确实是个很棒的选择,继续保持好状态哦! 😊🏀",
        additional_kwargs={"refusal": None},
        response_metadata={
            "token_usage": {
                "completion_tokens": 30,
                "prompt_tokens": 58,
                "total_tokens": 88,
                "completion_tokens_details": None,
                "prompt_tokens_details": None,
            },
            "model_name": "Pro/deepseek-ai/DeepSeek-V3",
            "system_fingerprint": "",
            "id": "019832d75408843db28af5fa11952722",
            "service_tier": None,
            "finish_reason": "stop",
            "logprobs": None,
        },
        id="run--cc6a1ec8-7b3d-4193-86e7-e4186188c415-0",
        usage_metadata={
            "input_tokens": 58,
            "output_tokens": 30,
            "total_tokens": 88,
            "input_token_details": {},
            "output_token_details": {},
        },
    ),
]

3.截取历史对话,部分总结历史

  • 当历史记录大于10条时,会截取最旧的6条作为总结,剩余的不变。
  • 汇总成一个背景,以system角色保存
  • 会议对话形式来交互

代码

from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_openai import ChatOpenAI
from langchain_core.runnables import RunnablePassthrough
from langchain_core.messages import SystemMessage

temp_chat_history = ChatMessageHistory()

prompt = ChatPromptTemplate.from_messages([
    ("system","你是一个专业的助手。尽力回答所有问题。提供的聊天历史包括与您交谈的用户的基本信息和对话内容。",),
    MessagesPlaceholder(variable_name="chat_history"),
    ("human", "{input}"),
])
model = ChatOpenAI(
    model="doubao-seed-1-6-flash-250615",
    openai_api_key="************",
    openai_api_base="https://ark.cn-beijing.volces.com/api/v3",
)
chain = prompt | model

chain_with_message_history = RunnableWithMessageHistory(
    runnable=chain,
    get_session_history=lambda session_id: temp_chat_history,
    input_messages_key="input",
    history_messages_key="chat_history",
)

def summarize_messages(chain_input):
    stored_messages = temp_chat_history.messages
    if len(stored_messages) == 0:
        return False
    summarization_prompt = ChatPromptTemplate.from_messages([
            MessagesPlaceholder(variable_name="chat_history"),
            ("user","""请将上述聊天历史总结成一条结构化的背景消息,必须包含以下信息:

【用户基本信息】(必须保留):
- 姓名、身份、职业等个人信息
- 重要的个人偏好和特征

【关键事件和时间】(重要):
- 提到的具体时间、地点
- 重要的事件和活动

【对话要点】(简要):
- 主要讨论的话题
- 用户的需求和问题

请确保用户的姓名等基本信息在每次汇总中都被明确保留,即使看起来不重要也要包含。
总结长度控制在150字以内。""",),
    ])
    summarization_chain = summarization_prompt | model

    msg_long = 10
    msg_cut = -4
    stored_messages = temp_chat_history.messages
    if len(stored_messages) < msg_long:         # 小于10条不处理
        return False
    # 保留最近的4条消息,总结其余的
    recent_messages = stored_messages[msg_cut:]
    messages_to_summarize = stored_messages[:msg_cut]
    
    # 总结较早的消息
    summarization_chain = summarization_prompt | model
    summary_message = summarization_chain.invoke(
        {"chat_history": messages_to_summarize}
    )
    
    # 清空并重新构建历史
    temp_chat_history.clear()
    summary_system_msg = SystemMessage(
        content=summary_message.content
    )
    temp_chat_history.add_message(summary_system_msg)
    for msg in recent_messages:
        temp_chat_history.add_message(msg)
    return True

chain_with_summarization = (
    RunnablePassthrough.assign(messages_summarized=summarize_messages)
    | chain_with_message_history
)

def main():
    while True:
        try:
            user_input = input("\n用户: ").strip()
            if user_input.lower() in ['quit', 'exit', '退出', 'q']:
                print("再见!")
                break
            if not user_input:
                continue
            # 调用AI模型
            response = chain_with_summarization.stream(
                {"input": user_input},
                {"configurable": {"session_id": "unused"}},
            )
            print(f"\nAI: ", end="", flush=True)
            for chunk in response:
                print(chunk.content, end="", flush=True)
            # print(f"\n[当前对话历史: {len(temp_chat_history.messages)} 条消息]")
            # print(temp_chat_history.messages)
       except KeyboardInterrupt:
            print("\n\n程序被用户中断,再见!")
            break
        except Exception as e:
            print(f"\n发生错误: {e}")
            print("请重试...")

if __name__ == "__main__":
    main()

总结

本文整理了 LangChain 中消息管理与聊天历史存储的常见做法,包括使用 RunnableWithMessageHistory 管理内存会话、通过 session_id / user_id / conversation_id 区分不同会话、使用 Redis 实现持久化保存,以及通过消息裁剪和总结记忆减少上下文 token 消耗。实际项目中,可以根据业务规模选择内存、Redis、文件、SQL 或 MongoDB 等不同存储方式,并结合裁剪或总结策略控制历史消息长度。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐