ARTICLE · 1351492
【LangGraph源码精读】第12期|一个Checkpoint存了什么?6个字段全拆解
【LangGraph源码精读】第12期|一个Checkpoint存了什么?6个字段全拆解
30篇源码精读系列·第12篇(持久化层第一篇)。你每次调用invoke,LangGraph都在保存Checkpoint。但一个Checkpoint到底存了什么?为什么不能只存"状态值"?为什么还要版本号、节点看到的版本、待执行Send?这篇把Checkpoint的6个核心字段、CheckpointMetadata、CheckpointTuple全部拆解,讲清楚每个字段的作用、为什么不能少、以及它们怎么一起支撑恢复、中断和时间旅行。
一、开头:为什么不能只存"状态"?
很多人对Checkpoint的理解是:把当前的State(比如messages列表、几个字段)存下来。这还不够吗?
不够。因为恢复时需要回答的问题,远比"状态是什么"多。
恢复时,Pregel引擎要知道:
- ● 这一步之后,下一轮该执行哪些节点?
- ● 每个节点上次看到的是哪个版本,要不要重新触发?
- ● 有没有节点执行到一半、writes已经保存但还没合并?
- ● 有没有运行时动态创建的Send任务还没完成?
- ● 这个Checkpoint是怎么来的?用户输入?循环?手动更新?
- ● 它在整个执行历史中的位置是什么?
只存状态值,这些问题一个都回答不了。 一个Checkpoint真正存储的是"恢复一个Pregel执行所需的完整信息"。这篇把6个核心字段逐个拆开。
二、Checkpoint的6个核心字段总览
另外还有一个schema版本字段v,用于标记Checkpoint结构本身的版本,兼容框架升级。
三、字段一:id:唯一标识
为什么需要id? 因为同一个thread会有很多个Checkpoint(每个Super-step一个)。时间旅行时,你要指定"回到哪一个",靠的就是id。config中可以传入checkpoint_id,让引擎从特定点恢复。
四、字段二:ts:时间戳
ts让Checkpoint形成一条时间线。 list_checkpoints按时间返回历史,前端时间旅行界面按时间展示,排查问题时可以按时间定位。
五、字段三:channel_values:状态的真正载体
这是Checkpoint中最核心、体积最大的字段。
5.1 存了什么?
5.2 为什么序列化?
官方原话:checkpoints that can be resumed on any machine, an arbitrary amount of time after they were saved。 这就是为什么channel_values必须序列化,不能直接存Python对象引用。
5.3 恢复时
channel_values被反序列化,重新填入所有Channel,状态就回到了这个Checkpoint的时刻。
六、字段四:channel_versions:版本号
6.1 存了什么?
6.2 为什么需要版本号?
关键:节点是否执行,看的不是"值变没变",而是"版本号变没变"。 即使新值和旧值相同,只要channel被写入,版本就递增,订阅节点就会触发。
6.3 版本号的递增
每次after_tick应用writes时:
- ● 被写入的channel,版本号递增
- ● 没被写入的channel,版本号保持不变
- ● 新创建的channel,获得初始版本
七、字段五:versions_seen:节点看到的版本
这是最容易被忽略、但最关键的字段。
7.1 存了什么?
7.2 为什么需要versions_seen?
这是Plan阶段判断节点是否执行的核心:
没有versions_seen,引擎无法知道"哪些节点还有新数据要处理"。 这就是为什么不能只存channel_values——还需要记录每个节点"看到了哪一步"。
7.3 一个具体例子
Plan阶段比较:agent看到messages版本4(最新),tools看到版本3(比当前4旧),但tools订阅的是agent节点输出,不是messages...具体触发取决于订阅关系。核心逻辑就是"当前版本 vs 看到版本"。
八、字段六:pending_sends:待执行的动态分支
8.1 存了什么?
8.2 为什么需要pending_sends?
没有pending_sends,恢复后动态创建的并行分支会丢失。
九、CheckpointMetadata:Checkpoint的"元信息"
Checkpoint本体之外,还有一组metadata。
9.1 source:这个Checkpoint怎么来的
9.2 step:在执行中的位置
9.3 其他字段
十、CheckpointTuple:存储层返回的完整对象
从Checkpointer读取时,返回的不是裸Checkpoint,而是CheckpointTuple。
CheckpointTuple = Checkpoint + 元信息 + 父链 + 半成品writes。 这是恢复时需要的所有信息。
10.1 pending_writes的作用
回顾第06期:节点执行完后,writes先通过put_writes保存(task级),在after_tick才合并。如果中间失败了,pending_writes记录了这些"已经完成但没合并"的结果,恢复时不重跑这些节点。
十一、为什么这些字段一个都不能少?
十二、常见坑:Checkpoint的5个问题
十三、面试关联:Checkpoint的5道高频题
十四、本期小结
Checkpoint结构,核心就6句话:
- 1. Checkpoint存储的是"恢复一个Pregel执行所需的完整信息",不是只存状态值。 6个核心字段:id、ts、channel_values、channel_versions、versions_seen、pending_sends。
- 2. channel_values是状态载体,所有channel当前值,序列化存储(默认MsgPack,可加密)。 序列化是为了跨机器、跨时间恢复,不依赖运行进程。
- 3. channel_versions记录每个channel的单调递增版本。 节点触发看的是版本变化,不是值变化。即使值相同,写入后版本递增,节点就会触发。
- 4. versions_seen记录每个节点最近看到的版本,是Plan阶段触发判断的核心。 当前版本 > 节点看到版本 → 触发;所有节点看到最新版本 → 终止。
- 5. pending_sends保存动态创建的并行分支,恢复时不丢失。 metadata记录source(input/loop/update/fork)、step(-1初始,0+循环)、父链。
- 6. CheckpointTuple是存储层返回的完整对象:config、checkpoint、metadata、parent_config、pending_writes。 pending_writes记录"已完成但未合并"的节点结果,支持部分恢复。
十五、下一期预告
下一期(源码13),持久化层第二篇:
【LangGraph源码精读】第13期|为什么持久化接口要这样设计?BaseCheckpointSaver精读
持久化的抽象接口:get_tuple、list、put、put_writes、delete_thread,同步和异步两套方法。搞懂为什么是这5个方法,以及如何实现自己的Checkpointer。
你之前理解Checkpoint存了什么吗?有没有漏掉版本相关的字段?评论区聊聊。
本文为《LangGraph源码精读30讲》系列第12篇。内容基于LangGraph v1.2.11源码结构梳理,核心文件位于libs/checkpoint/langgraph/checkpoint/base/init.py(Checkpoint、CheckpointMetadata、CheckpointTuple、create_checkpoint),以及官方文档checkpointers。版本更新后部分细节可能变化。如有错漏欢迎指正。















































































