培歌行学LangGraph(16):持久化模式——查看历史检查点——你的图走过的每一步都有记录

0 阅读7分钟
  1. 培歌行学LangGraph(1):从 LangChain 到 LangGraph 的思维跃迁]
  2. 培歌行学LangGraph(2):Reducer(归约器)彻底搞懂
  3. 培歌行学LangGraph(3):编译、执行与可视化
  4. 培歌行学LangGraph(4):一文搞懂图的状态(State)管理
  5. 培歌行学LangGraph(5):状态管理与graph.invoke入参深度解析
  6. 培歌行学LangGraph(6):Multi Schema多状态管理详解
  7. 培歌行学LangGraph(7):预定义状态MessagesState与AgentState
  8. 培歌行学LangGraph(8):控制流详解
  9. 培歌行学LangGraph(9):控制流详解:defer延迟节点——让收尾工作自动排到最后
  10. 培歌行学LangGraph(10):多分支汇聚Fan-in——多个分支如何汇聚到一起?
  11. 培歌行学LangGraph(11):用循环结构实现 ReAct Agent
  12. 培歌行学LangGraph(12):递归限制——别让你的图无限跑下去
  13. 培歌行学LangGraph(13):节点执行与容错机制
  14. 培歌行学LangGraph(14):启用可恢复执行(Checkpoint)
  15. 培歌行学LangGraph(15):持久化模式——何时保存检查点?

一、为什么需要查看历史检查点?

想象一下你在玩游戏:

  • 你存了好几个档
  • 有时候你想看看某个存档点的人物属性
  • 有时候你想读档回到某个位置

检查点就是这个"存档"。启用检查点后,LangGraph 会把同一个 thread_id下的每一次执行都记录下来。你可以:

  • 回头看:某一步执行完后,状态长什么样?
  • 读档:回到某个历史时刻,看看当时的情况
  • 恢复:程序崩了,从最近的存档点继续

LangGraph 给了你两个方法来看这些"存档":

方法作用
get_state_history(config)查看所有历史存档(从新到旧)
get_state(config)查看最新存档,或者查看指定 ID​ 的存档

二、每个存档长什么样?(StateSnapshot)

每个检查点都是一个 StateSnapshot对象,里面装着这些信息:

字段意思举个例子
values这一刻的状态值{"topic": "猫咪", "poem": "床前明月光..."}
next下一步要执行哪个节点("node_poem", "node_joke")
config这个存档的身份证,里面有 thread_idcheckpoint_id一串乱码一样的 ID
metadata元数据,比如这是第几步{"step": 1}
created_at什么时候创建的时间戳
parent_config上一个存档的身份证串成一串链条
tasks下一步要做哪些任务比如"写诗"、"讲笑话"
interrupts有没有被人为打断一般是空的

三、查看所有历史存档(get_state_history)

3.1 怎么用

history = list(graph.get_state_history(config=config))

就这么一行,就能拿到这个 thread_id下的所有存档

注意:返回的顺序是从新到旧,最新的排第一个。

3.2 举个例子

假设我们有一个图,功能是:给定一个主题,同时生成一首诗和一个笑话,然后把它们拼在一起输出。

# 省略了图和节点的定义...

checkpointer = InMemorySaver()
config = {"configurable": {"thread_id": "123"}}
graph = builder.compile(checkpointer=checkpointer)

# 跑一次
result = graph.invoke({"topic": "猫咪"}, config=config)

# 查看所有存档
history = list(graph.get_state_history(config=config))

3.3 你会看到什么?

你会看到 4 个存档,按顺序是:

[第2步, 第1步, 第0步, 第-1步]

每个存档的内容如下:

位置第几步状态里有什么下一步要去哪
第1个第2步主题、诗、笑话、最终输出都有了结束了,哪儿也不去
第2个第1步主题、诗、笑话都有了去汇总节点
第3个第0步只有主题去写诗和讲笑话两个节点
第4个第-1步空的去启动节点

3.4 每个存档到底什么意思?

第-1步(输入存档)

  • 刚刚收到你的输入,还没开始处理
  • 状态是空的 {}
  • 下一步:启动内部流程

第0步(第一个工作存档)

  • 你的输入"猫咪"已经被写进状态了
  • 状态:{"topic": "猫咪"}
  • 下一步:同时去写诗和讲笑话

