LangChain LangGraph
Module 1 Module 2 Module 3 Module 4 Module 5 Module 6
Module 1 · 基础入门
1 1-1 Simple Graph
2 1-2 Chain
3 1-3 Router
4 1-4 Agent
5 1-5 Agent Memory
6 1-6 Deployment
SUM Module Summary
HomeLangGraphModule 1 · 基础入门1-5 Agent Memory
📓 Notebook 📖 Explained
LANGGRAPH MODULE 1 · LESSON 5

Agent with Memory:给 Agent 装上持久化记忆
Checkpointer · MemorySaver · Thread ID

上一讲的 Agent 每次对话都是"失忆"的——invoke() 执行完,State 就消失了。
这一讲用 Checkpointer 让 Agent 记住对话历史,真正实现多轮对话。

1 问题:无状态 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,完全错误。

为什么会这样?State 的生命周期

LangGraph 中的 State 是每次 invoke() 临时创建的对象。执行流程如下:

State 的生命周期(无 Checkpointer)

📥
invoke() 第一次调用
创建新的 State = {messages: [HumanMessage("Add 3 and 4.")]},图执行完毕后……
🗑️
State 被丢弃
所有对话历史(add(3,4)=7 的记录)从内存中消失,没有任何持久化
📥
invoke() 第二次调用
又创建全新的 State = {messages: [HumanMessage("Multiply that by 2.")]},LLM 完全不知道 "that" 指什么

本质上,这是因为 LangGraph 的图执行是纯函数式的——输入进来,输出出去,不保留任何副作用。这在大多数情况下是好事(可预测、可测试),但多轮对话场景需要状态持久化。

2 解决方案:Checkpointer 检查点机制

Checkpointer(检查点器)是 LangGraph 的持久化层。它的职责是:在图的每个节点执行完毕后,自动把当前 State 序列化并保存到存储后端

核心思路

Checkpointer 让图在每一步都"留下足迹"。下次 invoke() 时,不再从空白开始,而是先把上次的足迹全部加载回来,然后在此基础上继续执行。

Checkpointer 做了什么(三件事)

1
保存(Save)
每个节点执行完毕后,将当前完整 State 序列化(如 JSON),存入 key-value 存储。key 是 thread_id + step_number
2
加载(Load)
新的 invoke() 调用时,先根据 thread_id 找到该对话最新的 checkpoint,反序列化为 State 对象,加载到图中。
3
合并(Merge)
把新输入(新的 HumanMessage)追加到已加载的历史 State 中,然后开始本轮执行。

MemorySaver:最简单的 Checkpointer 实现

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 使用内存中的字典存储所有检查点,零配置、开箱即用,适合开发阶段和单元测试。缺点是进程重启后数据全部丢失。

3 Thread ID:区分不同的对话会话

一个 Agent 可能同时服务多个用户,每个用户的对话历史必须隔离。Thread ID 就是"对话会话 ID",相当于数据库里的主键。

不同 Thread ID 对应不同的独立对话

thread_id = "1"
(用户 Alice)
Add 3 and 4. → 7
Multiply that by 2. → 14 ✓
Now add 10. → 24 ✓
thread_id = "2"
(用户 Bob)
What is 100 / 4? → 25
Add 5 to that. → 30 ✓
thread_id = "42"
(用户 Carol)
Multiply 6 and 7. → 42
Divide that by 3. → 14 ✓

三个线程的对话历史完全独立存储、互不干扰。Alice 的 "that" 是 7,Bob 的 "that" 是 25,Carol 的 "that" 是 42。

Thread ID 是你自己决定的

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}"}}

4 核心代码:只差一个参数

给 Agent 加上记忆,代码改动极其微小——只需要在 compile() 时传入 checkpointer,其余所有代码保持不变。

无记忆版(1-4 Agent)

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,无法区分对话
)

有记忆版(1-5 本讲)

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."

5 两次 invoke 如何"连接"——完整生命周期

要真正理解记忆机制,需要知道每次 invoke() 在内部经历了什么步骤。

