总结之LangGraph Agent 记忆能力

LangGraph Agent 记忆能力学习笔记

一、为什么 Agent 需要记忆

默认情况下,LLM 是无状态的------每次调用都是独立的,不记得之前说过什么。要让 Agent 具备"记忆",需要在每一轮对话之间保存和恢复对话状态。

LangGraph 通过 Checkpointer 机制解决这个问题:
Checkpointer Agent 用户 Checkpointer Agent 用户 #mermaid-svg-sxytHuZWj4MbrzmO{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-sxytHuZWj4MbrzmO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-sxytHuZWj4MbrzmO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-sxytHuZWj4MbrzmO .error-icon{fill:#552222;}#mermaid-svg-sxytHuZWj4MbrzmO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-sxytHuZWj4MbrzmO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-sxytHuZWj4MbrzmO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-sxytHuZWj4MbrzmO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-sxytHuZWj4MbrzmO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-sxytHuZWj4MbrzmO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-sxytHuZWj4MbrzmO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-sxytHuZWj4MbrzmO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-sxytHuZWj4MbrzmO .marker.cross{stroke:#333333;}#mermaid-svg-sxytHuZWj4MbrzmO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-sxytHuZWj4MbrzmO p{margin:0;}#mermaid-svg-sxytHuZWj4MbrzmO .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-sxytHuZWj4MbrzmO text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-sxytHuZWj4MbrzmO .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-sxytHuZWj4MbrzmO .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-sxytHuZWj4MbrzmO .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-sxytHuZWj4MbrzmO .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-sxytHuZWj4MbrzmO #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-sxytHuZWj4MbrzmO .sequenceNumber{fill:white;}#mermaid-svg-sxytHuZWj4MbrzmO #sequencenumber{fill:#333;}#mermaid-svg-sxytHuZWj4MbrzmO #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-sxytHuZWj4MbrzmO .messageText{fill:#333;stroke:none;}#mermaid-svg-sxytHuZWj4MbrzmO .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-sxytHuZWj4MbrzmO .labelText,#mermaid-svg-sxytHuZWj4MbrzmO .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-sxytHuZWj4MbrzmO .loopText,#mermaid-svg-sxytHuZWj4MbrzmO .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-sxytHuZWj4MbrzmO .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-sxytHuZWj4MbrzmO .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-sxytHuZWj4MbrzmO .noteText,#mermaid-svg-sxytHuZWj4MbrzmO .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-sxytHuZWj4MbrzmO .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-sxytHuZWj4MbrzmO .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-sxytHuZWj4MbrzmO .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-sxytHuZWj4MbrzmO .actorPopupMenu{position:absolute;}#mermaid-svg-sxytHuZWj4MbrzmO .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-sxytHuZWj4MbrzmO .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-sxytHuZWj4MbrzmO .actor-man circle,#mermaid-svg-sxytHuZWj4MbrzmO line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-sxytHuZWj4MbrzmO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 你好,我叫 Sam保存对话状态 (put)你好 Sam!我叫什么名字?恢复对话历史 (get_tuple)返回之前的对话记录你叫 Sam

二、核心概念

2.1 Checkpointer

Checkpointer 是 LangGraph 的状态持久化接口,负责:

  • put / put_writes:保存 Agent 的对话状态
  • get_tuple / list:恢复之前的对话历史
  • get_next_version:生成递增的版本号

所有 Checkpointer 都继承自 BaseCheckpointSaver。

2.2 thread_id

thread_id 是会话的唯一标识。相同 thread_id 的调用共享同一份记忆 ,不同 thread_id 互相隔离。

python 复制代码
config = RunnableConfig(configurable={"thread_id": "session-001"})

2.3 使用方式(通用模板)

无论使用哪种 Checkpointer,代码模板都是一样的:

python 复制代码
from langgraph.prebuilt import create_react_agent

# 1. 创建 checkpointer(具体实现不同)
memory = XxxSaver(...)

# 2. 创建 Agent,传入 checkpointer
agent = create_react_agent(model=llm, tools=[], checkpointer=memory)

