LangGraph 状态机持久化:数据库存储 vs 文件存储的优缺点对比


1. 引入与连接(唤起兴趣与建立关联)

1.1 引人入胜的开场:AI Agent 团队协作中的崩溃时刻

想象你正在构建一个多Agent 医疗诊断助手

  • 第一个Agent是“病历收集师”,负责与用户聊天10分钟,提取结构化症状(发热、咳嗽、胸痛、血氧92%、糖尿病史)和非结构化病历片段(上周受凉后开始,自行吃阿莫西林无效,痰中带少量血丝);
  • 第二个Agent是“影像判读助手”,等待用户上传CT片;
  • 第三个Agent是“初步诊断师”,结合症状+影像生成3种可能诊断(社区获得性肺炎、肺结核、肺癌早期排查);
  • 第四个Agent是“专家复核助手”,结合循证医学知识库调整诊断优先级;
  • 第五个Agent是“报告生成与预约助手”,输出最终报告并自动联系附近三甲医院的呼吸内科。

这是一个完美的LangGraph 状态机(State Machine)驱动的长流程多Agent系统,对吧?但有一天,你正在测试这个系统,到“初步诊断师”生成完2种诊断、正要调用知识库生成第3种时——服务器突然断电重启了!

所有Agent之前的努力瞬间化为乌有:

  • 收集的病历片段和结构化症状丢了;
  • 用户上传CT片的临时存储路径丢了;
  • 前两种初步诊断和调整优先级的中间状态丢了;
  • 用户肯定会生气地关掉页面,再也不用你的诊断助手了。

为什么会发生这种事?
因为你偷懒了!你没有给LangGraph状态机做持久化(Persistence)——也就是没有把状态机运行过程中的当前节点、已完成节点的输出、等待外部输入的标记、Agent的上下文记忆这些“状态快照”保存到**非易失性存储(Non-Volatile Storage,NVS)里,比如硬盘上的文件或数据库,而是只放在了内存(RAM,随机存取存储器)**里,一旦断电或进程重启,内存里的所有数据都会被清空。

1.2 与读者已有知识建立连接

如果你已经用过LangChain,那你应该对记忆(Memory)模块不陌生——它是用来保存Agent与用户的对话历史、内部推理过程的。但LangGraph的持久化,和LangChain的传统记忆模块本质上是不同的两个概念

  • LangChain的记忆模块,更像是一个**“附加的对话记录本”**,它只记录特定的对话或推理片段,不能完整记录整个Agent流程的“位置”——比如当前正在运行第几个节点、节点1有没有成功调用API、节点2有没有收到外部触发(比如CT片上传);
  • 而LangGraph的持久化,记录的是**“整个状态机的完整快照”**——包括:
    1. 当前状态(Current State):状态机中定义的所有状态变量的值,比如收集的症状、上传的CT片路径、已生成的诊断、用户的对话历史;
    2. 历史轨迹(History Trace):状态机从启动到现在所有走过的节点、每个节点的输入输出、调用的工具、执行的时间戳;
    3. 检查点信息(Checkpoint Info):比如上一次保存检查点的时间、检查点的版本号、当前状态是否是“等待外部触发(Awaiting Message/Input)”的状态;
    4. 流程控制信息(Flow Control Info):比如当前正在执行的节点ID、下一个可能执行的节点ID列表(如果有分支的话)、分支的条件判断结果。

简单来说:

LangChain的记忆模块 = 状态机的一部分状态变量
LangGraph的持久化 = 状态机的完整“时光机记录”

有了这个“时光机记录”,即使服务器断电重启,你也可以从最后一个保存的检查点恢复状态机的运行——比如刚才的医疗诊断助手,可以直接恢复到“初步诊断师正在调用知识库生成第3种诊断”的状态,用户甚至不用重新输入任何信息,系统就会继续完成剩下的工作。

1.3 学习价值与应用场景预览

