LangGraph 状态机持久化:数据库存储 vs 文件存储的优缺点对比
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的持久化,记录的是**“整个状态机的完整快照”**——包括:
- 当前状态(Current State):状态机中定义的所有状态变量的值,比如收集的症状、上传的CT片路径、已生成的诊断、用户的对话历史;
- 历史轨迹(History Trace):状态机从启动到现在所有走过的节点、每个节点的输入输出、调用的工具、执行的时间戳;
- 检查点信息(Checkpoint Info):比如上一次保存检查点的时间、检查点的版本号、当前状态是否是“等待外部触发(Awaiting Message/Input)”的状态;
- 流程控制信息(Flow Control Info):比如当前正在执行的节点ID、下一个可能执行的节点ID列表(如果有分支的话)、分支的条件判断结果。
简单来说:
LangChain的记忆模块 = 状态机的一部分状态变量
LangGraph的持久化 = 状态机的完整“时光机记录”
有了这个“时光机记录”,即使服务器断电重启,你也可以从最后一个保存的检查点恢复状态机的运行——比如刚才的医疗诊断助手,可以直接恢复到“初步诊断师正在调用知识库生成第3种诊断”的状态,用户甚至不用重新输入任何信息,系统就会继续完成剩下的工作。
1.3 学习价值与应用场景预览
这篇文章能给你带来什么价值?
- 深入理解LangGraph持久化的核心原理:你会知道LangGraph是怎么设计检查点机制的,检查点里到底存了什么;
- 掌握LangGraph持久化的两种主流方案:文件存储(File Storage)和数据库存储(Database Storage)的具体实现方法;
- 搞懂两种方案的优缺点对比维度:比如性能、可扩展性、可靠性、安全性、易用性、成本、维护难度;
- 学会根据不同的应用场景选择合适的持久化方案:比如个人原型项目选什么?小团队生产环境选什么?大型企业级多Agent平台选什么?
- 获取完整的代码示例和最佳实践:你可以直接把代码复制到你的项目里,或者根据最佳实践调整你的持久化方案。
这篇文章的适用场景非常广泛,只要你用LangGraph构建长流程、多节点、有外部触发、需要恢复运行的系统,都用得上:
- 长对话/长任务Agent:比如在线编程助手(可以连续写1000行代码、调试3次、生成测试用例)、论文写作助手(可以连续查10篇文献、写10个章节、修改5次);
- 多Agent协作系统:比如医疗诊断助手、电商客服+库存+物流联动系统、游戏NPC协作系统;
- 有外部触发的系统:比如审批流程(需要用户/领导多次点击“同意/驳回”)、数据处理管道(需要等待外部数据上传);
- 需要高可靠性的系统:比如金融风控系统、医疗系统、工业控制系统。
1.4 学习路径概览
为了让你更好地理解这篇文章,我按照知识金字塔构建者的方法论,设计了一个由浅入深的学习路径:
接下来,就让我们沿着这条学习路径,一步步深入探索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 层次关系
2.2.2 交互关系
接下来,我们再来看一下LangGraph状态机和LangGraph持久化模块之间的交互关系:
2.3 学科定位与边界
2.3.1 学科定位
LangGraph状态机持久化,属于**人工智能工程(AI Engineering)**领域的一个子方向,涉及以下几个学科的知识:
- 计算机科学(CS):数据结构、算法、操作系统、文件系统、数据库系统;
- 软件工程(SE):系统设计、架构设计、接口设计、代码实现、测试、部署、维护;
- 人工智能(AI):大语言模型(LLM)、Agent系统、LangChain框架;
- 分布式系统(DS):如果是大型企业级应用,还涉及分布式存储、分布式一致性、高可用性、容错性。
2.3.2 边界
我们需要明确LangGraph状态机持久化的边界,避免把它和其他相关概念混淆:
- 边界1:≠ LangChain记忆模块:正如我们在1.2节所说的,LangChain记忆模块只是状态机的一部分状态变量,而LangGraph持久化记录的是状态机的完整快照;
- 边界2:≠ 大语言模型(LLM)的上下文窗口:LLM的上下文窗口是有限的(比如GPT-4o的上下文窗口是128K token,Claude 3 Opus的上下文窗口是200K token),而LangGraph持久化可以保存无限多的历史轨迹和上下文记忆(只要你的存储足够大);
- 边界3:≠ 工具调用的缓存:工具调用的缓存是用来保存工具的输出,避免重复调用工具的,而LangGraph持久化是用来保存状态机的完整快照的,工具调用的缓存可以作为状态机的一部分状态变量保存到检查点里;
- 边界4:≠ 数据库的事务(Transaction):数据库的事务是用来保证数据库操作的原子性、一致性、隔离性、持久性(ACID)的,而LangGraph持久化的检查点保存可以看作是一个“简化的事务”——它只保证检查点本身的原子性和持久性,不保证节点执行过程中的所有操作(比如工具调用、数据库写入)的原子性(如果需要保证节点执行过程中的所有操作的原子性,你需要自己实现事务机制)。
2.4 思维导图或知识图谱
最后,我们把刚才的核心概念、层次关系、交互关系、学科定位与边界整合起来,画一个完整的知识图谱:
3. 基础理解(建立直观认识)
3.1 核心概念的生活化解释
在2.1节中,我们已经给每个核心概念都做了生活化类比,现在我们再把这些类比串起来,用一个完整的生活化故事来解释LangGraph状态机持久化的整个过程:
3.1.1 生活化故事:小明的“周末家庭作业流水线”
小明是一个小学生,他有一份周末家庭作业:
- 语文作业:写一篇300字的作文《我的周末计划》;
- 数学作业:做10道应用题;
- 英语作业:背20个单词,默写一遍;
- 家长签字:写完所有作业后,让爸爸或妈妈签字。
小明不想一次性写完所有作业,他想写一会儿作业,玩一会儿游戏,但他又怕玩游戏的时候忘记自己写到哪里了,于是他想了一个办法——用一个“作业进度本”来记录自己的作业进度。
现在,我们把这个故事映射到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”,它的状态机有三个节点:
- 入口节点:初始化状态(接收用户的问题);
- LLM节点:调用LLM回答用户的问题;
- 结束节点:返回最终答案。
用户的问题是:“中国的首都是哪里?”,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的检查点存储实现,它使用了三个表来存储检查点的内容:
checkpoints表:存储检查点的核心元数据;checkpoint_blobs表:存储检查点的核心内容(序列化后的二进制数据);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_blobs和checkpoint_writes记录也会被自动删除); - 它使用了索引来提高查询性能(比如
checkpoints_thread_ts_idx索引可以快速查询某个会话的所有检查点,按时间倒序排列); - 它使用了**
更多推荐
所有评论(0)