- 发布日期
第 17 讲 · 检查点实现对比:InMemory / SQLite / Postgres
学习目标
- 理解三种 saver 的存储结构与差异。
- 掌握"三表分离"设计(checkpoint / blobs / writes)。
- 看懂
channels_from_checkpoint如何从存档重建通道。
三表同构的设计思想
三种实现都把数据拆成逻辑上的三部分(Postgres 是三张表,InMemory 是三个 dict, SQLite 略有合并):
- checkpoint 骨架:版本、id、metadata、父链——小而多。
- channel blobs:通道大对象的值——大但可复用(多个 checkpoint 共享同一版本的通道值)。
- writes:pending writes——增量。
把"大对象"和"骨架"分开存,是为了避免每个 checkpoint 都重复存一遍完整通道值 (回顾第 6 讲 DeltaChannel 也是冲着这个膨胀问题去的)。
1. InMemorySaver:最小参考实现
libs/checkpoint/langgraph/checkpoint/memory/__init__.py,InMemorySaver(33 行起,别名 MemorySaver)。
# thread_id → ns → checkpoint_id → (checkpoint_blob, metadata_blob, parent_id)
storage
# (thread_id, ns, checkpoint_id) → {(task_id, idx): (task_id, channel, blob, task_path)}
writes
# (thread_id, ns, channel, version) → (type, bytes)
blobs
关键方法:
put(427–471 行):把channel_values剥离到blobs,checkpoint 只存骨架 + 父指针。get_tuple(236–316 行):有checkpoint_id就精确查,否则取max(checkpoint_id)(最新)。_load_blobs(125–140 行):按channel_versions从 blobs 拼回 channel_values。
用途:调试/测试/教学。进程退出即丢,不能用于生产。读它的源码是理解检查点机制的最佳入口 (没有 SQL/连接池干扰)。
2. PostgresSaver:生产首选
libs/checkpoint-postgres/。表结构(postgres/base.py 43–91 行):
| 表 | 主键 | 内容 |
|---|---|---|
checkpoint_migrations | v | schema 迁移版本 |
checkpoints | (thread_id, checkpoint_ns, checkpoint_id) | checkpoint JSONB 骨架 + metadata + parent id |
checkpoint_blobs | (thread_id, checkpoint_ns, channel, version) | 通道大对象(type + BYTEA) |
checkpoint_writes | (thread_id, checkpoint_ns, checkpoint_id, task_id, idx) | pending writes |
特点:
- 读取一条 SQL 搞定(
SELECT_SQL,93–118 行):用jsonb_each_text(channel_versions)JOIN blobs,并聚合 writes,一次取回完整 tuple。 - 写入分流(
put,263–345 行):原始类型(str/int/float/bool/None)内联进 checkpoint JSONB, 复杂对象才落checkpoint_blobs表——小值省一次 JOIN,大值不重复存。 - 连接池 + pipeline:高并发友好。
- DeltaChannel 历史(
get_delta_channel_history,444–550 行):两阶段 SQL 走祖先链(第 6 讲)。
还有 ShallowPostgresSaver(shallow.py):每个 namespace 只保留最新一个 checkpoint (PK 为 (thread_id, checkpoint_ns)),不存历史——省空间,但不能时间旅行(第 18 讲)。
3. SqliteSaver:轻量本地
libs/checkpoint-sqlite/。表结构(sqlite/__init__.py 139–163 行)只有两张:
| 表 | 内容 |
|---|---|
checkpoints | 完整 checkpoint BLOB(serde 整体序列化)+ metadata |
writes | pending writes |
与 Postgres 的差异:
| 维度 | Postgres | SQLite |
|---|---|---|
| 通道大对象 | 独立 checkpoint_blobs 表 | 全部 inline 在 checkpoint BLOB |
| metadata | JSONB 原生可查 | json.dumps 存 BLOB |
| 并发 | 连接池 + pipeline | 单连接 + 锁,WAL 模式 |
| 异步 | AsyncPostgresSaver | AsyncSqliteSaver(同步版的 a* 会报错) |
| 适用 | 生产、高并发、需历史查询 | 本地开发、单机、嵌入式 |
重建通道:channels_from_checkpoint
无论哪种存储,加载时都汇聚到 libs/langgraph/langgraph/pregel/_checkpoint.py 的 channels_from_checkpoint(136–184 行):
flowchart TD
A[get_tuple 取 CheckpointTuple] --> B[遍历每个通道]
B --> C{channel_values 有值?}
C -->|有| D[channel.from_checkpoint 还原]
C -->|无 且是 DeltaChannel| E[get_delta_channel_history<br/>祖先 replay 重建]
D --> F[活的内存通道]
E --> F
F --> G[交给 PregelLoop 继续执行]
- 普通通道:
from_checkpoint(blob)还原(第 3 讲)。 - DeltaChannel:缺完整值时走祖先链 replay(第 6 讲),这就是各 saver 要实现
get_delta_channel_history的原因。
配套的 create_checkpoint(61–121 行)是反向:从活通道构建新 checkpoint, 并决定 DeltaChannel 是否写快照。
使用场景:选型决策
| 场景 | 选择 |
|---|---|
| 单元测试 / demo | InMemorySaver |
| 本地开发 / 桌面应用 / 单机 | SqliteSaver |
| 生产 / 多副本 / 高并发 | PostgresSaver |
| 只要"当前状态"不要历史(省空间,放弃时间旅行) | ShallowPostgresSaver |
| 敏感数据合规 | 任意 saver + EncryptedSerializer(第 16 讲) |
动手实验
- 同一个图分别用
InMemorySaver和SqliteSaver跑,确认行为一致。 - 用 SQLite 跑几轮后,直接打开 .db 文件看
checkpoints/writes两张表的行。 - 读
InMemorySaver.put与get_tuple,画出 storage/blobs/writes 三结构的读写路径。
阅读作业
- 精读
checkpoint/memory/__init__.py的put/get_tuple/_load_blobs。 - 浏览
checkpoint-postgres/.../base.py的表结构与SELECT_SQL。 - 精读
pregel/_checkpoint.py的channels_from_checkpoint与create_checkpoint。
小结
- 三种 saver 共享"checkpoint 骨架 / channel blobs / writes"的逻辑三分结构。
- Postgres 三表 + 大对象分离 + 一条 SQL 取回;SQLite 两表 inline;InMemory 是最佳学习样本。
- 加载统一经
channels_from_checkpoint重建通道,DeltaChannel 走祖先 replay。
下一讲:基于检查点的三大高级能力——续跑、时间旅行、人审中断。