- 发布于
LangGraph Memory 学习笔记
- Authors

- Name
- Charly
学习 LangGraph Memory 时,最容易混淆的是 State、Checkpointer、Store 和消息摘要。它们都与“记忆”有关,但解决的是不同问题。
可以先记住这张表:
| 概念 | 作用 | 生命周期 |
|---|---|---|
AgentState | Agent 当前运行所需的数据 | 单次运行中始终存在 |
Checkpointer | 按 thread_id 保存和恢复 State | 跨多次 invoke() |
Store | 保存跨线程共享的长期资料 | 跨对话、跨线程 |
| Runtime Context | 向本次调用提供不可变上下文 | 当前调用 |
SummarizationMiddleware | 压缩消息历史,控制上下文长度 | 更新当前线程的 messages |
一句话概括:
State = Agent 当前正在使用的数据
Checkpointer = 保存和恢复某个对话线程的 State
Store = 跨对话保存用户的长期资料
Summarization = 压缩 messages,避免上下文不断增长
1. 没有 Checkpointer,也存在 State
State 是 Agent 执行时的数据容器。即使没有配置 Checkpointer,LangGraph 仍然需要在本次调用期间创建和维护 State。
result = agent.invoke({
"messages": [
{"role": "user", "content": "Look up user information"}
],
"user_id": "user_123",
})
本次调用内部可以理解为存在下面的状态:
{
"messages": [...],
"user_id": "user_123",
}
工具因此可以通过 runtime.state 读取它:
@tool
def get_user_info(runtime: ToolRuntime) -> str:
"""Look up information about the current user."""
user_id = runtime.state["user_id"]
return f"Current user ID: {user_id}"
但这只证明 State 在当前运行中存在,并不表示它已经持久化。
State 负责运行
Checkpointer 负责保存
create_agent() 没有传入 Checkpointer 时,可以理解为:
agent = create_agent(
model=model,
tools=tools,
checkpointer=None,
)
此时:
- 单次
invoke()中仍然可以读写 State; - 不需要提供
thread_id; - 下一次独立调用不会自动继承上次 State;
- Python 进程退出后不能恢复。
2. AgentState 是状态结构,不是数据库
AgentState 描述 Agent State 中有哪些字段。默认 Agent State 至少包含 messages,也可以添加业务字段:
from langchain.agents import AgentState
class CustomState(AgentState):
user_id: str
user_name: str
request_count: int
创建 Agent 时注册该结构:
agent = create_agent(
model=model,
tools=tools,
state_schema=CustomState,
)
需要区分:
state_schema = 定义状态的结构
checkpointer = 保存状态字段的值
CustomState 这个 Python 类型仍然位于应用代码中。数据库保存的是 messages、user_id、request_count 等字段在某个 checkpoint 中的值。进程重启时,应用需要重新创建相同结构的 Agent,然后再从 Checkpointer 恢复数据。
3. ToolRuntime 让工具读取当前 State
工具声明 runtime: ToolRuntime 后,LangChain 会在执行工具时自动注入 Runtime:
from langchain.tools import tool, ToolRuntime
@tool
def get_user_info(runtime: ToolRuntime) -> str:
"""Get information about the current user."""
user_id = runtime.state["user_id"]
return "User is John Smith" if user_id == "user_123" else "Unknown user"
模型看到的工具参数中不会出现 runtime,也不需要由模型生成 user_id。常用 Runtime 信息包括:
runtime.state # 当前线程的 State
runtime.context # 本次调用的不可变上下文
runtime.store # 长期记忆 Store
runtime.tool_call_id # 当前工具调用 ID
runtime.config # RunnableConfig
runtime.stream_writer # 自定义流式事件
4. 工具更新 State 为什么要返回 Command
普通工具返回值默认是“工具结果”,会被包装成 ToolMessage 交给模型,并不会自动成为 State 更新。
@tool
def set_name(name: str) -> dict:
return {"user_name": name}
这里的字典是给模型读取的工具输出。要明确更新 Agent State,应返回 LangGraph 的 Command:
from langgraph.types import Command
@tool
def set_name(name: str) -> Command:
return Command(
update={"user_name": name}
)
如果后续模型还需要看到工具执行结果,可以同时写入 messages:
from langchain.messages import ToolMessage
from langchain.tools import ToolRuntime
@tool
def set_name(name: str, runtime: ToolRuntime) -> Command:
"""Set the current user's name."""
return Command(
update={
"user_name": name,
"messages": [
ToolMessage(
content=f"Name set to {name}",
tool_call_id=runtime.tool_call_id,
)
],
}
)
这样,user_name 会成为正式的 State channel write,经过 reducer 合并,并在配置 Checkpointer 时进入 checkpoint。
不同位置更新 State 的方式不同:
| 更新位置 | 推荐方式 |
|---|---|
| 普通 LangGraph 节点 | return {"user_name": "Bob"} |
| Node 风格 Middleware | return {"user_name": "Bob"} |
| 工具函数内部 | return Command(update={...}) |
| 图外部手动修改 | agent.update_state(config, {...}) |
| State 更新并动态跳转 | Command(update={...}, goto="node") |
| 从 interrupt 恢复 | Command(resume=...) |
不要依赖直接修改:
runtime.state["user_name"] = name
这类修改未必会被识别为正式的 channel write,也无法可靠经过 reducer 和 checkpoint 机制。
5. State 字段、Channel 和 Reducer
LangGraph 可以把 State 的每个字段理解成一个 channel:
messages
user_id
user_name
request_count
节点返回 State 更新后,Reducer 决定新值如何与旧值合并。普通字段通常采用新值覆盖旧值;messages 则使用消息 reducer,把新消息加入消息历史。
自定义累加字段可以声明 Reducer:
import operator
from typing import Annotated
class CustomState(AgentState):
request_count: Annotated[int, operator.add]
节点每次返回:
return {"request_count": 1}
最终结果会累加。Reducer 在并行节点或多个工具同时更新同一字段时尤其重要,否则 LangGraph 无法确定如何解决写入冲突。
6. Checkpointer 是线程级短期记忆
Checkpointer 按 thread_id 保存 checkpoint:
config = {
"configurable": {
"thread_id": "thread-001"
}
}
相同 thread_id 表示继续同一段对话:
agent.invoke(
{
"messages": [
{"role": "user", "content": "My name is Bob"}
]
},
config,
)
agent.invoke(
{
"messages": [
{"role": "user", "content": "What is my name?"}
]
},
config,
)
换成另一个 thread_id 就会成为一个新的对话线程。
常见实现的区别:
| Checkpointer | 跨 invoke() | 跨进程重启 | 适用场景 |
|---|---|---|---|
None | 否 | 否 | 无状态调用 |
InMemorySaver | 是 | 否 | 学习、测试 |
SqliteSaver | 是 | 是 | 本地开发 |
PostgresSaver | 是 | 是 | 生产环境 |
7. PostgresSaver 的使用与恢复
from langgraph.checkpoint.postgres import PostgresSaver
DB_URI = "postgresql://postgres:password@localhost:5432/langgraph"
config = {
"configurable": {
"thread_id": "thread-001"
}
}
with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
checkpointer.setup()
agent = create_agent(
model=model,
tools=tools,
state_schema=CustomState,
checkpointer=checkpointer,
)
result = agent.invoke(
{
"messages": [
{"role": "user", "content": "Hello"}
],
"user_id": "user_123",
},
config,
)
setup() 用于创建或升级数据库表,通常在首次部署或数据库迁移阶段执行,不需要在每个业务请求中重复执行。
进程退出后,只要满足下面几个条件,就可以恢复:
- PostgreSQL 数据仍然存在;
- 连接到同一个数据库;
- 使用相同
thread_id; - 应用重新创建兼容的 Agent 和 State schema;
- 自定义 State 值能够被序列化和反序列化。
Checkpointer 恢复的是 LangGraph State 和执行信息,不会恢复 Python 局部变量、模型实例、数据库连接或未写入 State 的对象。
8. PostgreSQL 中保存了什么
PostgresSaver 主要使用下面几张表:
checkpoint_migrations
checkpoints
checkpoint_blobs
checkpoint_writes
checkpoints
保存 checkpoint 的整体结构,包括:
thread_id;checkpoint_id;- 父 checkpoint;
- channel 版本;
- 图执行结构;
- metadata。
checkpoint_blobs
保存各 State channel 的序列化值。messages 包含 HumanMessage、AIMessage、ToolMessage 等复杂对象,通常会以二进制数据保存。
checkpoint_writes
保存节点已经产生、但尚未合并进下一个完整 checkpoint 的写入,包括:
- 普通 State 字段更新;
- 模型生成的
AIMessage; - 工具生成的
ToolMessage; - 节点错误;
- interrupt 和 resume 数据;
- 内部任务调度信息。
它对故障恢复非常重要。假设同一个 superstep 中有两个并行节点,一个成功、一个失败,LangGraph 可以保留成功节点的 pending writes,恢复时只重试失败节点。
9. Checkpointer 不等于长期记忆 Store
Checkpointer 保存的是某个对话线程的状态,Store 保存的是多个对话可以共享的长期资料。
user_123
├── thread_001
├── thread_002
└── thread_003
三个线程拥有各自的 checkpoint,但可以从 Store 读取同一位用户的长期偏好。
适合放进 Checkpointer 的数据:
- 当前对话消息;
- 当前任务进度;
- 临时选择;
- 工作流状态;
- interrupt 状态。
适合放进 Store 的数据:
- 用户长期偏好;
- 用户档案;
- 跨会话共享事实;
- 可检索的语义记忆;
- 多个 Agent 或多个线程共享的数据。
10. State、Context 和 Store 如何选择
稳定且不应该被 Agent 修改的身份信息,通常更适合放在 Runtime Context 中。例如登录用户 ID:
from dataclasses import dataclass
@dataclass
class AppContext:
user_id: str
工具通过下面的方式读取:
@tool
def get_user_info(runtime: ToolRuntime[AppContext]) -> str:
user_id = runtime.context.user_id
return f"User ID: {user_id}"
可以按照下面的规则选择:
| 数据 | 推荐位置 |
|---|---|
| 登录用户 ID、权限信息 | Context |
| 当前对话消息 | State |
| 当前任务进度 | State |
| Agent 可以修改的用户称呼 | State |
| 跨会话用户偏好 | Store |
| 数据库连接、服务客户端 | Runtime 或依赖注入 |
11. Summarization 是上下文压缩
SummarizationMiddleware 负责控制消息上下文长度:
SummarizationMiddleware(
model=summarization_model,
trigger=("tokens", 4000),
keep=("messages", 20),
)
它的逻辑可以理解为:
消息达到触发阈值
↓
将较早消息生成摘要
↓
保留最近 20 条消息
↓
使用摘要替换更早的消息
需要注意:
- 中间件通常在下一次模型调用前检查阈值;
- 消息数量不超过
keep时,没有旧消息可供摘要; - 摘要会成为当前
messagesState 的一部分; - 摘要属于当前 thread 的短期记忆;
- 它不能代替跨线程的 Store。
调试当前 LangChain 实现时,可以检查摘要消息是否包含内部来源标记:
message.additional_kwargs.get("lc_source") == "summarization"
这个字段适合调试,但属于实现细节,不应作为核心业务协议。
12. 裁剪消息不等于删除数据库历史
从当前 State 删除旧消息,只表示后续模型不再接收这些消息。PostgresSaver 仍可能在历史 checkpoint 中保留原始数据。
因此,真正的数据删除还需要考虑:
- 删除整个 thread;
- 清理过期 checkpoint;
- 设置数据保留策略;
- 根据隐私要求删除关联的 blobs 和 writes。
上下文管理和数据库数据保留是两个不同问题。
13. 查看和调试 State
查看最新状态:
snapshot = agent.get_state(config)
print(snapshot.values)
print(snapshot.next)
print(snapshot.tasks)
print(snapshot.metadata)
查看所有字段:
for key, value in snapshot.values.items():
print(key, value)
查看 State 历史:
for snapshot in agent.get_state_history(config):
print(snapshot.config)
print(snapshot.values)
print(snapshot.next)
查看反序列化后的 pending writes:
checkpoint_tuple = checkpointer.get_tuple(config)
if checkpoint_tuple:
for task_id, channel, value in checkpoint_tuple.pending_writes:
print(task_id, channel, value)
不要只查询 PostgreSQL 的一张表来手动重建 State。完整状态可能分布在 checkpoints、checkpoint_blobs 和 checkpoint_writes 中,让 LangGraph 负责反序列化通常更可靠。
14. 最需要记住的十句话
AgentState是运行状态,Checkpointer 是状态持久化。- 没有 Checkpointer,单次
invoke()仍然有 State。 create_agent()不传 Checkpointer 时,默认没有跨调用记忆。- 相同
thread_id才会恢复相同对话。 InMemorySaver不能跨进程,PostgresSaver可以。- 工具通过
runtime.state读取当前 State。 - 工具要更新 State,应返回
Command(update=...)。 - 普通图节点返回字典即可更新 State。
- Checkpointer 是线程内记忆,Store 是跨线程长期记忆。
- Summarization 只是压缩消息上下文,不等于长期记忆。