这篇文章能给你带来什么价值?

  1. 深入理解LangGraph持久化的核心原理:你会知道LangGraph是怎么设计检查点机制的,检查点里到底存了什么;
  2. 掌握LangGraph持久化的两种主流方案:文件存储(File Storage)和数据库存储(Database Storage)的具体实现方法;
  3. 搞懂两种方案的优缺点对比维度:比如性能、可扩展性、可靠性、安全性、易用性、成本、维护难度;
  4. 学会根据不同的应用场景选择合适的持久化方案:比如个人原型项目选什么?小团队生产环境选什么?大型企业级多Agent平台选什么?
  5. 获取完整的代码示例和最佳实践:你可以直接把代码复制到你的项目里,或者根据最佳实践调整你的持久化方案。

这篇文章的适用场景非常广泛,只要你用LangGraph构建长流程、多节点、有外部触发、需要恢复运行的系统,都用得上:

  • 长对话/长任务Agent:比如在线编程助手(可以连续写1000行代码、调试3次、生成测试用例)、论文写作助手(可以连续查10篇文献、写10个章节、修改5次);
  • 多Agent协作系统:比如医疗诊断助手、电商客服+库存+物流联动系统、游戏NPC协作系统;
  • 有外部触发的系统:比如审批流程(需要用户/领导多次点击“同意/驳回”)、数据处理管道(需要等待外部数据上传);
  • 需要高可靠性的系统:比如金融风控系统、医疗系统、工业控制系统。

1.4 学习路径概览

为了让你更好地理解这篇文章,我按照知识金字塔构建者的方法论,设计了一个由浅入深的学习路径:

引入与连接
崩溃场景+记忆vs持久化
适用场景预览

概念地图
核心概念+术语
学科定位+知识图谱

基础理解
状态机持久化的直观解释
LangGraph检查点的简化模型
两种方案的直观示例
常见误解澄清

层层深入
1. LangGraph持久化的核心原理
2. 文件存储方案的细节与实现
3. 数据库存储方案的细节与实现
4. 两种方案的底层逻辑对比

多维透视
历史视角:LangGraph持久化的发展
实践视角:两种方案的真实应用案例
批判视角:两种方案的局限性
未来视角:LangGraph持久化的发展趋势

实践转化
1. 环境安装与配置
2. 系统功能与架构设计
3. 两种方案的核心实现代码
4. 常见问题与解决方案
5. 项目实战演练

整合提升
核心观点回顾
知识体系重构
思考问题与拓展任务
学习资源与进阶路径

接下来,就让我们沿着这条学习路径,一步步深入探索LangGraph状态机持久化的奥秘吧!


2. 概念地图(建立整体认知框架)

2.1 核心概念与关键术语

在开始深入之前,我们先把这篇文章中会用到的核心概念和关键术语定义清楚,避免后面出现歧义:

