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

技能点

  1. 消息存储在内存
  2. 消息持久化到redis
  3. 修改聊天历史
  4. 裁剪消息

消息存储在内存

下面我们展示一个简单的示例,其中聊天历史保存在内存中,默认情况下,期望配置参数是一个字符串thread_id。

使用单参数默认值:

python 复制代码
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_deepseek import ChatDeepSeek
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import END, START, MessagesState, StateGraph

model = ChatDeepSeek(model="deepseek-chat")


# MessagesState是LangGraph的内置状态,里面最关键的是messages(消息列表)
# 然后又扩展了一个字段alibity,用于系统提示词(比如:math)
# 之后在运行时,会围绕这个状态读写
class ChatState(MessagesState):
    ability: str


def call_model(state: ChatState):
    # 从状态里读取ability字段,拼出系统提示词
    system_message = SystemMessage(
        content=f"你是一个{state.get('ability', 'general')}的助手,响应20字以内"
    )
    # 把系统提示+历史/本轮消息一起发给模型
    response = model.invoke([system_message, *state["messages"]])
    # 返回 {"messages": [response]},把模型回复追加回状态里的messages
    # 也就是说:每轮都会把AI回复写会会话消息历史
    return {"messages": [response]}


# 这是单节点流程,START-chatbot-END
# 每次调用就走一遍这个流程
# 搭建执行流程图
# 创建一个状态图构建器,声明整张图的状态结构是 ChatState
builder = StateGraph(ChatState)
# 注册一个节点,名字叫chatbot,执行逻辑是call_model函数
builder.add_node("chatbot", call_model)
# 定义起点:图开始时先进入chatbot节点
builder.add_edge(START, "chatbot")
# 定义终点:chatbot执行完结束本次流程
builder.add_edge("chatbot", END)
# 合起来就是一个最简单的单节点流程:START-chatbot-END

# 这是记住历史的核心
# MemorySaver(): 把每次运行后的状态做 checkpoint (内存里)。
graph = builder.compile(checkpointer=MemorySaver())
# 同一个会话id,下一次你在用同一个thread_id调graph.invoke()时
# LangGraph会先把这条会话之前保存的状态取出来,在合并本次输入继续跑
config = {"configurable": {"thread_id": "123"}}
response_1 = graph.invoke(
    {"ability": "得力", "messages": [HumanMessage(content="我叫小明。")]},
    config=config,
)
print("第一轮:", response_1["messages"][-1].content)

response_2 = graph.invoke(
    {"ability": "得力", "messages": [HumanMessage(content="我叫啥?")]},
    config=config,
)
print("第二轮:", response_2["messages"][-1].content)

config = {"configurable": {"thread_id": "456"}}
response_3 = graph.invoke(
    {"ability": "得力", "messages": [HumanMessage(content="我叫啥?")]},
    config=config,
)
print("第三轮:", response_3["messages"][-1].content)

本次调用完成后,状态里会有这轮用户消息+模型回复,并按thread_id保存起来

记住历史的本质

  1. 同一个thread_id
  2. 每轮把messages写回状态
  3. checkpointer保存并在下一轮恢复状态

三者组合形成会话记忆

一个重要的实现点:

现在是用的是MemorySaver,它是进程内存,脚本不退出时能记住,脚本重启后,记忆丢失

如果想要重启后也记住,需要持久化checkpointer(比如:sqlite、postgres对应实现)

配置会话唯一键

下面我们使用了两个参数:user_id和thread_id

配置user_id和thread_id作为会话唯一键

python 复制代码
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_deepseek import ChatDeepSeek
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import END, START, MessagesState, StateGraph

model = ChatDeepSeek(model="deepseek-chat")


# MessagesState是LangGraph的内置状态,里面最关键的是messages(消息列表)
# 然后又扩展了一个字段alibity,用于系统提示词(比如:math)
# 之后在运行时,会围绕这个状态读写
class ChatState(MessagesState):
    ability: str


def call_model(state: ChatState):
    # 从状态里读取ability字段,拼出系统提示词
    system_message = SystemMessage(
        content=f"你是一个{state.get('ability', 'general')}的助手,响应20字以内"
    )
    # 把系统提示+历史/本轮消息一起发给模型
    response = model.invoke([system_message, *state["messages"]])
    # 返回 {"messages": [response]},把模型回复追加回状态里的messages
    # 也就是说:每轮都会把AI回复写会会话消息历史
    return {"messages": [response]}