# 3. 使用相同的 thread_id 进行多轮对话
config = RunnableConfig(configurable={"thread_id": "test-001"})
agent.invoke({"messages": [("user", "你好")]}, config=config)
agent.invoke({"messages": [("user", "你还记得我吗")]}, config=config)  # Agent 有记忆

三、四种持久化方案

#mermaid-svg-rYrcwQrByFYJLLkR{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-rYrcwQrByFYJLLkR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-rYrcwQrByFYJLLkR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-rYrcwQrByFYJLLkR .error-icon{fill:#552222;}#mermaid-svg-rYrcwQrByFYJLLkR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rYrcwQrByFYJLLkR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-rYrcwQrByFYJLLkR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rYrcwQrByFYJLLkR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rYrcwQrByFYJLLkR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-rYrcwQrByFYJLLkR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rYrcwQrByFYJLLkR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rYrcwQrByFYJLLkR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rYrcwQrByFYJLLkR .marker.cross{stroke:#333333;}#mermaid-svg-rYrcwQrByFYJLLkR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rYrcwQrByFYJLLkR p{margin:0;}#mermaid-svg-rYrcwQrByFYJLLkR .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-rYrcwQrByFYJLLkR .cluster-label text{fill:#333;}#mermaid-svg-rYrcwQrByFYJLLkR .cluster-label span{color:#333;}#mermaid-svg-rYrcwQrByFYJLLkR .cluster-label span p{background-color:transparent;}#mermaid-svg-rYrcwQrByFYJLLkR .label text,#mermaid-svg-rYrcwQrByFYJLLkR span{fill:#333;color:#333;}#mermaid-svg-rYrcwQrByFYJLLkR .node rect,#mermaid-svg-rYrcwQrByFYJLLkR .node circle,#mermaid-svg-rYrcwQrByFYJLLkR .node ellipse,#mermaid-svg-rYrcwQrByFYJLLkR .node polygon,#mermaid-svg-rYrcwQrByFYJLLkR .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-rYrcwQrByFYJLLkR .rough-node .label text,#mermaid-svg-rYrcwQrByFYJLLkR .node .label text,#mermaid-svg-rYrcwQrByFYJLLkR .image-shape .label,#mermaid-svg-rYrcwQrByFYJLLkR .icon-shape .label{text-anchor:middle;}#mermaid-svg-rYrcwQrByFYJLLkR .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-rYrcwQrByFYJLLkR .rough-node .label,#mermaid-svg-rYrcwQrByFYJLLkR .node .label,#mermaid-svg-rYrcwQrByFYJLLkR .image-shape .label,#mermaid-svg-rYrcwQrByFYJLLkR .icon-shape .label{text-align:center;}#mermaid-svg-rYrcwQrByFYJLLkR .node.clickable{cursor:pointer;}#mermaid-svg-rYrcwQrByFYJLLkR .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-rYrcwQrByFYJLLkR .arrowheadPath{fill:#333333;}#mermaid-svg-rYrcwQrByFYJLLkR .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-rYrcwQrByFYJLLkR .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-rYrcwQrByFYJLLkR .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rYrcwQrByFYJLLkR .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-rYrcwQrByFYJLLkR .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rYrcwQrByFYJLLkR .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-rYrcwQrByFYJLLkR .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-rYrcwQrByFYJLLkR .cluster text{fill:#333;}#mermaid-svg-rYrcwQrByFYJLLkR .cluster span{color:#333;}#mermaid-svg-rYrcwQrByFYJLLkR div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-rYrcwQrByFYJLLkR .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-rYrcwQrByFYJLLkR rect.text{fill:none;stroke-width:0;}#mermaid-svg-rYrcwQrByFYJLLkR .icon-shape,#mermaid-svg-rYrcwQrByFYJLLkR .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rYrcwQrByFYJLLkR .icon-shape p,#mermaid-svg-rYrcwQrByFYJLLkR .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-rYrcwQrByFYJLLkR .icon-shape .label rect,#mermaid-svg-rYrcwQrByFYJLLkR .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rYrcwQrByFYJLLkR .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-rYrcwQrByFYJLLkR .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-rYrcwQrByFYJLLkR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 自定义实现
FileSaver
本地文件
官方插件
RedisSaver
Redis
PyMySQLSaver
MySQL
内置方案
MemorySaver
内存存储