2.1.1 LangGraph 相关概念
术语 简明定义 生活化类比
LangGraph 由LangChain团队开发的,用于构建有状态、可循环、多节点、可控制流的Agent系统的框架。 工厂流水线的控制系统:可以控制产品(任务)在不同的工位(节点)之间流转,支持循环(比如产品不合格可以返回到质检前的工位)、分支(比如根据产品类型选择不同的包装工位)、暂停(比如等待原材料到货)。
状态机(State Machine) LangGraph的核心组件,由**状态(State)、节点(Node)、边(Edge)、条件(Condition)**组成,用于定义Agent系统的流程和状态变化规则。 地铁线路图+列车调度系统:地铁线路图的站点是“节点”,轨道是“边”,换乘站的指示牌是“条件”,列车当前所在的站点和车厢里的乘客(任务数据)是“状态”。
状态(State) 状态机运行过程中所有数据的集合,是一个可序列化的字典或对象 列车车厢里的所有东西:乘客(用户数据)、行李(工具输出)、列车时刻表(流程控制信息)、列车广播(等待外部触发的标记)。
节点(Node) 状态机的基本执行单元,负责修改状态调用工具生成输出 地铁线路图的站点:比如“北京站”(负责检票、安检)、“国贸站”(负责换乘1号线/10号线)、“四惠站”(负责终点/起点)。
边(Edge) 连接两个节点的路径,用于控制状态机的流转方向 地铁线路图的轨道:比如“北京站→建国门站”的轨道,“建国门站→国贸站”的轨道。
条件边(Conditional Edge) 带条件判断的边,用于根据当前状态选择不同的流转方向 地铁换乘站的指示牌:比如“建国门站→1号线方向”的指示牌(条件是乘客要去1号线的站点),“建国门站→2号线外环方向”的指示牌(条件是乘客要去2号线外环的站点)。
入口边(Entry Point) 状态机的启动入口,用于初始化状态并进入第一个节点 地铁线路的起点站入口:比如“四惠站的A口”,乘客从这里进站,上车,然后列车开始运行。
结束边(Finish) 状态机的结束出口,用于终止状态机的运行 地铁线路的终点站出口:比如“苹果园站的B口”,乘客从这里下车,出站,然后列车结束运行。
等待消息(Awaiting Message) 状态机的一种特殊状态,用于暂停状态机的运行,等待外部输入(比如用户的消息、文件的上传、API的回调) 地铁列车在站点停车等待乘客上车:比如列车在“国贸站”停车,等待换乘1号线的乘客上车,乘客不上车,列车就不会继续运行。
2.1.2 持久化相关概念
术语 简明定义 生活化类比
持久化(Persistence) 内存中的易失性数据保存到**非易失性存储(NVS)**里的过程,以便在进程重启、服务器断电、网络故障之后恢复数据。 把笔记本上的草稿整理到硬盘上的文档里:草稿写在笔记本上(内存),如果笔记本丢了或者没电了(进程重启/断电),草稿就没了;整理到硬盘上的文档里(NVS),即使笔记本丢了,你也可以在另一台电脑上打开文档(恢复数据)。
检查点(Checkpoint) 持久化过程中保存的状态机的完整快照,是恢复状态机运行的依据。 游戏中的存档点:你在游戏中打到第10关,保存了存档(检查点);如果游戏崩溃了,你可以从第10关的存档点继续玩(恢复状态机),不用从第1关重新开始。
检查点存储(Checkpoint Storage) 专门用于存储检查点的非易失性存储系统,是LangGraph持久化的核心组件。 游戏存档的存储位置:比如你可以把游戏存档存在硬盘上的文件夹里(文件存储),也可以存在云盘上的数据库里(数据库存储)。
序列化(Serialization) 把**内存中的复杂数据结构(比如字典、对象、列表)转换成可存储、可传输的简单格式(比如JSON、Pickle、Protobuf)**的过程。 把家里的家具拆开打包:家具是复杂的数据结构,拆开打包成纸箱(序列化格式)之后,才能存到仓库里(NVS)或者运到别的地方(传输)。
反序列化(Deserialization) 序列化的逆过程,把可存储、可传输的简单格式转换成内存中的复杂数据结构的过程。 把仓库里的纸箱拆开组装成家具:纸箱是序列化格式,拆开组装成家具(复杂数据结构)之后,才能放在家里使用(内存中运行)。
非易失性存储(NVS) 断电后数据不会丢失的存储系统,比如硬盘、SSD、U盘、云盘、数据库。 仓库:仓库里的东西不会因为仓库关门(断电)而丢失。
易失性存储(VS) 断电后数据会丢失的存储系统,比如内存(RAM)、CPU缓存。 桌子:桌子上的东西如果没人收拾(保存到仓库),桌子被搬走(断电)之后,东西就会丢失。
2.1.3 存储方案相关概念
术语 简明定义 生活化类比
文件存储(File Storage) 检查点保存到文件系统里的持久化方案,比如保存到本地硬盘的JSON/Pickle文件、云存储的对象存储(AWS S3、阿里云OSS、腾讯云COS)。 把游戏存档存在硬盘上的“游戏存档”文件夹里:每个存档是一个单独的文件,文件名是存档的ID。
数据库存储(Database Storage) 检查点保存到数据库里的持久化方案,比如关系型数据库(MySQL、PostgreSQL、SQLite)、NoSQL数据库(MongoDB、Redis、Cassandra)。 把游戏存档存在云盘上的“游戏存档数据库”里:每个存档是数据库里的一条记录,记录的主键是存档的ID。
关系型数据库(RDBMS) 基于关系模型的数据库,数据以**表(Table)的形式存储,表与表之间通过外键(Foreign Key)关联,支持SQL(结构化查询语言)**查询。 Excel表格:每个Excel文件是一个数据库,每个Sheet是一个表,每行是一条记录,每列是一个字段,表与表之间可以通过VLOOKUP函数关联。
NoSQL数据库 非关系型数据库,数据以键值对(Key-Value)、文档(Document)、列族(Column Family)、图(Graph)的形式存储,不支持或部分支持SQL查询,具有高可扩展性、高性能、高可用性的特点。 便利贴:键值对数据库就像“便利贴墙”,每个便利贴有一个唯一的标签(键)和内容(值);文档数据库就像“文件夹里的Word文档”,每个文档是一个单独的文件,内容可以是任意结构。
对象存储(Object Storage) 一种云存储服务,数据以对象(Object)的形式存储,每个对象有一个唯一的键(Key)、内容(Value)、元数据(Metadata),具有无限扩展性、高可用性、低成本的特点,适合存储大文件、非结构化数据 云盘上的“文件库”:每个文件是一个对象,文件名是键,文件内容是值,文件的大小、创建时间、修改时间是元数据。

