一句话价值:前三篇的图都在"一次调用"里活着,跑完就消失。
checkpointer让图每走一步都把状态存成快照——于是"记住上一轮对话""转账停下来等人审批""崩溃后接着跑"三件看似不同的事,其实是同一个机制。
前几篇藏着的共同缺陷
前三篇的图都能跑、能分支、能循环。但它们有个你没注意的前提:每次 invoke 都是一次"全新"的执行——状态从 default 起步,跑完就丢。
可真实的产品需要状态跨次调用地活着:
- 多轮客服:第二句话要"记住"第一句话说了什么;
- 转账:执行前要停下来等用户确认"对,转 300 给张三",确认才继续;
- 长任务:中途进程崩了,恢复后要接着上次跑,而不是从头再来。
这三件事,LangGraph 用一个参数解决:compile({ checkpointer })。
checkpointer:每步存快照,不是只存结果
加上 checkpointer 后,图每执行完一个节点,就把当时的完整状态存一份快照。这跟"只存最终返回值"是两回事——正是每一步都在,才能"从任意中间点接着跑"。
最直观的验证,是让节点给一个计数 +1,然后对同一会话连续调用两次:
const app = graph.compile({ checkpointer: new MemorySaver() })
// 同一个会话(thread_id 相同),第二次传空对象也能拿到上次的 1
await app.invoke({}, { configurable: { thread_id: '用户-小张' } })
// → { visitCount: 1, message: '第 1 次进入' }
await app.invoke({}, { configurable: { thread_id: '用户-小张' } })
// → { visitCount: 2, ... } ← 关键!没传 visitCount,却是 2
// 换一个会话(thread_id 不同),从 default 重新开始,互不污染
await app.invoke({}, { configurable: { thread_id: '用户-小李' } })
// → { visitCount: 1, ... }
第二个结果为什么是 2?因为图不是从 default: () => 0 起步,而是从检查点读出了上次的 1。第三次为 1 则证明 thread_id 是会话的隔离键——小张的历史绝不会串到小李头上。
顺带纠一个易错点:
default只在"没有任何检查点"时生效。有历史就从历史恢复,没有才回退到default。写 reducer 时以为 default 每次都会跑,是常见误解。
MemorySaver → SqliteSaver:只换一行,记忆就跨进程了
MemorySaver 把快照存在进程内存里——重启就丢,只适合开发调试。换成 SqliteSaver,快照落磁盘文件,就能跨进程、跨重启地活着:
import { MemorySaver } from '@langchain/langgraph'
import { SqliteSaver } from '@langchain/langgraph-checkpoint-sqlite'
// 进程内:重启即忘
const app = graph.compile({ checkpointer: new MemorySaver() })
// 磁盘上:状态写进 checkpoints.sqlite,进程退出也在
const checkpointer = SqliteSaver.fromConnString('./checkpoints.sqlite')
const app = graph.compile({ checkpointer })
节点、边、状态声明一行都不用改,只换 checkpointer 实例,持久化后端就换了——以后再换 Postgres,同理只改这一行。这正是 LangGraph 抽象干净的地方:持久化能力是"插拔"的,不是散落在代码里。
工程上顺带两个提醒(都是踩过的坑):
- SQLite 数据库是运行时产物,要加进
.gitignore(含 WAL 模式的-shm/-wal副产物),别提交进仓库。 @langchain/langgraph-checkpoint-sqlite依赖原生模块better-sqlite3,pnpm 11 默认禁止依赖执行构建脚本,需在pnpm-workspace.yaml的allowBuilds里放行并rebuild一次——装任何带原生编译的包都会遇到。
interrupt:让图"停下来等一个人"
checkpointer 最有价值的应用,是把"执行到一半"的图暂停,交给人类审批,再原地续跑。核心是两个原语:
import { interrupt, Command } from '@langchain/langgraph'
const showTransfer = () => ({ actionSummary: '向张三转账 $100' })
const waitConfirm = (state) => {
const text = interrupt({
hint: '请在终端输入[确认]或备注后回车', // 暂停时抛给"人"的待办卡
actionSummary: state.actionSummary,
})
return { userInput: String(text) } // ← 第一次执行时这行不会跑
}
// 第一次 invoke:图跑到 interrupt() 就"停住"返回,控制权还给调用方
const paused = await app.invoke({}, config)
console.log(paused.__interrupt__?.[0]?.value)
// → { hint: '...', actionSummary: '向张三转账 $100' } 待你确认
// ……人看完待办卡,输入裁决……
// 第二次 invoke:用 Command({ resume }) 续跑,从 waitConfirm 的断点接着执行
const done = await app.invoke(new Command({ resume: '同意' }), config)
// → { actionSummary: '向张三转账 $100', userInput: '同意' }
理解的关键在 interrupt() 的"双重身份":
- 第一次执行到这一行:函数被掐住,后面的
return不执行,当前状态已被 checkpointer 存好,图暂停返回; - 第二次恢复时:这一行像一个普通函数调用一样,返回
Command({ resume })传进去的值,然后函数继续往下走完。
同一行代码,两段生命周期——暂停点就是恢复的接缝。暂停时塞给 interrupt 的参数会出现在返回值 __interrupt__ 里,那是给外部系统/人看的"待办卡"。
从"进程内暂停"到"跨请求审批"
上面用的 MemorySaver 有局限:暂停状态在内存里,而 paused 是当前进程的变量——同一进程还活着才能 resume。
真实产品是跨请求的:HTTP 请求① 跑到 interrupt 返回暂停(状态已持久化到 SQLite)→ 请求结束、进程可能被回收 → 用户审批后 HTTP 请求② 拿着同一个 thread_id 来 Command({ resume }) 续跑。
做到这一点只需:把 MemorySaver 换成 SqliteSaver,并改用 await app.getState(config) 从检查点读取当前暂停的 __interrupt__(而不是依赖内存里的 paused 变量),这样任何进程都能查到"这笔转账正停在待确认"。
另外 LangGraph 还提供编译期开关——compile({ checkpointer, interruptBefore: ['waitConfirm'] }) 让你不用在节点里写 interrupt(),而是指定"在某个节点执行前自动暂停"。手动 interrupt() 更灵活(能带待办卡、能停在节点中途);interruptBefore 更省事。两者底层是同一套快照恢复。
收束:一个机制,三种能力
现在把系列第一篇的心智模型闭环:
图 = 可暂停、可恢复、可审计的数据流程序。
checkpointer每步存快照,而"从快照继续"这一个动作,分别表现为:多轮对话的记忆(接着上次状态跑)、转账审批的人工介入(停住等人,批完续跑)、崩溃后的断点续跑(从最近的检查点恢复,不从头再来)。
它们不是三个功能,是**"每步快照 + 可恢复"的三种用法**。这就是为什么持久化在 LangGraph 里是地基级能力,而不是某个可选插件。
系列小结与下一步
至此,四篇串起一条完整的 LangGraph 主线:
- 为什么需要图:链表达不了分支/循环/暂停;图 = 可恢复的数据流程序;
- State 怎么不写乱:节点发增量、reducer 决定怎么合、超步归约让并行安全;
- 怎么让状态驱动流程:条件边把"下一跳"交给状态,配 LLM 分类器与自环,就能表达工具循环;
- 怎么让状态活过单次调用:一个 checkpointer,解锁记忆、人工介入、断点续跑。
往前,自然延伸的方向:让 LLM 真正调用工具(ReAct 自环,官方预建的 createReactAgent 就是现成封装)、多 Agent 协作(supervisor / 子图只是"图里多一个节点",同一套运行时全复用)、以及 streaming 与 time-travel。到了那一步你会发现——图的原语你都已经会了,剩下的只是把它们叠成更大的图。