LangGraph 智能体终于有了一种可靠的方法来保持其状态,此前它们经历了数周的静默数据丢失。在尝试了三种失败的检查点(checkpointing)方案——SQLite、原始对象存储以及每种方案的一个损坏版本后,作者最终采用了一种原子更新模式,从而防止智能体在每次收到请求时都从头开始。

为什么检查点对 LangGraph 至关重要

LangGraph 让开发者能够将 LLM 调用组合成可复用的“智能体”,这些智能体可以记住对话早期的内容。这些智能体会将用户请求分解为子任务,存储中间结果,并在下次调用时从上次中断的地方继续。如果存储的状态消失了,智能体就会重新计算所有内容,从而浪费计算资源、增加延迟并导致糟糕的用户体验。在一个处理 Telegram 消息的生产级机器人中,这种丢失抹去了数周的对话历史。

第一种修复方案:SQLite saver

当单个实例运行智能体时,内置的 SqliteSaver 工作良好。它将每个检查点作为 JSON blob 写入本地 SQLite 文件。当开发者向 AgentState 类型添加新字段并重新部署时,问题出现了。在 Schema 变更之前创建的现有检查点缺少该新字段。由于 SqliteSaver 从不运行迁移(migration),LangGraph 加载了不完整的 JSON,丢弃了缺失的数据,导致智能体从头开始。

关键点:当需要进行 Schema 演进时,SQLite 存储只是一个演示工具,而非生产级解决方案。

第二种修复方案:对象存储

为了获得对序列化格式的控制权,作者编写了一个自定义的 saver,将 JSON 检查点上传到 Oracle Cloud Object Storage。此举为手动管理 Schema 版本提供了灵活性,但也引入了一种新的故障模式。当两个请求同时访问同一个对话线程时,两者都会尝试覆盖同一个对象。对象存储服务针对“一次写入,多次读取”模式进行了优化;它们不提供原子覆盖语义。竞态条件导致了格式错误或截断的 JSON 文件,智能体再次丢失了上下文。

关键点:当多个工作进程可以同时操作同一个键时,对象存储中的简单覆盖操作是不安全的。

第三种修复方案:带版本控制的原子更新

最终稳定的设计结合了两个想法:显式版本号和基于对象 ETag(存储服务的校验和标识符)的条件写入。

  1. 读取当前检查点并获取其 ETag。
  2. 递增检查点外壳(envelope)内部的版本字段。
  3. 写入更新后的检查点,使用条件请求,该请求仅在 ETag 与之前读取的 ETag 匹配时才会成功。
  4. 如果由于另一个进程更改了对象而导致条件写入失败,则重试整个“读取-递增-写入”循环。

由于只有在没有其他进程修改文件的情况下写入才会成功,因此一次只能有一个工作进程提交新状态。版本字段还使得检测过时的检查点并在 Schema 变更时进行迁移变得容易。

该模式适用于支持基于 ETag 的条件写入的对象存储。

给 AI 工程师的教训

  • 仅将 SQLite 用于原型。 生产级智能体需要能够处理 Schema 变更和并发写入的存储。
  • 自行规划 Schema 迁移。 类型字典(TypedDict)描述了用于静态分析的形状,但并不强制执行运行时结构。
  • 将状态视为共享资源。 并发 Bug 会表现为静默数据丢失;它们比直接抛出异常更难调试。
  • 利用云原生原语。 基于 ETag 的条件写入可以提供廉价的乐观锁,而无需单独的锁服务。
  • 记录每一步。 静默失败——例如 LangGraph 忽略的缺失字段——是最难追踪的。

LangGraph 检查点的下一步是什么?

对于已经遇到相同障碍的团队,原子更新方案提供了一个快速、低成本的修复方法。它表明,可靠的生产流水线并不需要重量级的状态存储——只需要仔细处理并发和版本控制。

总结: 一个简单的带版本号的外壳加上条件写入,就能将一个不稳定的系统转变为可靠的系统,让 AI 工程师能够专注于智能体逻辑,而不是无休止的数据丢失调试。