2.2 概念间的层次与关系

现在,我们把刚才定义的核心概念和关键术语,按照层次关系交互关系组织起来,建立一个整体的认知框架。

2.2.1 层次关系

LangGraph 多Agent系统

LangGraph 状态机

状态
State

节点
Node


Edge

等待消息
Awaiting Message

用户数据
UserData

工具输出
ToolOutput

流程控制信息
FlowControl

上下文记忆
ContextMemory

LangGraph 持久化模块

检查点生成器
CheckpointGenerator

序列化/反序列化器
Serde

检查点存储
CheckpointStorage

文件存储
FileStorage

数据库存储
DatabaseStorage

本地文件系统
LocalFS

云对象存储
CloudObjectStorage

关系型数据库
RDBMS

NoSQL数据库
NoSQL

2.2.2 交互关系

接下来,我们再来看一下LangGraph状态机LangGraph持久化模块之间的交互关系:

非易失性存储 检查点存储 序列化/反序列化器 检查点生成器 状态机 LangGraph Agent系统 用户 非易失性存储 检查点存储 序列化/反序列化器 检查点生成器 状态机 LangGraph Agent系统 用户 服务器突然断电重启,内存中的所有数据丢失 发送初始请求 初始化状态机(创建初始状态) 触发检查点保存(启动时) 获取当前状态+历史轨迹+检查点信息+流程控制信息 序列化检查点 返回序列化后的检查点 保存检查点到存储 写入检查点 写入成功 保存成功 检查点保存完成 执行节点1(修改状态) 触发检查点保存(节点1执行完成后) 获取当前状态+... 检查点保存完成 进入等待消息状态(需要用户上传CT片) 触发检查点保存(进入等待消息状态时) 获取当前状态+...(标记为等待消息) 检查点保存完成 返回等待消息的提示 请上传CT片 再次访问系统(或上传CT片) 查询最新的检查点(根据用户ID/会话ID) 读取检查点 返回检查点 返回最新的检查点 反序列化检查点 返回反序列化后的检查点 从检查点恢复状态机(恢复状态+历史轨迹+流程控制信息+等待消息标记) 状态机恢复成功 上传CT片 发送外部消息(CT片路径) 接收外部消息,继续执行节点2(修改状态) 触发检查点保存(节点2执行完成后) 执行后续节点,直到完成 触发检查点保存(结束时) 返回最终结果 返回最终诊断报告

2.3 学科定位与边界

2.3.1 学科定位

LangGraph状态机持久化,属于**人工智能工程(AI Engineering)**领域的一个子方向,涉及以下几个学科的知识:

  1. 计算机科学(CS):数据结构、算法、操作系统、文件系统、数据库系统;
  2. 软件工程(SE):系统设计、架构设计、接口设计、代码实现、测试、部署、维护;
  3. 人工智能(AI):大语言模型(LLM)、Agent系统、LangChain框架;
  4. 分布式系统(DS):如果是大型企业级应用,还涉及分布式存储、分布式一致性、高可用性、容错性。
2.3.2 边界

