上一讲的 Agent 每次对话都是"失忆"的——invoke() 执行完,State 就消失了。
这一讲用 Checkpointer 让 Agent 记住对话历史,真正实现多轮对话。
上一讲(1-4)我们构建了一个完整的 ReAct Agent:它能循环调用工具,直到问题解决。但它有一个致命缺陷——每次 invoke() 都是全新的对话,不记得之前说过任何内容。
先问:"Add 3 and 4."——Agent 调用 add(3, 4) 返回 7。
紧接着再问:"Multiply that by 2."——"that" 是什么?
没有记忆的 Agent 根本不知道"that"是 7!它可能随机猜测,比如 multiply(2, 2) = 4,完全错误。
LangGraph 中的 State 是每次 invoke() 临时创建的对象。执行流程如下:
本质上,这是因为 LangGraph 的图执行是纯函数式的——输入进来,输出出去,不保留任何副作用。这在大多数情况下是好事(可预测、可测试),但多轮对话场景需要状态持久化。
Checkpointer(检查点器)是 LangGraph 的持久化层。它的职责是:在图的每个节点执行完毕后,自动把当前 State 序列化并保存到存储后端。
Checkpointer 让图在每一步都"留下足迹"。下次 invoke() 时,不再从空白开始,而是先把上次的足迹全部加载回来,然后在此基础上继续执行。
thread_id + step_number。from langgraph.checkpoint.memory import MemorySaver
# MemorySaver 把所有 checkpoint 存在 Python 进程内存里
# 本质上是一个字典:{thread_id: {step: state_snapshot}}
memory = MemorySaver()
# 编译时把 checkpointer 传进去
react_graph_memory = builder.compile(checkpointer=memory)
MemorySaver 使用内存中的字典存储所有检查点,零配置、开箱即用,适合开发阶段和单元测试。缺点是进程重启后数据全部丢失。
一个 Agent 可能同时服务多个用户,每个用户的对话历史必须隔离。Thread ID 就是"对话会话 ID",相当于数据库里的主键。
三个线程的对话历史完全独立存储、互不干扰。Alice 的 "that" 是 7,Bob 的 "that" 是 25,Carol 的 "that" 是 42。
LangGraph 不会自动生成 thread_id。你需要在 config 里传入,通常由你的应用层生成(比如 UUID、用户 ID、Session ID)。同一个用户的多次请求用同一个 thread_id,不同用户用不同的 thread_id。
import uuid
# 方式一:固定字符串(测试/demo 时常用)
config = {"configurable": {"thread_id": "1"}}
# 方式二:UUID(生产环境,每个用户 session 一个)
thread_id = str(uuid.uuid4())
config = {"configurable": {"thread_id": thread_id}}
# 方式三:与业务 ID 绑定(最常见的实践)
config = {"configurable": {"thread_id": f"user_{user_id}_session_{session_id}"}}
给 Agent 加上记忆,代码改动极其微小——只需要在 compile() 时传入 checkpointer,其余所有代码保持不变。
from langgraph.graph import (
StateGraph, MessagesState,
START, END
)
from langgraph.prebuilt import (
ToolNode, tools_condition
)
# 无 checkpointer
react_graph = builder.compile()
# 每次 invoke 都是全新对话
messages = react_graph.invoke(
{"messages": messages}
# 没有 config,无法区分对话
)
from langgraph.graph import (
StateGraph, MessagesState,
START, END
)
from langgraph.prebuilt import (
ToolNode, tools_condition
)
from langgraph.checkpoint.memory import MemorySaver
memory = MemorySaver()
# 传入 checkpointer ← 唯一改动!
react_graph_memory = builder.compile(
checkpointer=memory
)
config = {"configurable": {"thread_id": "1"}}
# 必须传 config,带上 thread_id
messages = react_graph_memory.invoke(
{"messages": messages},
config # ← 告诉图用哪个 thread
)
from langchain_core.messages import HumanMessage
from langgraph.checkpoint.memory import MemorySaver
# ─── 初始化 Checkpointer ───
memory = MemorySaver()
react_graph_memory = builder.compile(checkpointer=memory)
# ─── 配置:使用 thread_id = "1" ───
config = {"configurable": {"thread_id": "1"}}
# ─── 第一轮对话 ───
messages = [HumanMessage(content="Add 3 and 4.")]
result = react_graph_memory.invoke({"messages": messages}, config)
# LLM 调用 add(3, 4) → ToolMessage("7") → AIMessage("The sum is 7.")
# ✅ Checkpointer 把这整个 State 保存到 thread "1" 的 checkpoint
# ─── 第二轮对话:注意只传了新的问题 ───
messages = [HumanMessage(content="Multiply that by 2.")]
result = react_graph_memory.invoke({"messages": messages}, config)
# ✅ LangGraph 先加载 thread "1" 的历史,再追加新消息
# LLM 看到完整历史,正确理解 "that" = 7
# → 调用 multiply(7, 2) → ToolMessage("14") → AIMessage("The result is 14.")
print(result["messages"][-1].content)
# 输出:"The result is 14."
要真正理解记忆机制,需要知道每次 invoke() 在内部经历了什么步骤。
第二次 invoke() 你只传入了 [HumanMessage("Multiply that by 2.")],但 LangGraph 内部悄悄地把它追加到历史的 5 条消息后面,LLM 实际看到的是 6 条消息(5 条历史 + 1 条新问题)。这就是记忆的本质——消息列表的持续累积。
下面直观展示第二轮 invoke() 时,LLM 实际收到的 messages 列表有何不同。
两轮对话执行完毕后,thread "1" 的 State 中 messages 共有 7 条:
# thread_id="1" 的最终 messages 列表:
[
HumanMessage(content="Add 3 and 4."),
AIMessage(content="", tool_calls=[{"name": "add", "args": {"a": 3, "b": 4}}]),
ToolMessage(content="7", name="add"),
AIMessage(content="The sum of 3 and 4 is 7."),
HumanMessage(content="Multiply that by 2."),
AIMessage(content="", tool_calls=[{"name": "multiply", "args": {"a": 7, "b": 2}}]),
ToolMessage(content="14", name="multiply"),
AIMessage(content="The result of multiplying 7 by 2 is 14."),
]
下图展示了带 Checkpointer 的图在单次 invoke() 内部如何在每个节点后自动保存 State。
因为每个步骤都有 checkpoint,LangGraph 支持"时间旅行"功能:你可以加载任意历史 checkpoint,从那个时间点重新执行(会覆盖之后的历史)。这是调试和纠错的强大工具,Module 3 会专门讲解。
MemorySaver 只适合开发阶段。生产环境需要能持久化到磁盘或外部数据库的 Checkpointer 实现。
| Checkpointer | 存储后端 | 适用场景 | 持久化 |
|---|---|---|---|
MemorySaver |
Python 内存(字典) | 本地开发、单元测试、Demo | ❌ 进程重启丢失 |
SqliteSaver |
SQLite 文件 | 单机部署、轻量级生产 | ✅ 持久到文件 |
PostgresSaver |
PostgreSQL | 多实例部署、高并发生产 | ✅ 持久到数据库 |
RedisSaver |
Redis | 高性能、缓存优先场景 | ✅ 可配置持久化 |
| 自定义实现 | 任意存储 | 特殊需求(如 MongoDB、S3) | 取决于实现 |
所有 Checkpointer 实现都遵循相同的接口(BaseCheckpointSaver),因此从 MemorySaver 切换到 PostgresSaver 时,业务代码零改动,只需替换编译时传入的 checkpointer 对象。
# 开发阶段
from langgraph.checkpoint.memory import MemorySaver
graph = builder.compile(checkpointer=MemorySaver())
# 生产阶段(替换一行,其余代码不变)
from langgraph.checkpoint.postgres import PostgresSaver
with PostgresSaver.from_conn_string("postgresql://user:pass@host/db") as checkpointer:
graph = builder.compile(checkpointer=checkpointer)
# 同样的 invoke(),同样的 config,完全一致的行为
如果你使用 LangGraph Platform(云托管服务,见 Module 6),平台会自动管理 Checkpointer,你甚至不需要手动配置——直接使用 thread_id 即可,底层持久化对你完全透明。
即使你传了 checkpointer,如果 invoke() 时没有提供 config(或没有 thread_id),LangGraph 会报错。这是设计上的强制约束。
# ❌ 错误:有 checkpointer 但没有 config
react_graph_memory.invoke({"messages": messages})
# 抛出 ValueError: "thread_id" is required in config["configurable"]
# when a checkpointer is used
# ✅ 正确:提供 config 和 thread_id
config = {"configurable": {"thread_id": "1"}}
react_graph_memory.invoke({"messages": messages}, config)
为什么是强制的?因为没有 thread_id,Checkpointer 根本不知道:
config 字典里的 configurable 不仅可以放 thread_id,还能放其他运行时参数(如 LLM temperature、用户权限、自定义参数等)。这是 LangGraph 实现"运行时配置"的标准机制,避免了把所有参数都塞进 State 的混乱。
有了 checkpointer,你还可以随时查询某个 thread 的历史 State,无需重新执行图:
# 获取某个 thread 的当前(最新)State 快照
snapshot = react_graph_memory.get_state(config)
print(snapshot.values["messages"]) # 查看完整消息历史
print(snapshot.next) # 查看下一步将执行哪个节点
# 获取某个 thread 的全部历史 checkpoints(时间旅行用)
for checkpoint in react_graph_memory.get_state_history(config):
print(checkpoint.config) # 该 checkpoint 的 config(含 checkpoint_id)
print(checkpoint.values["messages"]) # 该时刻的消息列表
这一讲在 1-4 的完整 ReAct Agent 基础上,用最小的代码改动实现了持久化多轮对话记忆。
| 概念 | 一句话总结 |
|---|---|
| 无状态的根源 | LangGraph 的 State 是每次 invoke() 临时创建、执行完就丢弃的对象 |
| Checkpointer | 在每个节点执行后自动保存 State 快照,下次 invoke() 时自动加载恢复 |
| MemorySaver | 内存版 Checkpointer,进程重启数据丢失,适合开发测试 |
| Thread ID | 对话会话的唯一标识,相同 thread_id 的多次 invoke() 共享并累积历史 |
| Config 参数 | 每次 invoke() 必须传入 {"configurable": {"thread_id": "xxx"}} |
| 连接机制 | 第二次 invoke() 先加载历史 State,再追加新消息,LLM 看到完整上下文 |
| 生产切换 | 换掉 MemorySaver 为 PostgresSaver/SqliteSaver,业务代码零改动 |
1-6 将介绍如何把 LangGraph Agent 部署为可以被外部访问的服务(LangGraph Server)。你会看到 Checkpointer 在云端如何工作,以及如何通过 REST API 和 SDK 与 Agent 交互。