发布日期

第 17 讲 · 检查点实现对比:InMemory / SQLite / Postgres

学习目标

  • 理解三种 saver 的存储结构与差异。
  • 掌握"三表分离"设计(checkpoint / blobs / writes)。
  • 看懂 channels_from_checkpoint 如何从存档重建通道。

三表同构的设计思想

三种实现都把数据拆成逻辑上的三部分(Postgres 是三张表,InMemory 是三个 dict, SQLite 略有合并):

  1. checkpoint 骨架:版本、id、metadata、父链——小而多。
  2. channel blobs:通道大对象的值——大但可复用(多个 checkpoint 共享同一版本的通道值)。
  3. writes:pending writes——增量。

把"大对象"和"骨架"分开存,是为了避免每个 checkpoint 都重复存一遍完整通道值 (回顾第 6 讲 DeltaChannel 也是冲着这个膨胀问题去的)。

1. InMemorySaver:最小参考实现

libs/checkpoint/langgraph/checkpoint/memory/__init__.pyInMemorySaver(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_migrationsvschema 迁移版本
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 讲)。

还有 ShallowPostgresSavershallow.py):每个 namespace 只保留最新一个 checkpoint (PK 为 (thread_id, checkpoint_ns)),不存历史——省空间,但不能时间旅行(第 18 讲)。

3. SqliteSaver:轻量本地

libs/checkpoint-sqlite/。表结构(sqlite/__init__.py 139–163 行)只有两张:

内容
checkpoints完整 checkpoint BLOB(serde 整体序列化)+ metadata
writespending writes

与 Postgres 的差异:

维度PostgresSQLite
通道大对象独立 checkpoint_blobs全部 inline 在 checkpoint BLOB
metadataJSONB 原生可查json.dumps 存 BLOB
并发连接池 + pipeline单连接 + 锁,WAL 模式
异步AsyncPostgresSaverAsyncSqliteSaver(同步版的 a* 会报错)
适用生产、高并发、需历史查询本地开发、单机、嵌入式

重建通道:channels_from_checkpoint

无论哪种存储,加载时都汇聚到 libs/langgraph/langgraph/pregel/_checkpoint.pychannels_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 是否写快照。

使用场景:选型决策

场景选择
单元测试 / demoInMemorySaver
本地开发 / 桌面应用 / 单机SqliteSaver
生产 / 多副本 / 高并发PostgresSaver
只要"当前状态"不要历史(省空间,放弃时间旅行)ShallowPostgresSaver
敏感数据合规任意 saver + EncryptedSerializer(第 16 讲)

动手实验

  1. 同一个图分别用 InMemorySaverSqliteSaver 跑,确认行为一致。
  2. 用 SQLite 跑几轮后,直接打开 .db 文件看 checkpoints / writes 两张表的行。
  3. InMemorySaver.putget_tuple,画出 storage/blobs/writes 三结构的读写路径。

阅读作业

  • 精读 checkpoint/memory/__init__.pyput / get_tuple / _load_blobs
  • 浏览 checkpoint-postgres/.../base.py 的表结构与 SELECT_SQL
  • 精读 pregel/_checkpoint.pychannels_from_checkpointcreate_checkpoint

小结

  • 三种 saver 共享"checkpoint 骨架 / channel blobs / writes"的逻辑三分结构。
  • Postgres 三表 + 大对象分离 + 一条 SQL 取回;SQLite 两表 inline;InMemory 是最佳学习样本。
  • 加载统一经 channels_from_checkpoint 重建通道,DeltaChannel 走祖先 replay。

下一讲:基于检查点的三大高级能力——续跑、时间旅行、人审中断。