我们需要明确LangGraph状态机持久化的边界,避免把它和其他相关概念混淆:

  1. 边界1:≠ LangChain记忆模块:正如我们在1.2节所说的,LangChain记忆模块只是状态机的一部分状态变量,而LangGraph持久化记录的是状态机的完整快照;
  2. 边界2:≠ 大语言模型(LLM)的上下文窗口:LLM的上下文窗口是有限的(比如GPT-4o的上下文窗口是128K token,Claude 3 Opus的上下文窗口是200K token),而LangGraph持久化可以保存无限多的历史轨迹和上下文记忆(只要你的存储足够大);
  3. 边界3:≠ 工具调用的缓存:工具调用的缓存是用来保存工具的输出,避免重复调用工具的,而LangGraph持久化是用来保存状态机的完整快照的,工具调用的缓存可以作为状态机的一部分状态变量保存到检查点里;
  4. 边界4:≠ 数据库的事务(Transaction):数据库的事务是用来保证数据库操作的原子性、一致性、隔离性、持久性(ACID)的,而LangGraph持久化的检查点保存可以看作是一个“简化的事务”——它只保证检查点本身的原子性和持久性,不保证节点执行过程中的所有操作(比如工具调用、数据库写入)的原子性(如果需要保证节点执行过程中的所有操作的原子性,你需要自己实现事务机制)。

2.4 思维导图或知识图谱

最后,我们把刚才的核心概念、层次关系、交互关系、学科定位与边界整合起来,画一个完整的知识图谱

LangGraph 状态机持久化:
数据库存储 vs 文件存储的优缺点对比

核心概念

LangGraph相关

LangGraph

状态机

状态/节点/边

等待消息

持久化相关

持久化

检查点

检查点存储

序列化/反序列化

NVS vs VS

存储方案相关

文件存储

数据库存储

RDBMS vs NoSQL

本地FS vs 云对象存储

整体框架

层次关系

交互关系

学科定位

边界

基础理解

直观解释

简化模型

直观示例

常见误解

层层深入

LangGraph持久化核心原理

检查点结构

检查点触发时机

Serde机制

文件存储方案

本地FS实现

云对象存储实现

优缺点

数据库存储方案

SQLite实现

PostgreSQL实现

MongoDB实现

Redis实现

优缺点

底层逻辑对比

数据结构对比

性能对比原理

可靠性对比原理

多维透视

历史视角

实践视角

批判视角

未来视角

实践转化

环境安装

系统设计

核心实现代码

常见问题与解决方案

项目实战

整合提升

核心观点回顾

知识体系重构

思考问题与拓展任务

学习资源与进阶路径


3. 基础理解(建立直观认识)

3.1 核心概念的生活化解释

在2.1节中,我们已经给每个核心概念都做了生活化类比,现在我们再把这些类比串起来,用一个完整的生活化故事来解释LangGraph状态机持久化的整个过程:

3.1.1 生活化故事:小明的“周末家庭作业流水线”

小明是一个小学生,他有一份周末家庭作业:

  1. 语文作业:写一篇300字的作文《我的周末计划》;
  2. 数学作业:做10道应用题;
  3. 英语作业:背20个单词,默写一遍;
  4. 家长签字:写完所有作业后,让爸爸或妈妈签字。

小明不想一次性写完所有作业,他想写一会儿作业,玩一会儿游戏,但他又怕玩游戏的时候忘记自己写到哪里了,于是他想了一个办法——用一个“作业进度本”来记录自己的作业进度

现在,我们把这个故事映射到LangGraph状态机持久化的概念上:

  • 小明 = LangGraph Agent系统;
  • 周末家庭作业 = Agent系统需要完成的任务;
  • 作业进度本 = 非易失性存储(NVS);
  • 作业进度本上的每一页 = 检查点(Checkpoint);
  • 作业进度本上的内容
    • 当前正在做的作业 = 当前状态(Current State);
    • 已经做完的作业内容 = 历史轨迹(History Trace);
    • 最后一次记录进度的时间 = 检查点信息(Checkpoint Info);
    • 接下来要做的作业 = 流程控制信息(Flow Control Info);
  • 小明写作业的过程 = 状态机的运行过程;
  • 小明停下来玩游戏之前,在作业进度本上记录进度 = 检查点的保存过程;
  • 小明玩完游戏之后,翻开作业进度本,找到最后一页,继续写作业 = 检查点的恢复过程;
  • 如果作业进度本是一个普通的笔记本(放在书桌上,不会丢) = 文件存储(File Storage);
  • 如果作业进度本是一个电子表格(存在爸爸的电脑里,不会丢) = 数据库存储(Database Storage)。