# 这是单节点流程,START-chatbot-END
# 每次调用就走一遍这个流程
# 搭建执行流程图
# 创建一个状态图构建器,声明整张图的状态结构是 ChatState
builder = StateGraph(ChatState)
# 注册一个节点,名字叫chatbot,执行逻辑是call_model函数
builder.add_node("chatbot", call_model)
# 定义起点:图开始时先进入chatbot节点
builder.add_edge(START, "chatbot")
# 定义终点:chatbot执行完结束本次流程
builder.add_edge("chatbot", END)
# 合起来就是一个最简单的单节点流程:START-chatbot-END

# 这是记住历史的核心
# MemorySaver(): 把每次运行后的状态做 checkpoint (内存里)。
graph = builder.compile(checkpointer=MemorySaver())

def build_config(user_id: str, thread_id: str) -> dict:
    # 用 user_id + thread_id 组合成唯一会话键,避免不同用户的同名线程串历史
    session_key = f"{user_id}:{thread_id}"
    return {"configurable": {"thread_id": session_key}}


# 同一用户同一线程:会复用历史
config = build_config(user_id="u_001", thread_id="t_123")
response_1 = graph.invoke(
    {"ability": "得力", "messages": [HumanMessage(content="我叫小明。")]},
    config=config,
)
print("第一轮:", response_1["messages"][-1].content)

response_2 = graph.invoke(
    {"ability": "得力", "messages": [HumanMessage(content="我叫啥?")]},
    config=config,
)
print("第二轮:", response_2["messages"][-1].content)

# 同一用户不同线程:不复用上面的历史
config = build_config(user_id="u_001", thread_id="t_456")
response_3 = graph.invoke(
    {"ability": "得力", "messages": [HumanMessage(content="我叫啥?")]},
    config=config,
)
print("第三轮:", response_3["messages"][-1].content)

# 不同用户同一线程号:也不会串历史
config = build_config(user_id="u_002", thread_id="t_123")
response_4 = graph.invoke(
    {"ability": "得力", "messages": [HumanMessage(content="我叫啥?")]},
    config=config,
)
print("第四轮:", response_4["messages"][-1].content)

消息持久化

请查看 memory integrations 页面,了解使用 Redis 和其他提供程序实现聊天消息历史的方法。

配置redis环境

启动redis服务,调用聊天接口,看Redis是否存储历史记录

安装redis依赖:pip install langgraph-checkpoint-redis redis

python 复制代码
import os
import atexit

from langchain_core.messages import HumanMessage, SystemMessage
from langchain_deepseek import ChatDeepSeek
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import END, START, MessagesState, StateGraph

model = ChatDeepSeek(model="deepseek-chat")


# MessagesState是LangGraph的内置状态,里面最关键的是messages(消息列表)
# 然后又扩展了一个字段alibity,用于系统提示词(比如:math)
# 之后在运行时,会围绕这个状态读写
class ChatState(MessagesState):
    ability: str


def call_model(state: ChatState):
    # 从状态里读取ability字段,拼出系统提示词
    system_message = SystemMessage(
        content=f"你是一个{state.get('ability', 'general')}的助手,响应20字以内"
    )
    # 把系统提示+历史/本轮消息一起发给模型
    response = model.invoke([system_message, *state["messages"]])
    # 返回 {"messages": [response]},把模型回复追加回状态里的messages
    # 也就是说:每轮都会把AI回复写会会话消息历史
    return {"messages": [response]}


# 这是单节点流程,START-chatbot-END
# 每次调用就走一遍这个流程
# 搭建执行流程图
# 创建一个状态图构建器,声明整张图的状态结构是 ChatState
builder = StateGraph(ChatState)
# 注册一个节点,名字叫chatbot,执行逻辑是call_model函数
builder.add_node("chatbot", call_model)
# 定义起点:图开始时先进入chatbot节点
builder.add_edge(START, "chatbot")
# 定义终点:chatbot执行完结束本次流程
builder.add_edge("chatbot", END)
# 合起来就是一个最简单的单节点流程:START-chatbot-END

_redis_checkpointer_cm = None


def _close_redis_checkpointer():
    global _redis_checkpointer_cm
    if _redis_checkpointer_cm is not None:
        _redis_checkpointer_cm.__exit__(None, None, None)
        _redis_checkpointer_cm = None


