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,导入路径为:
pythonfrom 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
# 文件持久化
# 无额外依赖,自定义实现