3.1.2 作业进度本的具体内容(检查点的直观结构)

现在,我们来看一下小明的作业进度本上的某一页(检查点)的具体内容:

作业进度本 - 第5页
记录时间:202X年X月X日 14:30
记录人:小明
当前状态:
  - 正在做的作业:英语作业
  - 已经背完的单词:15个(apple, banana, cherry, ..., orange)
  - 已经默写的单词:0个
历史轨迹:
  - 202X年X月X日 10:00:开始做语文作业
  - 202X年X月X日 10:45:完成语文作业,作文题目《我的周末计划》,内容320字
  - 202X年X月X日 10:50:开始做数学作业
  - 202X年X月X日 11:30:完成数学作业,10道应用题全对
  - 202X年X月X日 11:35:开始玩游戏(休息)
  - 202X年X月X日 13:30:开始做英语作业
  - 202X年X月X日 14:25:背完15个单词
接下来要做的作业:
  - 继续背剩下的5个单词(pear, peach, grape, watermelon, strawberry)
  - 默写20个单词
  - 让爸爸或妈妈签字
备注:
  - 现在有点累了,想再玩1小时游戏

这就是一个直观的检查点结构,它记录了小明的作业进度的所有信息——即使小明玩游戏玩到忘记自己写到哪里了,只要翻开作业进度本的第5页,他就能继续完成剩下的作业。

3.2 简化模型与类比

3.2.1 LangGraph持久化的简化模型

为了让你更好地理解LangGraph持久化的核心原理,我们把它简化成一个**“三层模型”**:

┌─────────────────────────────────────────────────────────┐
│                    应用层(Agent系统)                    │
│  - 接收用户请求                                           │
│  - 调用状态机执行任务                                     │
│  - 返回最终结果给用户                                     │
└──────────────────────┬──────────────────────────────────┘
                       │
┌──────────────────────▼──────────────────────────────────┐
│              持久化层(LangGraph持久化模块)               │
│  - 检查点生成器:生成状态机的完整快照                     │
│  - 序列化/反序列化器:转换检查点的格式                   │
│  - 检查点存储抽象接口:定义检查点存储的统一操作           │
│    - 保存检查点(save_checkpoint)                       │
│    - 读取检查点(load_checkpoint)                       │
│    - 查询检查点列表(list_checkpoints)                  │
│    - 删除检查点(delete_checkpoint)                     │
└──────────────────────┬──────────────────────────────────┘
                       │
┌──────────────────────▼──────────────────────────────────┐
│                  存储层(检查点存储)                       │
│  - 文件存储实现:实现检查点存储抽象接口,使用文件系统存储   │
│  - 数据库存储实现:实现检查点存储抽象接口,使用数据库存储   │
└─────────────────────────────────────────────────────────┘

这个三层模型的核心是**“检查点存储抽象接口”**——它把持久化层和存储层解耦了,你可以根据自己的需要选择不同的存储层实现(文件存储或数据库存储),而不需要修改应用层和持久化层的代码。

3.2.2 两种存储方案的简化类比

现在,我们再用一个**“图书馆借书记录系统”**的简化类比,来解释文件存储和数据库存储的区别:

  • 图书馆借书记录系统 = 检查点存储系统;
  • 每一本被借出去的书 = 每一个检查点;
  • 书的借阅信息(书名、作者、借阅人、借阅时间、归还时间) = 检查点的内容(当前状态、历史轨迹、检查点信息、流程控制信息);
文件存储的简化类比:“卡片式借书记录系统”

如果图书馆使用的是**“卡片式借书记录系统”**(文件存储):

  • 每一本被借出去的书,都有一张单独的借阅卡片(单独的文件);
  • 借阅卡片上记录了书的借阅信息(文件的内容);
  • 所有的借阅卡片都放在一个文件柜里(本地文件系统的文件夹),或者一个云端的文件柜里(云对象存储的Bucket);
  • 如果你想找某一本书的借阅信息(读取某一个检查点):
    • 你需要知道这本书的借阅卡片的编号(文件的键/文件名);
    • 然后你需要在文件柜里找到对应的借阅卡片(在文件系统里找到对应的文件);
    • 最后你需要翻开借阅卡片查看借阅信息(读取文件的内容);
  • 如果你想找所有“小明”借的书的借阅信息(查询所有属于某一个会话ID的检查点):
    • 你需要把文件柜里的所有借阅卡片都翻一遍(遍历文件系统里的所有文件);
    • 然后你需要找出借阅人是“小明”的借阅卡片(筛选出会话ID匹配的文件);
  • 如果你想修改某一本书的借阅信息(更新某一个检查点):
    • 你需要找到对应的借阅卡片(找到对应的文件);
    • 然后你需要用橡皮擦把旧的信息擦掉(删除旧的文件);
    • 最后你需要写上新的信息(写入新的文件);
