总结之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 中创建 checkpointscheckpoint_writescheckpoint_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 内部的 writesblobs 字典的 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

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