第1步(第二个工作存档)

  • 诗和笑话都写完了
  • 状态:{"topic": "猫咪", "poem": "...", "joke": "..."}
  • 下一步:去汇总节点,把诗和笑话拼起来

第2步(第三个工作存档)

  • 全都完成了
  • 状态里连最终输出都有了
  • 下一步:空,结束了

3.5 一个小坑:tasks 字段

你可能会在 Step 0 的存档里看到这样的情况:

tasks=(
    任务(name="写诗", result={"poem": "床前明月光..."}),
    任务(name="讲笑话", result={"joke": "一只猫走进酒吧..."}),
)

哎?这不明明是 Step 0 的存档吗?怎么连 Step 1 的结果都有了?

解释:这是 LangGraph 的一个优化。它会在 Step 0 执行完后,先把 Step 0 的"主存档"保存好,然后把 Step 1 要执行的任务结果也绑到这个存档上。这样,如果 Step 1 某个节点失败了,已经成功的节点就不用重新跑了。


四、只看最新存档(get_state)

4.1 怎么用

latest = graph.get_state(config=config)

只返回最新的那个存档

4.2 什么时候用

  • 图跑完了,想看看最终状态
  • 图跑到一半停了,想看看卡在哪一步

4.3 怎么看

latest = graph.get_state(config=config)
print(latest.values)  # 最新的状态
print(latest.next)    # 下一步要去哪(空=结束了)

如果图已经跑完了,next会是空的 ()


五、查看指定存档(get_state + checkpoint_id)

5.1 怎么用

# 先拿到某个存档的 checkpoint_id
checkpoint_id = history[2].config["configurable"]["checkpoint_id"]

# 构造目标配置
target_config = {
    "configurable": {
        "thread_id": "123",
        "checkpoint_id": checkpoint_id
    }
}

# 查看这个存档
snapshot = graph.get_state(config=target_config)

5.2 什么时候用

  • 你想回到第 0 步看看当时的状态
  • 你想从某个历史时刻"分叉",开一条新的执行路线
  • 你想审计某个时间点的状态

5.3 完整例子

# 1. 拿到所有存档
history = list(graph.get_state_history(config=config))

# 2. 挑一个存档(比如第 0 步)
checkpoint_id = history[2].config["configurable"]["checkpoint_id"]

# 3. 查看这个存档
target_config = {
    "configurable": {
        "thread_id": "123",
        "checkpoint_id": checkpoint_id
    }
}
snapshot = graph.get_state(config=target_config)

# 4. 看看当时的状态
print(snapshot.values)  # 只有 {"topic": "猫咪"}
print(snapshot.next)    # 下一步去写诗和讲笑话

六、三种方式总结

方式怎么写拿到什么什么时候用
看所有存档get_state_history(config)全部存档,从新到旧调试、审计、选一个存档来回退
看最新存档get_state(config)最新的那个看看现在跑到哪了
看指定存档get_state(config + checkpoint_id)指定的那一个回溯、分叉、恢复

七、面试题

面试题1:get_state_history返回的存档是按什么顺序排列的?

:从最新到最旧。第一个是最新的,最后一个是第 -1 步的输入存档。

面试题2:第 -1 步的存档是什么意思?为什么它的值是空的?

:第 -1 步是"输入存档",刚收到你的输入但还没开始处理。所以状态是空的 {},下一步是去启动内部流程。

面试题3:为什么 Step 0 的存档里,会有 Step 1 的任务结果?

:这是 LangGraph 的优化。它会先把"主存档"保存好,然后把下一步的任务结果也绑到这个存档上。这样如果某个节点失败了,已经成功的节点就不用重新跑。

面试题4:get_state(config)get_state(config + checkpoint_id)有什么区别?

  • 只传 thread_id:看最新的存档
  • thread_id+ checkpoint_id:看指定的存档

一个看"现在",一个看"当时"。

面试题5:查看历史存档在实际项目中有啥用?

  1. 查 bug:看看某一步的状态对不对
  2. 审计:记录每一步的变化
  3. 分叉:从某个历史时刻开一条新路
  4. 恢复:从故障点继续跑
  5. 回滚:发现错了,回到之前的状态重新来

检查点就像你图的"朋友圈"。 ​ 每走一步就发一条动态,记录着那一刻的状态。学会翻看这些动态,你就能清楚地知道你的图每一步都干了什么。