方案 类名 存储位置 持久化 外部依赖 适用场景
内存记忆 MemorySaver 内存 否 无 开发调试、短期会话
Redis 记忆 RedisSaver Redis 是 Redis Stack 生产环境、跨进程共享
MySQL 记忆 PyMySQLSaver MySQL 是 MySQL 8.0+ 生产环境、结构化存储
文件记忆 FileSaver(自定义) 本地 .pkl 文件 是 无 单机部署、轻量持久化

四、方案一:内存记忆(MemorySaver)

特点

  • 数据存储在内存中,速度快
  • 程序重启后记忆丢失
  • 无需任何外部依赖

代码

python 复制代码
from langgraph.prebuilt import create_react_agent
from langgraph.checkpoint.memory import MemorySaver
from langchain_core.runnables import RunnableConfig

# 创建内存存储器
memory = MemorySaver()

# 创建 Agent
agent = create_react_agent(
    model=llm,
    tools=[],
    checkpointer=memory,
)

# 多轮对话测试
config = RunnableConfig(configurable={"thread_id": "test-session-001"})

# 第一轮:告诉名字
agent.invoke(
    {"messages": [("user", "你好,我叫Sam,请记住我的名字")]},
    config=config,
)

# 第二轮:验证记忆(相同 thread_id,Agent 能回答出 Sam)
agent.invoke(
    {"messages": [("user", "你还记得我叫什么名字吗?")]},
    config=config,
)

# 第三轮:不同 thread_id(新会话,Agent 不知道你是谁)
new_config = RunnableConfig(configurable={"thread_id": "test-session-002"})
agent.invoke(
    {"messages": [("user", "我叫什么名字?")]},
    config=new_config,
)

验证结论

轮次 thread_id Agent 是否记得名字
第一轮 test-session-001 ---
第二轮 test-session-001 记得(Sam)
第三轮 test-session-002 不记得

五、方案二:Redis 持久化记忆(RedisSaver)

特点

  • 数据存储在 Redis 中,程序重启后记忆仍在
  • 支持跨进程、跨机器共享
  • 需要 Redis Stack(包含 RediSearch 模块),普通 Redis 不行

安装依赖

bash 复制代码
pip install langgraph-checkpoint-redis

代码

python 复制代码
from langgraph.prebuilt import create_react_agent
from langgraph.checkpoint.redis import RedisSaver
from langchain_core.runnables import RunnableConfig

REDIS_URL = "redis://127.0.0.1:6379"

# with 语句自动管理连接
with RedisSaver.from_conn_string(REDIS_URL) as memory:
    # 首次使用必须调用 setup() 初始化索引
    memory.setup()

    agent = create_react_agent(
        model=llm,
        tools=[],
        checkpointer=memory,
    )

    config = RunnableConfig(configurable={"thread_id": "redis-test-001"})
    agent.invoke({"messages": [("user", "你好,我叫Sam")]}, config=config)

踩坑记录

普通 Redis 报 FT._LIST 错误

langgraph-checkpoint-redis 依赖 Redis Stack 中的 RediSearch 模块来创建搜索索引。

如果使用普通 Redis(如通过 apt install redis 安装的),执行 memory.setup() 时会报错:

复制代码
redis.exceptions.ResponseError: unknown command 'FT._LIST'

解决方案 :使用 Redis Stack(docker run redis/redis-stack)替代普通 Redis。


六、方案三:MySQL 持久化记忆(PyMySQLSaver)

特点

  • 数据存储在 MySQL 表中,支持 SQL 查询和管理
  • 程序重启后记忆仍在
  • 适合已有 MySQL 基础设施的生产环境

安装依赖

bash 复制代码
pip install langgraph-checkpoint-mysql

代码

python 复制代码
from langgraph.prebuilt import create_react_agent
from langgraph.checkpoint.mysql.pymysql import PyMySQLSaver  # 注意类名
from langchain_core.runnables import RunnableConfig

MYSQL_URL = "mysql://root:123456@127.0.0.1:3306/langgraph_test"