数据库存储的简化类比:“电子借书记录系统”

如果图书馆使用的是**“电子借书记录系统”**(数据库存储):

  • 所有书的借阅信息都存储在一个电子表格里(关系型数据库的表),或者一个电子文档库里(NoSQL数据库的集合);
  • 如果你想找某一本书的借阅信息(读取某一个检查点):
    • 你只需要在电子表格的搜索框里输入书的编号(检查点的ID);
    • 电子表格会立刻给你显示对应的借阅信息(数据库会立刻返回对应的记录);
  • 如果你想找所有“小明”借的书的借阅信息(查询所有属于某一个会话ID的检查点):
    • 你只需要在电子表格的筛选框里选择借阅人是“小明”(数据库的WHERE语句);
    • 电子表格会立刻给你显示所有匹配的借阅信息(数据库会立刻返回所有匹配的记录);
  • 如果你想修改某一本书的借阅信息(更新某一个检查点):
    • 你只需要在电子表格里找到对应的记录(数据库的WHERE语句);
    • 然后你直接修改对应的单元格(数据库的UPDATE语句);
    • 不需要删除旧的记录,也不需要写入新的记录;

3.3 直观示例与案例

3.3.1 直观示例1:文件存储的检查点(JSON格式)

现在,我们来看一个真实的LangGraph检查点的JSON格式示例(文件存储的情况):
假设我们有一个简单的“问答Agent”,它的状态机有三个节点:

  1. 入口节点:初始化状态(接收用户的问题);
  2. LLM节点:调用LLM回答用户的问题;
  3. 结束节点:返回最终答案。

用户的问题是:“中国的首都是哪里?”,LLM的回答是:“中国的首都是北京。”,那么这个问答Agent的检查点(在LLM节点执行完成后、结束节点执行前保存的)的JSON格式如下:

{
  "checkpoint_id": "8a7b6c5d-4e3f-2a1b-0c9d-8e7f6a5b4c3d",
  "parent_checkpoint_id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
  "thread_id": "user-123-session-456",
  "checkpoint_ns": "",
  "checkpoint": {
    "v": 1,
    "ts": "202X-05-20T12:34:56.789Z",
    "channel_values": {
      "messages": [
        {
          "role": "user",
          "content": "中国的首都是哪里?"
        },
        {
          "role": "assistant",
          "content": "中国的首都是北京。"
        }
      ],
      "next": "end"
    },
    "channel_versions": {
      "messages": 2,
      "next": 2,
      "__start__": 1
    },
    "versions_seen": {
      "__start__": {
        "__pregel__": 1
      }
    },
    "current_tasks": [],
    "pending_sends": []
  },
  "metadata": {
    "source": "loop",
    "step": 2,
    "writes": {
      "llm": {
        "messages": [
          {
            "role": "assistant",
            "content": "中国的首都是北京。"
          }
        ]
      }
    }
  }
}