def build_checkpointer():
    # 优先使用 Redis 做持久化;如果本地未安装 Redis checkpointer,则回退到内存。
    global _redis_checkpointer_cm
    redis_url = os.getenv("REDIS_URL", "redis://localhost:6379/0")
    try:
        from langgraph.checkpoint.redis import RedisSaver  # type: ignore[reportMissingImports]

        checkpointer_or_cm = RedisSaver.from_conn_string(redis_url)
        # 某些版本返回上下文管理器,需要先 enter 拿到真正的 saver。
        if hasattr(checkpointer_or_cm, "__enter__") and hasattr(checkpointer_or_cm, "__exit__"):
            _redis_checkpointer_cm = checkpointer_or_cm
            checkpointer = _redis_checkpointer_cm.__enter__()
            atexit.register(_close_redis_checkpointer)
        else:
            checkpointer = checkpointer_or_cm
        # 某些实现需要先初始化索引/结构,存在 setup 方法时执行一次。
        if hasattr(checkpointer, "setup"):
            checkpointer.setup()
        print(f"已启用 Redis 持久化: {redis_url}")
        return checkpointer
    except Exception as exc:
        print(f"Redis checkpointer 不可用,回退到内存模式: {exc}")
        return MemorySaver()


# 这是记住历史的核心:checkpointer 负责按 thread_id 保存/恢复状态。
graph = builder.compile(checkpointer=build_checkpointer())

def build_config(user_id: str, thread_id: str) -> dict:
    # 用 user_id + thread_id 组合成唯一会话键,避免不同用户的同名线程串历史
    session_key = f"{user_id}:{thread_id}"
    return {"configurable": {"thread_id": session_key}}


# 同一用户同一线程:会复用历史
config = build_config(user_id="u_001", thread_id="t_123")
response_1 = graph.invoke(
    {"ability": "得力", "messages": [HumanMessage(content="我叫小明。")]},
    config=config,
)
print("第一轮:", response_1["messages"][-1].content)

response_2 = graph.invoke(
    {"ability": "得力", "messages": [HumanMessage(content="我叫啥?")]},
    config=config,
)
print("第二轮:", response_2["messages"][-1].content)

# 同一用户不同线程:不复用上面的历史
config = build_config(user_id="u_001", thread_id="t_456")
response_3 = graph.invoke(
    {"ability": "得力", "messages": [HumanMessage(content="我叫啥?")]},
    config=config,
)
print("第三轮:", response_3["messages"][-1].content)

# 不同用户同一线程号:也不会串历史
config = build_config(user_id="u_002", thread_id="t_123")
response_4 = graph.invoke(
    {"ability": "得力", "messages": [HumanMessage(content="我叫啥?")]},
    config=config,
)
print("第四轮:", response_4["messages"][-1].content)

执行过程中,没有走持久化redis,走的内存,查了下原因是因为需要这个报错的根因是:你现在连的是普通 Redis,但 langgraph 的 Redis checkpointer 需要 RediSearch 命令(FT.*)。
FT._LIST 不存在就说明当前实例没装 RediSearch 模块。

所有打算换 Redis Stack服务,

  1. 第一步安装windows下安装了docker desktop

  2. docker启动redis stack

    shell 复制代码
     docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latest

    这样启动完redis之后,不仅有服务,还有一个web界面8001端口,可以访问redis,还是蛮方便的。

  3. 在次执行脚本,正常可以持久化记住历史记忆了。

修改聊天历史

修改存储的聊天消息可以帮助您的聊天机器人处理各种情况。以下是一些示例:

裁剪消息

LLM 和聊天模型有限的上下文窗口,即使您没有直接达到限制,您可能也希望限制模型处理的干扰量。一种解决方案是只加载和存储最近的 n 条消息。让我们使用一个带有一些预加载消息的示例历史记录:

python 复制代码
import os
import atexit

from langchain_core.messages import HumanMessage, RemoveMessage, SystemMessage
from langchain_deepseek import ChatDeepSeek
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import END, START, MessagesState, StateGraph

model = ChatDeepSeek(model="deepseek-chat")


# MessagesState是LangGraph的内置状态,里面最关键的是messages(消息列表)
# 然后又扩展了一个字段alibity,用于系统提示词(比如:math)
# 之后在运行时,会围绕这个状态读写
class ChatState(MessagesState):
    ability: str


MAX_ROUNDS = 3
MAX_MESSAGES = MAX_ROUNDS * 2