with PyMySQLSaver.from_conn_string(MYSQL_URL) as memory:
    # 首次使用必须调用 setup() 创建数据表
    memory.setup()

    agent = create_react_agent(
        model=llm,
        tools=[],
        checkpointer=memory,
    )

    config = RunnableConfig(configurable={"thread_id": "mysql-test-001"})
    agent.invoke({"messages": [("user", "你好,我叫Sam")]}, config=config)

踩坑记录

类名是 PyMySQLSaver,不是 MySQLSaver

langgraph-checkpoint-mysql 包提供两个类:

  • MySQLSaver:基于 mysql-connector-python(官方驱动)
  • PyMySQLSaver:基于 pymysql(纯 Python 驱动)

如果使用 PyMySQLSaver,导入路径为:

python 复制代码
from langgraph.checkpoint.mysql.pymysql import PyMySQLSaver

setup() 只需调用一次

setup() 会在 MySQL 中创建 checkpoints、checkpoint_writes、checkpoint_blobs 三张表。

首次运行后表已存在,后续可以省略(但重复调用也不会报错)。


七、方案四:文件持久化记忆(自定义 FileSaver)

特点

  • LangGraph 没有内置文件持久化方案,需要自己实现
  • 数据存储在本地 .pkl 文件中,无需外部服务
  • 适合单机部署、轻量级持久化需求

实现原理

#mermaid-svg-ILl1c0L9uaqv9ALP{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ILl1c0L9uaqv9ALP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ILl1c0L9uaqv9ALP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ILl1c0L9uaqv9ALP .error-icon{fill:#552222;}#mermaid-svg-ILl1c0L9uaqv9ALP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ILl1c0L9uaqv9ALP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ILl1c0L9uaqv9ALP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ILl1c0L9uaqv9ALP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ILl1c0L9uaqv9ALP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ILl1c0L9uaqv9ALP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ILl1c0L9uaqv9ALP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ILl1c0L9uaqv9ALP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ILl1c0L9uaqv9ALP .marker.cross{stroke:#333333;}#mermaid-svg-ILl1c0L9uaqv9ALP svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ILl1c0L9uaqv9ALP p{margin:0;}#mermaid-svg-ILl1c0L9uaqv9ALP .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ILl1c0L9uaqv9ALP .cluster-label text{fill:#333;}#mermaid-svg-ILl1c0L9uaqv9ALP .cluster-label span{color:#333;}#mermaid-svg-ILl1c0L9uaqv9ALP .cluster-label span p{background-color:transparent;}#mermaid-svg-ILl1c0L9uaqv9ALP .label text,#mermaid-svg-ILl1c0L9uaqv9ALP span{fill:#333;color:#333;}#mermaid-svg-ILl1c0L9uaqv9ALP .node rect,#mermaid-svg-ILl1c0L9uaqv9ALP .node circle,#mermaid-svg-ILl1c0L9uaqv9ALP .node ellipse,#mermaid-svg-ILl1c0L9uaqv9ALP .node polygon,#mermaid-svg-ILl1c0L9uaqv9ALP .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ILl1c0L9uaqv9ALP .rough-node .label text,#mermaid-svg-ILl1c0L9uaqv9ALP .node .label text,#mermaid-svg-ILl1c0L9uaqv9ALP .image-shape .label,#mermaid-svg-ILl1c0L9uaqv9ALP .icon-shape .label{text-anchor:middle;}#mermaid-svg-ILl1c0L9uaqv9ALP .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ILl1c0L9uaqv9ALP .rough-node .label,#mermaid-svg-ILl1c0L9uaqv9ALP .node .label,#mermaid-svg-ILl1c0L9uaqv9ALP .image-shape .label,#mermaid-svg-ILl1c0L9uaqv9ALP .icon-shape .label{text-align:center;}#mermaid-svg-ILl1c0L9uaqv9ALP .node.clickable{cursor:pointer;}#mermaid-svg-ILl1c0L9uaqv9ALP .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ILl1c0L9uaqv9ALP .arrowheadPath{fill:#333333;}#mermaid-svg-ILl1c0L9uaqv9ALP .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ILl1c0L9uaqv9ALP .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ILl1c0L9uaqv9ALP .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ILl1c0L9uaqv9ALP .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ILl1c0L9uaqv9ALP .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ILl1c0L9uaqv9ALP .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ILl1c0L9uaqv9ALP .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ILl1c0L9uaqv9ALP .cluster text{fill:#333;}#mermaid-svg-ILl1c0L9uaqv9ALP .cluster span{color:#333;}#mermaid-svg-ILl1c0L9uaqv9ALP div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ILl1c0L9uaqv9ALP .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ILl1c0L9uaqv9ALP rect.text{fill:none;stroke-width:0;}#mermaid-svg-ILl1c0L9uaqv9ALP .icon-shape,#mermaid-svg-ILl1c0L9uaqv9ALP .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ILl1c0L9uaqv9ALP .icon-shape p,#mermaid-svg-ILl1c0L9uaqv9ALP .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ILl1c0L9uaqv9ALP .icon-shape .label rect,#mermaid-svg-ILl1c0L9uaqv9ALP .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ILl1c0L9uaqv9ALP .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ILl1c0L9uaqv9ALP .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ILl1c0L9uaqv9ALP :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 读取流程
get_tuple / list
从 .pkl 文件加载
恢复到 InMemorySaver 内存
返回数据
写入流程
put / put_writes
委托给 InMemorySaver
pickle.dump 保存到 .pkl 文件
FileSaver
内部复用 InMemorySaver
写入后序列化到文件
启动时从文件加载