第一次 invoke("Add 3 and 4.", config={"thread_id": "1"})
1
查找历史:LangGraph 去 MemorySaver 里找 thread_id="1" 的 checkpoint。找不到(第一次),从空 State 开始。
2
初始化 State:State = {messages: [HumanMessage("Add 3 and 4.")]}
3
执行 agent 节点:LLM 处理消息,返回 AIMessage(tool_calls=[add(3,4)])。Checkpoint 保存步骤 1
4
执行 tools 节点:ToolNode 执行 add(3,4)=7,追加 ToolMessage("7")。Checkpoint 保存步骤 2
5
再次执行 agent 节点:LLM 看到工具结果,返回 AIMessage("The sum of 3 and 4 is 7.")。Checkpoint 保存步骤 3(最终 State)
6
tools_condition 判断:最新消息无 tool_calls,返回 END。执行结束,返回结果。
↓   用户看到答案 7,然后发送第二个问题
第二次 invoke("Multiply that by 2.", config={"thread_id": "1"})
1
查找历史:LangGraph 去 MemorySaver 里找 thread_id="1" 的最新 checkpoint。找到了!加载已有的完整 State。
2
合并新输入:把新的 HumanMessage("Multiply that by 2.") 追加到历史消息列表末尾。现在 messages 有 5 条。
3
执行 agent 节点:LLM 看到完整 5 条消息历史,明白 "that" = 7,返回 AIMessage(tool_calls=[multiply(7,2)])。Checkpoint 保存。
4
执行 tools 节点:multiply(7, 2) = 14,追加 ToolMessage("14")。Checkpoint 保存。
5
再次执行 agent 节点:LLM 返回 AIMessage("The result is 14.")。Checkpoint 保存最终 State(现在有 7 条消息)。
6
结束:正确答案 14 返回给用户。
关键理解

第二次 invoke() 你只传入了 [HumanMessage("Multiply that by 2.")],但 LangGraph 内部悄悄地把它追加到历史的 5 条消息后面,LLM 实际看到的是 6 条消息(5 条历史 + 1 条新问题)。这就是记忆的本质——消息列表的持续累积。

6 对比演示:有记忆 vs 无记忆

下面直观展示第二轮 invoke() 时,LLM 实际收到的 messages 列表有何不同。

无记忆版
LLM 在第二轮看到的 messages

仅有 1 条消息,完全没有上下文
HumanMessage Multiply that by 2.
LLM 的困境
"that" 是什么?完全不知道。可能猜测 multiply(2, 2) = 4,或者要求用户澄清。

有记忆版
LLM 在第二轮看到的 messages

共 6 条消息,完整对话历史
HumanMessage Add 3 and 4.
AIMessage (tool_call) add(a=3, b=4)
ToolMessage 7
AIMessage The sum of 3 and 4 is 7.
第二轮新增
HumanMessage Multiply that by 2.
LLM 完全理解
"that" 显然就是上一条 AIMessage 说的 7。调用 multiply(7, 2) = 14。

第二轮执行后的完整消息列表(有记忆版)

两轮对话执行完毕后,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."),
]

7 Checkpointer 工作流程图

下图展示了带 Checkpointer 的图在单次 invoke() 内部如何在每个节点后自动保存 State。

每个节点执行后自动触发 Checkpoint 保存

START
agent 节点
LLM 调用
tools 节点
执行工具
agent 节点
LLM 整理答案
END
保存 Checkpoint #1
{msgs:[Human, AIToolCall]}
保存 Checkpoint #2
{msgs:[..., ToolMsg]}
保存 Checkpoint #3
{msgs:[..., AIFinal]}
MemorySaver
存储后端
thread_id: "1"
step_0: State{1条}
step_1: State{2条}
step_2: State{3条}
step_3: State{4条} ← 最新
下次 invoke() 时,LangGraph 直接加载 step_3(最新 checkpoint),把新消息追加后继续执行
时间旅行(Time Travel)

因为每个步骤都有 checkpoint,LangGraph 支持"时间旅行"功能:你可以加载任意历史 checkpoint,从那个时间点重新执行(会覆盖之后的历史)。这是调试和纠错的强大工具,Module 3 会专门讲解。

8 MemorySaver vs 生产级存储

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

如果你使用 LangGraph Platform(云托管服务,见 Module 6),平台会自动管理 Checkpointer,你甚至不需要手动配置——直接使用 thread_id 即可,底层持久化对你完全透明。

9 Config 参数的必要性

即使你传了 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 的其他用途

config 字典里的 configurable 不仅可以放 thread_id,还能放其他运行时参数(如 LLM temperature、用户权限、自定义参数等)。这是 LangGraph 实现"运行时配置"的标准机制,避免了把所有参数都塞进 State 的混乱。

查询历史 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"])  # 该时刻的消息列表

10 总结:这一讲学到了什么

这一讲在 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,业务代码零改动
下一讲预告:Deployment

1-6 将介绍如何把 LangGraph Agent 部署为可以被外部访问的服务(LangGraph Server)。你会看到 Checkpointer 在云端如何工作,以及如何通过 REST API 和 SDK 与 Agent 交互。

Module 1 学习路径
1-1 Simple Graph 1-2 Chain 1-3 Router 1-4 Agent 1-5 Memory ← 你在这里 1-6 Deployment