def _trim_to_recent_rounds(messages: list, max_messages: int = MAX_MESSAGES) -> list:
    """删除超出窗口的旧消息,仅保留最近 max_messages 条。"""
    if len(messages) <= max_messages:
        return []
    old_messages = messages[: -max_messages]
    # LangGraph 通过 RemoveMessage 实现"从状态中删除指定消息"
    return [RemoveMessage(id=msg.id) for msg in old_messages]


def call_model(state: ChatState):
    # 从状态里读取ability字段,拼出系统提示词
    system_message = SystemMessage(
        content=f"你是一个{state.get('ability', 'general')}的助手,响应20字以内"
    )
    # 把系统提示+最近窗口内历史/本轮消息一起发给模型
    recent_messages = state["messages"][-MAX_MESSAGES:]
    response = model.invoke([system_message, *recent_messages])
    # 追加本轮回复后,再把旧消息删掉,保证持久化中最多只保留最近 3 轮。
    remove_updates = _trim_to_recent_rounds([*state["messages"], response])
    # 返回 {"messages": [response]},把模型回复追加回状态里的messages
    # 也就是说:每轮都会把AI回复写回会话消息历史
    # 此处的messages这个字段在LangGraph离是"增量更新指令",不是整份最终消息列表
    # return {"messages": [*remove_updates, response]}的含义是:
    # remove_updates里每个元素是RemoveMessage(id=...), 表示把这些旧消息从状态里删除掉
    # response是新增一条AI回复,表示把这条消息追加到状态里
    # 是把删除动作提交给系统,如果只返回response,旧消息不回被删除,历史会一直累积
    # 可以理解成一次事务:先删除旧的,在增加新的
    return {"messages": [*remove_updates, response]}


# 这是单节点流程,START-chatbot-END
# 每次调用就走一遍这个流程
# 搭建执行流程图
# 创建一个状态图构建器,声明整张图的状态结构是 ChatState
builder = StateGraph(ChatState)
# 注册一个节点,名字叫chatbot,执行逻辑是call_model函数
builder.add_node("chatbot", call_model)
# 定义起点:图开始时先进入chatbot节点
builder.add_edge(START, "chatbot")
# 定义终点:chatbot执行完结束本次流程
builder.add_edge("chatbot", END)
# 合起来就是一个最简单的单节点流程:START-chatbot-END

_redis_checkpointer_cm = None


def _close_redis_checkpointer():
    global _redis_checkpointer_cm
    if _redis_checkpointer_cm is not None:
        _redis_checkpointer_cm.__exit__(None, None, None)
        _redis_checkpointer_cm = None


def build_checkpointer():
    # 优先使用 Redis 做持久化;如果本地未安装 Redis checkpointer,则回退到内存。
    global _redis_checkpointer_cm
    redis_url = os.getenv("REDIS_URL", "redis://localhost:6379/0")
    try:
        from langgraph.checkpoint.redis import RedisSaver  # type: ignore[reportMissingImports]

        checkpointer_or_cm = RedisSaver.from_conn_string(redis_url)
        # 某些版本返回上下文管理器,需要先 enter 拿到真正的 saver。
        if hasattr(checkpointer_or_cm, "__enter__") and hasattr(checkpointer_or_cm, "__exit__"):
            _redis_checkpointer_cm = checkpointer_or_cm
            checkpointer = _redis_checkpointer_cm.__enter__()
            atexit.register(_close_redis_checkpointer)
        else:
            checkpointer = checkpointer_or_cm
        # 某些实现需要先初始化索引/结构,存在 setup 方法时执行一次。
        if hasattr(checkpointer, "setup"):
            checkpointer.setup()
        print(f"已启用 Redis 持久化: {redis_url}")
        return checkpointer
    except Exception as exc:
        print(f"Redis checkpointer 不可用,回退到内存模式: {exc}")
        return MemorySaver()


# 这是记住历史的核心:checkpointer 负责按 thread_id 保存/恢复状态。
graph = builder.compile(checkpointer=build_checkpointer())

def build_config(user_id: str, thread_id: str) -> dict:
    # 用 user_id + thread_id 组合成唯一会话键,避免不同用户的同名线程串历史
    session_key = f"{user_id}:{thread_id}"
    return {"configurable": {"thread_id": session_key}}


# 同一用户同一线程:会复用历史
config = build_config(user_id="u_001", thread_id="t_123")
response_1 = graph.invoke(
    {"ability": "得力", "messages": [HumanMessage(content="我叫小明。")]},
    config=config,
)
print("第一轮:", response_1["messages"][-1].content)