关键设计决策

决策 选择 原因
序列化方式 pickle 二进制 JSON/base64 无法序列化 bytes 等复杂对象
文件粒度 每个 thread_id 一个文件 隔离性好,互不影响
内存逻辑 复用 InMemorySaver 避免重复实现复杂的内存操作

代码:完整 FileSaver 实现

python 复制代码
import os, pickle, random
from typing import Any, Optional, Sequence
from contextlib import AbstractContextManager

from langgraph.checkpoint.base import (
    BaseCheckpointSaver, Checkpoint, CheckpointMetadata,
    CheckpointTuple, ChannelVersions,
)
from langgraph.checkpoint.memory import InMemorySaver
from langchain_core.runnables import RunnableConfig


class FileSaver(BaseCheckpointSaver, AbstractContextManager):
    """基于文件的 Checkpoint 持久化器"""

    def __init__(self, storage_dir: str = "./checkpoint_data"):
        super().__init__()
        self.storage_dir = storage_dir
        os.makedirs(storage_dir, exist_ok=True)
        self._memory = InMemorySaver()        # 内部复用内存逻辑
        self._loaded_threads: set[str] = set() # 避免重复加载

    def _get_checkpoint_path(self, thread_id: str) -> str:
        safe_id = thread_id.replace("/", "_").replace("\\", "_")
        return os.path.join(self.storage_dir, f"checkpoint_{safe_id}.pkl")

    def _save_to_file(self, thread_id: str) -> None:
        """pickle 二进制序列化到文件"""
        path = self._get_checkpoint_path(thread_id)
        data = {
            "thread_id": thread_id,
            "storage": dict(self._memory.storage.get(thread_id, {})),
            "writes": {k: v for k, v in self._memory.writes.items() if k[0] == thread_id},
            "blobs": {k: v for k, v in self._memory.blobs.items() if k[0] == thread_id},
        }
        with open(path, "wb") as f:
            pickle.dump(data, f)

    def _load_from_file(self, thread_id: str) -> None:
        """从文件加载到内存"""
        if thread_id in self._loaded_threads:
            return
        path = self._get_checkpoint_path(thread_id)
        if not os.path.exists(path):
            self._loaded_threads.add(thread_id)
            return
        with open(path, "rb") as f:
            data = pickle.load(f)
        if data.get("storage"):
            self._memory.storage[thread_id] = data["storage"]
        for k, v in data.get("writes", {}).items():
            self._memory.writes[k] = v
        for k, v in data.get("blobs", {}).items():
            self._memory.blobs[k] = v
        self._loaded_threads.add(thread_id)

    # ---- 核心接口 ----
    def put(self, config, checkpoint, metadata, new_versions):
        thread_id = config["configurable"]["thread_id"]
        self._load_from_file(thread_id)
        result = self._memory.put(config, checkpoint, metadata, new_versions)
        self._save_to_file(thread_id)
        return result

    def put_writes(self, config, writes, task_id, task_path=""):
        thread_id = config["configurable"]["thread_id"]
        self._load_from_file(thread_id)
        self._memory.put_writes(config, writes, task_id, task_path)
        self._save_to_file(thread_id)

    def get_tuple(self, config):
        thread_id = config["configurable"]["thread_id"]
        self._load_from_file(thread_id)
        return self._memory.get_tuple(config)

    def list(self, config, *, filter=None, before=None, limit=None):
        if config:
            thread_id = config["configurable"]["thread_id"]
            self._load_from_file(thread_id)
        return self._memory.list(config, filter=filter, before=before, limit=limit)

    def get_next_version(self, current, channel):
        current_v = 0 if current is None else int(str(current).split(".")[0])
        return f"{current_v + 1:032}.{random.random():016}"

    # ---- 异步方法(直接委托给同步方法)----
    async def aget_tuple(self, config):     return self.get_tuple(config)
    async def aput(self, config, checkpoint, metadata, new_versions):
        return self.put(config, checkpoint, metadata, new_versions)
    async def aput_writes(self, config, writes, task_id, task_path=""):
        return self.put_writes(config, writes, task_id, task_path)

    def __exit__(self, *args): pass

