很多同学在第一次接入 LangGraph 时,会发现图默认是「无状态」的——每次invoke,上一轮的消息就消失了。你以为加了 MessagesState 就有记忆了,结果测试一问,Agent 完全不知道「你叫什么名字」。
更惨的是什么?生产环境跑一个需要 30 步的复杂工作流,第 25 步因为网络超时失败了。重跑?从头开始,前面 24 步白跑,费时又费钱。
这些问题的根源,就是没搞懂 LangGraph 的Checkpoint 持久化机制。这篇我们把它从头拆开来看——Checkpoint 是什么、怎么存的、thread_id 是什么关系、三种存储后端怎么选、断点续跑怎么用。
01 Checkpoint 不是"存消息",是"存图的完整状态"
很多人把 Checkpoint 等同于「保存对话历史」,这是第一个认知偏差。
Checkpoint 保存的是Graph 在某一执行步骤的完整状态快照,包括:
- 所有 Channel(State 里每个字段)的当前值
- 当前执行到哪个节点
- 父检查点 ID(形成版本链)
- 时间戳和元数据
Graph 执行一个 Super-Step 的流程: [读取上一个 Checkpoint] ↓ [执行当前节点,更新 State] ↓ [写入新 Checkpoint(快照)] ↓ [决定下一步:继续 / 等待 / 结束]你可以把它理解成Git 的 commit 历史:每次节点执行,都会产生一个 commit,记录「这一刻图的状态是什么」。出了问题,可以 checkout 到任意一个历史节点重跑,而不是从头来。
Checkpoint 的数据结构
interfaceCheckpointvnumber// 版本号(目前是 4)tsstring// 时间戳 ISO 格式idstring// UUID,唯一标识这个快照channel_values// State 中每个字段的当前值messagesBaseMessagekeystringanychannel_versions// 每个字段的版本号,用于冲突检测channelstringnumberversions_seen// 记录各节点看到的版本,避免重复处理nodestringchannelstringnumberpending_sendsany// 待发送的消息队列注意channel_versions——这不是废字段。LangGraph 用版本号判断「某个节点是否需要重新执行」,这是断点续跑的底层依据。
02 thread_id:多会话隔离的"平行宇宙坐标"
如果说 Checkpoint 是存档文件,thread_id 就是存档槽。
一个 Graph 实例可以服务无数个对话线程,每个线程有独立的 Checkpoint 序列,互不干扰:
同一个 Graph 编译实例 ├── thread_id: "user-001" │ ├── checkpoint-v1 (第1轮对话) │ ├── checkpoint-v2 (第2轮对话) │ └── checkpoint-v3 (第3轮对话) ├── thread_id: "user-002" │ ├── checkpoint-v1 │ └── checkpoint-v2 └── thread_id: "workflow-batch-20240427" ├── checkpoint-v1 └── ...(可随时恢复)调用时,thread_id 通过config传入:
importMemorySaverfrom"@langchain/langgraph"importHumanMessagefrom"@langchain/core/messages"constnewMemorySaverconstcompile// 第一轮对话constawaitinvokemessagesnewHumanMessage"我叫 James,我在学 LangGraph"configurablethread_id"user-001"// 第二轮——Graph 自动加载 user-001 的历史状态constawaitinvokemessagesnewHumanMessage"我之前说我叫什么?"configurablethread_id"user-001"// 同一个 thread_id// 输出:你之前说你叫 Jamesconsolelogmessagesat1content关键点:不传 thread_id,Graph 不会持久化任何状态,就算 compile 时传了 checkpointer 也没用。thread_id 是持久化的「触发器」。
03 三种存储后端:钱、可靠性、复杂度的三角博弈
LangGraph.js 官方提供三种 Checkpointer,选哪个取决于你的场景:
| 存储后端 | 包名 | 重启后保留? | 适合场景 | 成本 |
|---|---|---|---|---|
MemorySaver | 内置 | ❌ 进程重启即丢失 | 开发调试、单元测试 | 零 |
SqliteSaver | @langchain/langgraph-checkpoint-sqlite | ✅ 文件持久化 | 单机部署、小规模生产 | 极低 |
PostgresSaver | @langchain/langgraph-checkpoint-postgres | ✅ 数据库持久化 | 多实例生产、高并发 | 中等 |
MemorySaver:开发必用,生产勿用
importMemorySaverfrom"@langchain/langgraph"constnewMemorySaverconstcompile// 简单,进程活着就有记忆,进程死了全没了SqliteSaver:单机生产的最佳选择
importSqliteSaverfrom"@langchain/langgraph-checkpoint-sqlite"// 同步版本constSqliteSaverfromConnString"./checkpoints.db"// 或者异步版本(推荐)importAsyncSqliteSaverfrom"@langchain/langgraph-checkpoint-sqlite"constawaitAsyncSqliteSaverfromConnString"./checkpoints.db"constcompile// 服务重启后,用同一个 thread_id,历史对话原封不动恢复constawaitinvokemessagesnewHumanMessage"我们之前聊到哪里了?"configurablethread_id"user-001"SQLite 文件会自动建表,schema 由 LangGraph 管理,你不需要手动 migration。
PostgresSaver:多实例水平扩展
importPostgresSaverfrom"@langchain/langgraph-checkpoint-postgres"importfrom"pg"constnewPoolconnectionStringenvDATABASE_URLconstPostgresSaverfromConnStringenvDATABASE_URL// 首次使用必须调用 setup(),创建必要的表awaitsetupconstcompilePostgresSaver 支持连接池,多个服务实例共享同一个状态库,这是 MemorySaver 和 SqliteSaver 做不到的。
04 断点续跑:Checkpoint 最强的能力
对话记忆只是 Checkpoint 的基础用法。更强的是断点续跑——工作流中途失败,从上次成功的节点继续跑,而不是从头来。
场景:30 步工作流,第 25 步超时
importStateGraphAnnotationfrom"@langchain/langgraph"importSqliteSaverfrom"@langchain/langgraph-checkpoint-sqlite"constWorkflowStateAnnotationRootstepAnnotationnumberreducer(_, next) =>resultAnnotationstringreducer(acc, next) =>errorAnnotationstringnullreducer(_, next) =>default() =>null// 工作流 ID 作为 thread_id,确保唯一constWORKFLOW_ID`batch-job-${Date.now()}`// 第一次执行(假设第 25 步超时失败)tryawaitinvokestep0resulterrornullconfigurablethread_idWORKFLOW_IDcatchconsoleerror"执行失败,但已保存 Checkpoint"// 查看当前状态——看到了失败前的最后一个快照constawaitgetStateconfigurablethread_idWORKFLOW_IDconsolelog`失败时执行到步骤:${state.values.step}`// 从断点继续——不传 input,直接 resumeawaitinvokenullconfigurablethread_idWORKFLOW_IDinvoke(null, config)是断点续跑的核心——传null表示「从上一个 Checkpoint 的状态继续执行,不重置状态」。
查看历史快照
// 获取某个 thread 的所有 Checkpoint 历史forawaitconstofgetStateHistoryconfigurablethread_id"user-001"consolelogidconfigconfigurablecheckpoint_idstepmetadatasteptimestampcreatedAtnextNodenext时光机:回滚到历史某一步重跑
// 从历史中找到某个 checkpoint_idconstforawaitconstofgetStateHistoryconfigurablethread_id"user-001"push// 回滚到第 3 步的状态重跑constfinds =>metadatastep3awaitinvokenullconfigurablethread_id"user-001"checkpoint_idconfigconfigurablecheckpoint_id这个能力在调试 Agent 行为时极其有用:找到 Agent 判断出错的那一步,修改状态,重跑后续逻辑,而不是从头走一遍。
05 手动更新 State:在 Checkpoint 之间注入数据
有时候你需要在两次 invoke 之间,手动修改 State——比如 Human-in-the-Loop 审核通过后,注入审核结果:
importHumanMessageAIMessagefrom"@langchain/core/messages"// 方式一:updateState 直接写入awaitupdateStateconfigurablethread_id"user-001"messagesnewAIMessage"(人工审核通过,继续执行)"approvedtrue// 更新后继续执行awaitinvokenullconfigurablethread_id"user-001"// 方式二:as_node 模拟某个节点写入(让 LangGraph 以为是从特定节点产出的数据)awaitupdateStateconfigurablethread_id"user-001"messagesnewHumanMessage"重新注入一条消息""human_review_node"// 第三个参数:以哪个节点的名义写入updateState内部也会创建一个新的 Checkpoint,保留完整的状态链路。
06 常见坑:踩过才知道有多痛
坑1:MemorySaver 用在生产环境,重启血崩
开发测试用 MemorySaver,上生产忘了换。服务一重启,所有用户的对话历史归零。判断标准:只要你的服务会重启(CI/CD、崩溃恢复),就必须用 Sqlite 或 Postgres。
坑2:没有传 thread_id,checkpointer 形同虚设
// ❌ 这样调用,就算 compile 了 checkpointer 也没有持久化效果awaitinvokemessagesnewHumanMessage"hi"// ✅ 必须传 configurable.thread_idawaitinvokemessagesnewHumanMessage"hi"configurablethread_id"some-unique-id"判断方法:调用graph.getState({ configurable: { thread_id: "your-id" } }),看values是否有内容。
坑3:thread_id 复用导致状态污染
多个用户用同一个 thread_id,状态相互覆盖。正确姿势:每个用户/会话用唯一 ID,推荐user_{userId}_session_{sessionId}格式。
坑4:PostgresSaver 忘记调用 setup()
Error: relation "checkpoints" does not existPostgresSaver 第一次使用必须await checkpointer.setup(),这会创建 LangGraph 需要的表结构。线上部署时,把setup()放在服务启动脚本里。
坑5:断点续跑时传了 input 导致状态被重置
// ❌ 想从断点继续,却传了 input,State 会被重置到初始值awaitinvokemessagesstep0// 这会覆盖掉保存的 Checkpoint!configurablethread_idWORKFLOW_ID// ✅ 断点续跑,传 nullawaitinvokenullconfigurablethread_idWORKFLOW_ID坑6:checkpoint 数据无限增长,没有清理策略
每次 invoke 都会写入新 Checkpoint,长期运行的生产环境会撑爆存储。最佳实践:
- SQLite:定期
DELETE FROM checkpoints WHERE thread_ts < ?(保留最近 N 条) - Postgres:用 pg_partman 按时间分表,配合 cron 清理旧分区
总结
Checkpoint 不是「存消息」,而是存整个 Graph 在某一刻的完整状态快照,包括所有 Channel 值、版本号和父子关系。
thread_id 是多会话隔离的坐标,不传 thread_id 等于没有持久化,这是新手最容易踩的坑。
三种存储后端各有边界:开发用 MemorySaver,单机生产用 SqliteSaver,多实例水平扩展用 PostgresSaver,不要混用。
断点续跑的核心是invoke(null, config),传 null 表示从上一个 Checkpoint 继续,传 input 会重置状态——这两者行为完全不同。
updateState+as_node是 Human-in-the-Loop 的底层支撑,让你在 invoke 之间注入数据,并以特定节点的名义写入 Checkpoint。
学AI大模型的正确顺序,千万不要搞错了
🤔2026年AI风口已来!各行各业的AI渗透肉眼可见,超多公司要么转型做AI相关产品,要么高薪挖AI技术人才,机遇直接摆在眼前!
有往AI方向发展,或者本身有后端编程基础的朋友,直接冲AI大模型应用开发转岗超合适!
就算暂时不打算转岗,了解大模型、RAG、Prompt、Agent这些热门概念,能上手做简单项目,也绝对是求职加分王🔋
📝给大家整理了超全最新的AI大模型应用开发学习清单和资料,手把手帮你快速入门!👇👇
学习路线:
✅大模型基础认知—大模型核心原理、发展历程、主流模型(GPT、文心一言等)特点解析
✅核心技术模块—RAG检索增强生成、Prompt工程实战、Agent智能体开发逻辑
✅开发基础能力—Python进阶、API接口调用、大模型开发框架(LangChain等)实操
✅应用场景开发—智能问答系统、企业知识库、AIGC内容生成工具、行业定制化大模型应用
✅项目落地流程—需求拆解、技术选型、模型调优、测试上线、运维迭代
✅面试求职冲刺—岗位JD解析、简历AI项目包装、高频面试题汇总、模拟面经
以上6大模块,看似清晰好上手,实则每个部分都有扎实的核心内容需要吃透!
我把大模型的学习全流程已经整理📚好了!抓住AI时代风口,轻松解锁职业新可能,希望大家都能把握机遇,实现薪资/职业跃迁~