- 培歌行学LangGraph(1):从 LangChain 到 LangGraph 的思维跃迁]
- 培歌行学LangGraph(2):Reducer(归约器)彻底搞懂
- 培歌行学LangGraph(3):编译、执行与可视化
- 培歌行学LangGraph(4):一文搞懂图的状态(State)管理
- 培歌行学LangGraph(5):状态管理与
graph.invoke入参深度解析 - 培歌行学LangGraph(6):Multi Schema多状态管理详解
- 培歌行学LangGraph(7):预定义状态MessagesState与AgentState
- 培歌行学LangGraph(8):控制流详解
- 培歌行学LangGraph(9):控制流详解:defer延迟节点——让收尾工作自动排到最后
- 培歌行学LangGraph(10):多分支汇聚Fan-in——多个分支如何汇聚到一起?
- 培歌行学LangGraph(11):用循环结构实现 ReAct Agent
- 培歌行学LangGraph(12):递归限制——别让你的图无限跑下去
- 培歌行学LangGraph(13):节点执行与容错机制
- 培歌行学LangGraph(14):启用可恢复执行(Checkpoint)
- 培歌行学LangGraph(15):持久化模式——何时保存检查点?
- 培歌行学LangGraph(16):持久化模式——查看历史检查点——你的图走过的每一步都有记录
一、多轮对话(基础用法)
这个最简单,前面已经讲过了。就是同一个 thread_id下多次调用,模型能记住之前说过的话。
# 第一轮
graph.invoke({"messages": [HumanMessage("你好,我是老王")]}, config=config)
# 第二轮:模型还记得你是老王
graph.invoke({"messages": [HumanMessage("我是谁?")]}, config=config)
二、失败后恢复运行
2.1 这是什么场景?
你的图跑到一半,某个节点挂了(比如网络波动、API 超时)。修复问题后,你希望从断点继续跑,而不是从头开始。
2.2 怎么做?
三步走:
- 图挂了:某个节点抛出异常,图停止
- 修BUG:修复出问题的节点代码
- 恢复运行:用
None作为输入,传入同一个thread_id
# 恢复运行:输入传 None,config 只传 thread_id
res = graph.invoke(None, config=config)
2.3 举个栗子
假设你的图是这样的:
START → change_topic → 写诗 → 汇总 → END
↘ 讲笑话 ↗
第一次运行时,讲笑话节点抛异常了。修复后,恢复运行:
# 恢复运行
res = graph.invoke(None, config=config)
你会发现:
写诗节点没有重新执行(结果被复用了)讲笑话节点重新执行了(因为之前失败了)汇总节点重新执行了(因为需要新的笑话结果)
2.4 关键点
| 条件 | 说明 |
|---|---|
输入传 None | 告诉 LangGraph "我要恢复,不是新任务" |
只传 thread_id | 不传 checkpoint_id,自动从最新检查点恢复 |
| 检查点后端要持久化 | 如果用 InMemorySaver,进程重启就丢了 |
三、Time Travel(检查点回溯)
3.1 这是什么?
Time Travel 就是回到过去的某个检查点,重新执行后面的流程。
有两种玩法:
| 玩法 | 英文 | 说明 |
|---|---|---|
| 重放 | Replay | 回到过去,原封不动重新跑一遍 |
| 分叉 | Fork | 回到过去,改点东西,跑一条新路线 |
3.2 Replay:重放
3.2.1 怎么做?
# 1. 拿到所有历史检查点
history = list(graph.get_state_history(config=config))
# 2. 选一个你想重放的检查点(比如写诗之前的那个)
target = None
for h in history:
if h.next == ('node_poem', 'node_joke'):
target = h
break
# 3. 重放:输入传 None,config 传这个检查点的 config
res = graph.invoke(None, config=target.config)
3.2.2 关键点
- 输入传
None:和失败恢复一样 - config 里带
checkpoint_id:告诉 LangGraph "从哪个存档点开始" - 后面的节点会重新执行:LLM 调用、API 请求都会重新触发
- 前面的节点不会重新执行:比如
change_topic不会再跑一遍
3.3 Fork:分叉
3.3.1 这是什么?
回到某个历史检查点,修改状态,然后跑一条新的执行分支。
比如:
- 用户说"写首诗",模型写了诗
- 你想试试:"如果当时用户说的是'讲个笑话',结果会怎样?"
- 回到那个检查点,改一下输入,重新跑
3.3.2 怎么做?
# 1. 找到你想分叉的检查点(比如 router_node 执行之前的那个)
before_router = next(h for h in history if h.next == ('router_node',))
# 2. 修改状态,创建分叉
fork_config = graph.update_state(
config=before_router.config,
values={"user_input": "帮我写一个关于布偶猫的笑话"},
as_node=START # 把这个修改当作 START 的输出
)
# 3. 从分叉点继续运行
res = graph.invoke(None, fork_config)
3.3.3 as_node是什么意思?
as_node告诉 LangGraph:"我这个修改,假装是某个节点的输出"。
as_node的值 | 效果 |
|---|---|
START | 重新执行所有节点 |
"router_node" | 跳过 router_node,直接从它的后继节点开始 |
"node_poem" | 跳过前面的所有节点,直接从 poem 开始 |
3.3.4 两种分叉方式对比
| 方式 | 代码 | 效果 |
|---|---|---|
| 只改状态 | as_node=START | 重新执行 router_node |
| 改状态+跳过节点 | as_node="router_node" | 跳过 router_node,伪造它的输出 |
方式一:只改状态
fork_config = graph.update_state(
config=before_router.config,
values={"user_input": "帮我写一个关于布偶猫的笑话"},
as_node=START
)
效果:user_input变了,router_node重新执行,根据新的输入重新路由。
方式二:改状态+跳过节点
fork_config = graph.update_state(
config=before_router.config,
values={"topic": "狸花猫", "mode": "joke"},
as_node="router_node"
)
效果:router_node不执行了,直接伪造它的输出。路由函数会根据新的 mode决定走向。
四、四种场景总结
| 场景 | 输入 | config | 效果 |
|---|---|---|---|
| 多轮对话 | 新消息 | 同 thread_id | 模型记得历史 |
| 失败恢复 | None | 同 thread_id | 从断点继续,成功节点不重跑 |
| Replay | None | thread_id + checkpoint_id | 从历史点重跑,后面的节点全重跑 |
| Fork | None | update_state 返回的 config | 修改状态后跑一条新分支 |
五、面试题
面试题1:失败恢复时,输入为什么要传 None?
答:传 None告诉 LangGraph "我不是要开启一个新任务,而是要恢复之前的任务"。LangGraph 会从检查点读取历史状态,而不是用新的输入覆盖它。
面试题2:失败恢复时,已经成功的节点会重新执行吗?
答:不会。LangGraph 会把已成功节点的结果保存为 pending_writes,恢复时直接复用。只有失败的节点和它后面的节点会重新执行。
面试题3:Replay 和失败恢复有什么区别?
答:
- 失败恢复:从最新检查点恢复,config 里只有
thread_id - Replay:从指定历史检查点恢复,config 里有
thread_id+checkpoint_id
失败恢复是"接着跑",Replay 是"回去重新跑"。
面试题4:Fork 时 as_node参数的作用是什么?
答:as_node告诉 LangGraph "我这个修改,假装是某个节点的输出"。这决定了:
- 哪些节点会被跳过(
as_node之前的节点不执行) - 路由函数会不会被触发(如果
as_node有条件边,路由函数会执行)
面试题5:Fork 和 Replay 的根本区别是什么?
答:
- Replay:不改状态,原封不动重跑后面的节点
- Fork:改了状态,跑一条新的执行分支
Replay 是"再看一遍电影",Fork 是"改个结局重拍"。
检查点就像你图的"存档系统"。 失败了可以读档重来,想试试不同选择可以分叉存档。用好这些功能,你的图就不再是一次性的脚本,而是一个可以反复调试、不断优化的可靠系统。