使用方式

python 复制代码
memory = FileSaver(storage_dir="./checkpoint_data")
agent = create_react_agent(model=llm, tools=[], checkpointer=memory)

config = RunnableConfig(configurable={"thread_id": "file-test-001"})
agent.invoke({"messages": [("user", "你好,我叫Sam")]}, config=config)

踩坑记录

base64 + JSON 序列化失败

最初尝试用 base64 + JSON 序列化,但 InMemorySaver 内部的 writes 和 blobs 字典的 key 是 tuple 类型(包含 bytes),JSON 不支持 tuple key,导致序列化时报 Incorrect padding 错误。

最终方案:使用 pickle 二进制序列化,原生支持所有 Python 对象。


八、自定义 Checkpointer 实现指南

如果你需要实现自己的 Checkpointer(如 MongoDB、SQLite 等),需要实现以下接口:
#mermaid-svg-Ge2gVjclaZdC8nfo{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Ge2gVjclaZdC8nfo .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Ge2gVjclaZdC8nfo .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Ge2gVjclaZdC8nfo .error-icon{fill:#552222;}#mermaid-svg-Ge2gVjclaZdC8nfo .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Ge2gVjclaZdC8nfo .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Ge2gVjclaZdC8nfo .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Ge2gVjclaZdC8nfo .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Ge2gVjclaZdC8nfo .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Ge2gVjclaZdC8nfo .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Ge2gVjclaZdC8nfo .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Ge2gVjclaZdC8nfo .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Ge2gVjclaZdC8nfo .marker.cross{stroke:#333333;}#mermaid-svg-Ge2gVjclaZdC8nfo svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Ge2gVjclaZdC8nfo p{margin:0;}#mermaid-svg-Ge2gVjclaZdC8nfo .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Ge2gVjclaZdC8nfo .cluster-label text{fill:#333;}#mermaid-svg-Ge2gVjclaZdC8nfo .cluster-label span{color:#333;}#mermaid-svg-Ge2gVjclaZdC8nfo .cluster-label span p{background-color:transparent;}#mermaid-svg-Ge2gVjclaZdC8nfo .label text,#mermaid-svg-Ge2gVjclaZdC8nfo span{fill:#333;color:#333;}#mermaid-svg-Ge2gVjclaZdC8nfo .node rect,#mermaid-svg-Ge2gVjclaZdC8nfo .node circle,#mermaid-svg-Ge2gVjclaZdC8nfo .node ellipse,#mermaid-svg-Ge2gVjclaZdC8nfo .node polygon,#mermaid-svg-Ge2gVjclaZdC8nfo .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Ge2gVjclaZdC8nfo .rough-node .label text,#mermaid-svg-Ge2gVjclaZdC8nfo .node .label text,#mermaid-svg-Ge2gVjclaZdC8nfo .image-shape .label,#mermaid-svg-Ge2gVjclaZdC8nfo .icon-shape .label{text-anchor:middle;}#mermaid-svg-Ge2gVjclaZdC8nfo .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Ge2gVjclaZdC8nfo .rough-node .label,#mermaid-svg-Ge2gVjclaZdC8nfo .node .label,#mermaid-svg-Ge2gVjclaZdC8nfo .image-shape .label,#mermaid-svg-Ge2gVjclaZdC8nfo .icon-shape .label{text-align:center;}#mermaid-svg-Ge2gVjclaZdC8nfo .node.clickable{cursor:pointer;}#mermaid-svg-Ge2gVjclaZdC8nfo .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Ge2gVjclaZdC8nfo .arrowheadPath{fill:#333333;}#mermaid-svg-Ge2gVjclaZdC8nfo .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Ge2gVjclaZdC8nfo .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Ge2gVjclaZdC8nfo .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Ge2gVjclaZdC8nfo .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Ge2gVjclaZdC8nfo .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Ge2gVjclaZdC8nfo .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Ge2gVjclaZdC8nfo .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Ge2gVjclaZdC8nfo .cluster text{fill:#333;}#mermaid-svg-Ge2gVjclaZdC8nfo .cluster span{color:#333;}#mermaid-svg-Ge2gVjclaZdC8nfo div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Ge2gVjclaZdC8nfo .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Ge2gVjclaZdC8nfo rect.text{fill:none;stroke-width:0;}#mermaid-svg-Ge2gVjclaZdC8nfo .icon-shape,#mermaid-svg-Ge2gVjclaZdC8nfo .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Ge2gVjclaZdC8nfo .icon-shape p,#mermaid-svg-Ge2gVjclaZdC8nfo .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Ge2gVjclaZdC8nfo .icon-shape .label rect,#mermaid-svg-Ge2gVjclaZdC8nfo .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Ge2gVjclaZdC8nfo .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Ge2gVjclaZdC8nfo .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Ge2gVjclaZdC8nfo :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} BaseCheckpointSaver
put - 保存 checkpoint
put_writes - 保存 writes
get_tuple - 获取最新 checkpoint
list - 列出 checkpoint 列表
get_next_version - 生成版本号
aget_tuple - 异步获取
aput - 异步保存
aput_writes - 异步保存 writes