这个JSON检查点的内容和我们刚才小明的作业进度本的内容是一一对应的:

  • checkpoint_id:检查点的唯一ID(相当于作业进度本的页码);
  • parent_checkpoint_id:上一个检查点的唯一ID(相当于作业进度本的上一页页码);
  • thread_id:会话的唯一ID(相当于作业进度本的主人——小明的名字);
  • checkpoint_ns:检查点的命名空间(用于区分同一个会话中的不同子流程,相当于作业进度本的科目——语文/数学/英语);
  • checkpoint:检查点的核心内容(相当于作业进度本上的当前状态、历史轨迹、流程控制信息):
    • v:检查点的版本号;
    • ts:检查点的保存时间戳(相当于作业进度本的记录时间);
    • channel_values:状态机的当前状态(相当于作业进度本上的当前正在做的作业、已经做完的作业内容):
      • messages:用户和Agent的对话历史(相当于作业进度本上的已经做完的作业内容);
      • next:下一个要执行的节点ID(相当于作业进度本上的接下来要做的作业);
    • channel_versions:每个状态变量的版本号(用于避免冲突);
    • versions_seen:每个节点已经看到的状态变量的版本号;
    • current_tasks:当前正在执行的任务列表;
    • pending_sends:待发送的消息列表;
  • metadata:检查点的元数据(相当于作业进度本上的备注):
    • source:检查点的来源(比如“loop”表示是在状态机的循环中保存的,“input”表示是在接收用户输入时保存的);
    • step:状态机的执行步数(相当于作业进度本上的已经完成的作业步骤数);
    • writes:上一个节点对状态变量的修改(相当于作业进度本上的上一个作业步骤的完成情况)。
3.3.2 直观示例2:数据库存储的检查点(PostgreSQL表结构)

现在,我们再来看一个真实的LangGraph检查点的PostgreSQL表结构示例(数据库存储的情况):
LangGraph官方提供了一个PostgreSQL的检查点存储实现,它使用了三个表来存储检查点的内容:

  1. checkpoints表:存储检查点的核心元数据;
  2. checkpoint_blobs表:存储检查点的核心内容(序列化后的二进制数据);
  3. checkpoint_writes表:存储每个检查点的上一个节点对状态变量的修改。
checkpoints表的结构
CREATE TABLE IF NOT EXISTS checkpoints (
    thread_id TEXT NOT NULL,
    checkpoint_ns TEXT NOT NULL DEFAULT '',
    checkpoint_id TEXT NOT NULL,
    parent_checkpoint_id TEXT,
    checkpoint_ts TIMESTAMPTZ NOT NULL,
    checkpoint_version INTEGER NOT NULL,
    metadata JSONB NOT NULL DEFAULT '{}',
    PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id)
);

CREATE INDEX IF NOT EXISTS checkpoints_thread_ts_idx ON checkpoints (thread_id, checkpoint_ns, checkpoint_ts DESC);
CREATE INDEX IF NOT EXISTS checkpoints_parent_idx ON checkpoints (parent_checkpoint_id);
checkpoint_blobs表的结构
CREATE TABLE IF NOT EXISTS checkpoint_blobs (
    thread_id TEXT NOT NULL,
    checkpoint_ns TEXT NOT NULL DEFAULT '',
    checkpoint_id TEXT NOT NULL,
    type TEXT NOT NULL,
    blob BYTEA NOT NULL,
    PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id, type),
    FOREIGN KEY (thread_id, checkpoint_ns, checkpoint_id) REFERENCES checkpoints (thread_id, checkpoint_ns, checkpoint_id) ON DELETE CASCADE
);
checkpoint_writes表的结构
CREATE TABLE IF NOT EXISTS checkpoint_writes (
    thread_id TEXT NOT NULL,
    checkpoint_ns TEXT NOT NULL DEFAULT '',
    checkpoint_id TEXT NOT NULL,
    task_id TEXT NOT NULL,
    idx INTEGER NOT NULL,
    channel TEXT NOT NULL,
    type TEXT NOT NULL,
    blob BYTEA NOT NULL,
    PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id, task_id, idx),
    FOREIGN KEY (thread_id, checkpoint_ns, checkpoint_id) REFERENCES checkpoints (thread_id, checkpoint_ns, checkpoint_id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS checkpoint_writes_channel_idx ON checkpoint_writes (channel);

这个PostgreSQL表结构的设计是非常合理的:

  • 它把检查点的元数据checkpoints表)和检查点的核心内容checkpoint_blobs表)分开存储,提高了查询元数据的性能;
  • 它使用了复合主键thread_id, checkpoint_ns, checkpoint_id)来唯一标识一个检查点;
  • 它使用了外键约束来保证数据的一致性(如果删除了一个检查点,那么对应的checkpoint_blobscheckpoint_writes记录也会被自动删除);
  • 它使用了索引来提高查询性能(比如checkpoints_thread_ts_idx索引可以快速查询某个会话的所有检查点,按时间倒序排列);
  • 它使用了**
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