LangGraph Agent 记忆能力学习笔记
一、为什么 Agent 需要记忆
默认情况下,LLM 是无状态的——每次调用都是独立的,不记得之前说过什么。要让 Agent 具备"记忆",需要在每一轮对话之间保存和恢复对话状态。
LangGraph 通过 Checkpointer 机制解决这个问题:
二、核心概念
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 互相隔离。
config = RunnableConfig(configurable={"thread_id": "session-001"})
2.3 使用方式(通用模板)
无论使用哪种 Checkpointer,代码模板都是一样的:
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 有记忆
三、四种持久化方案
| 方案 | 类名 | 存储位置 | 持久化 | 外部依赖 | 适用场景 |
|---|---|---|---|---|---|
| 内存记忆 | MemorySaver | 内存 | 否 | 无 | 开发调试、短期会话 |
| Redis 记忆 | RedisSaver | Redis | 是 | Redis Stack | 生产环境、跨进程共享 |
| MySQL 记忆 | PyMySQLSaver | MySQL | 是 | MySQL 8.0+ | 生产环境、结构化存储 |
| 文件记忆 | FileSaver(自定义) | 本地 .pkl 文件 | 是 | 无 | 单机部署、轻量持久化 |
四、方案一:内存记忆(MemorySaver)
特点
- 数据存储在内存中,速度快
- 程序重启后记忆丢失
- 无需任何外部依赖
代码
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 不行
安装依赖
pip install langgraph-checkpoint-redis
代码
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 基础设施的生产环境
安装依赖
pip install langgraph-checkpoint-mysql
代码
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,导入路径为:from langgraph.checkpoint.mysql.pymysql import PyMySQLSaver
setup() 只需调用一次
setup()会在 MySQL 中创建checkpoints、checkpoint_writes、checkpoint_blobs三张表。
首次运行后表已存在,后续可以省略(但重复调用也不会报错)。
七、方案四:文件持久化记忆(自定义 FileSaver)
特点
- LangGraph 没有内置文件持久化方案,需要自己实现
- 数据存储在本地
.pkl文件中,无需外部服务 - 适合单机部署、轻量级持久化需求
实现原理
关键设计决策
| 决策 | 选择 | 原因 |
|---|---|---|
| 序列化方式 | pickle 二进制 | JSON/base64 无法序列化 bytes 等复杂对象 |
| 文件粒度 | 每个 thread_id 一个文件 | 隔离性好,互不影响 |
| 内存逻辑 | 复用 InMemorySaver | 避免重复实现复杂的内存操作 |
代码:完整 FileSaver 实现
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
使用方式
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 等),需要实现以下接口:
| 方法 | 作用 | 必须实现 |
|---|---|---|
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,只额外处理序列化/反序列化:
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 基础设施 | 单机轻量部署 |
十、依赖安装速查
# 内存记忆(随 langgraph 安装)
pip install langgraph
# Redis 持久化
pip install langgraph-checkpoint-redis
# 注意:需要 Redis Stack(含 RediSearch),普通 Redis 不行
# MySQL 持久化
pip install langgraph-checkpoint-mysql
# 注意:类名是 PyMySQLSaver,不是 MySQLSaver
# 文件持久化
# 无额外依赖,自定义实现

2812

被折叠的 条评论
为什么被折叠?