response_2 = graph.invoke(
    {"ability": "得力", "messages": [HumanMessage(content="我叫啥?")]},
    config=config,
)
print("第二轮:", response_2["messages"][-1].content)

response_3 = graph.invoke(
    {"ability": "得力", "messages": [HumanMessage(content="今天天气是晴天的,38度。")]},
    config=config,
)
print("第三轮:", response_3["messages"][-1].content)

response_4 = graph.invoke(
    {"ability": "得力", "messages": [HumanMessage(content="我喜欢打篮球。")]},
    config=config,
)
print("第四轮:", response_4["messages"][-1].content)

response_5 = graph.invoke(
    {"ability": "得力", "messages": [HumanMessage(content="我喜欢啥?")]},
    config=config,
)
print("第五轮:", response_5["messages"][-1].content)

response_6 = graph.invoke(
    {"ability": "得力", "messages": [HumanMessage(content="我叫啥?")]},
    config=config,
)
print("第六轮:", response_6["messages"][-1].content)

# 测试持久化消息是否真正保存了最近3轮
state_snapshot = graph.get_state(config)
persisted_messages = state_snapshot.values.get("messages", [])
print(f"持久化消息总数: {len(persisted_messages)}(预期 <= {MAX_MESSAGES})")
for idx, msg in enumerate(persisted_messages, start=1):
    msg_type = getattr(msg, "type", msg.__class__.__name__)
    print(f"{idx:02d}. {msg_type}: {msg.content}")

保留了最近3轮的历史对话,第六轮询问叫啥时,大模型已经不知道了。

如何验证redis里的最新状态确实只剩3轮

新建一个脚本,只读取同一个 thread_id的状态,不在invoke, redis_view.py:

python 复制代码
from chat_history_message import graph, build_config, MAX_MESSAGES

config = build_config(user_id="u_001", thread_id="t_123")
state = graph.get_state(config)
msgs = state.values.get("messages", [])
print("从Redis恢复的消息数:", len(msgs), "预期<=", MAX_MESSAGES)
for i, m in enumerate(msgs, 1):
    print(i, getattr(m, "type", m.__class__.__name__), m.content)

结果是拿到了最新的3轮对话,六条消息

text 复制代码
已启用 Redis 持久化: redis://localhost:6379/0
从Redis恢复的消息数: 6 预期<= 6
1 human 我喜欢打篮球。
2 ai 好运动,注意补水。
3 human 我喜欢啥?
4 ai 你喜欢打篮球。
5 human 我叫啥?
6 ai 抱歉,您尚未告知您的姓名。

说明 Redis 中"最新可恢复状态"确实已经被裁剪成最近3轮。

redis为啥不显示执行DEL

因为checkpoint的模型是:

  • 每轮写入一个新的checkpoint(包含应用了删除操作后的新状态)
  • 读取时取 "最新checkpoint"

所以旧的checkpoint可能还在(用于追溯历史版本),但当前会话恢复看到的状态已经是裁剪后的。

相关推荐
唐璜Taro1 天前
LangChain与LangGraph多Agent实战:从工具链到工作流编排(上)
langchain·agent·langgraph
唐璜Taro1 天前
LangChain与LangGraph多Agent实战:从工具链到工作流编排(下)
langchain·langgraph
嗯、.2 天前
Agent 路由架构的一次尝试:LangGraph + Swarm Handoff + 小模型 Router
人工智能·python·swarm·langgraph·multi-agent·model-routing
情绪总是阴雨天~5 天前
深度解析:LangChain、Agent、RAG、FC、ReAct、LangGraph、A2A、MCP — 区别、联系与全景图
python·langchain·agent·rag·langgraph·mcp·a2a
花千树-0105 天前
SubAgent 基础:拥有自主工具的子代理
java·langchain·llm·agent·langgraph·subagent·harness
情绪总是阴雨天~6 天前
深入理解A2A协议:从零搭建多Agent协作系统实战
python·langchain·langgraph·a2a
nix.gnehc7 天前
LangX实战:从Spring生态理解LLM应用开发
人工智能·langchain·langgraph·langfuse
程序员果子7 天前
LangGraph :构建复杂有状态智能体的核心框架
人工智能·python·架构·langchain·prompt·ai编程·langgraph
ftpeak7 天前
LangGraph Agent 开发指南(12~函数式 API)
人工智能·python·ai·langchain·langgraph