方法 作用 必须实现
put(config, checkpoint, metadata, new_versions) 保存对话状态 是
put_writes(config, writes, task_id, task_path) 保存中间写入 是
get_tuple(config) 获取最新状态 是
list(config, filter, before, limit) 列出历史状态 是
get_next_version(current, channel) 生成递增版本号 是
aget_tuple / aput / aput_writes 异步版本 是

实现模板

最简单的实现方式是内部复用 InMemorySaver,只额外处理序列化/反序列化:

python 复制代码
class MyCustomSaver(BaseCheckpointSaver, AbstractContextManager):
    def __init__(self):
        super().__init__()
        self._memory = InMemorySaver()

    def put(self, config, checkpoint, metadata, new_versions):
        result = self._memory.put(config, checkpoint, metadata, new_versions)
        self._persist(config)   # 你的持久化逻辑
        return result

    def get_tuple(self, config):
        self._restore(config)    # 你的恢复逻辑
        return self._memory.get_tuple(config)

    # ... 其他方法同理

九、方案对比总结

维度 MemorySaver RedisSaver PyMySQLSaver FileSaver(自定义)
持久化 否 是 是 是
外部依赖 无 Redis Stack MySQL 8.0+ 无
跨进程共享 否 是 是 否
性能 最快 快 中等 中等(IO 开销)
实现复杂度 无需实现 安装即用 安装即用 需自己实现
数据可查询 否 有限 SQL 查询 需读文件
推荐场景 开发调试 高并发生产 已有 MySQL 基础设施 单机轻量部署

十、依赖安装速查

bash 复制代码
# 内存记忆(随 langgraph 安装)
pip install langgraph

# Redis 持久化
pip install langgraph-checkpoint-redis
# 注意:需要 Redis Stack(含 RediSearch),普通 Redis 不行

# MySQL 持久化
pip install langgraph-checkpoint-mysql
# 注意:类名是 PyMySQLSaver,不是 MySQLSaver

# 文件持久化
# 无额外依赖,自定义实现