持久化执行
让长时间运行的智能体在关键时刻保存进度,崩了也能从断点精准恢复
检查点 + 回放 + task() —— 从"一次跑完"到"随时中断、随时继续"跑了半天崩了
你的 Agent 跑到一半,LLM 超时了 / 网络断了 / 服务器重启了 —— 然后呢?
没有持久化执行
- 调了 3 分钟 LLM,网络闪断,全部白跑
- 执行到第 5 步崩了,只能从头再来,前 4 步的 API 调用费用打水漂
- 想暂停等人确认?可以,但恢复后不确定从哪里继续
- 节点的副作用(发邮件、写数据库)可能重复执行
- 长时间运行的任务不敢用 Agent,怕中途挂掉无法恢复
有持久化执行
- LLM 超时?用同一个
thread_id重新调用,自动从断点恢复 - 前 4 步的结果已存检查点,只重跑第 5 步
interrupt()暂停 +Command({ resume })精准继续task()包裹副作用,重放时自动跳过已执行的- 长时间任务也能放心交给 Agent,崩了就重跑,不用怕
task()。
开始之前
需要具备
- 已完成第 05 章持久化,理解检查点、线程、超级步骤
- 能使用
MemorySaver编译带持久化的图 - 理解
getState()/updateState()的基本用法 - 熟悉 TypeScript 的 async/await
读完后你应该能
- 说清"回放"机制的工作原理和它带来的约束
- 用
task()包裹副作用,避免重复执行 - 根据场景选择
exit/async/sync三种持久化模式 - 在崩了之后用相同
thread_id恢复执行 - 写出符合持久化执行约束的安全代码
暂不涉及
- 自定义 Checkpointer 后端的实现细节
- 分布式场景下的一致性保证
- 时间旅行的完整用法(后续章节)
- 生产环境部署与监控
什么是持久化
一句话:在关键节点保存进度,暂停后能从断点精准恢复
持久化执行(Durable Execution)不是什么新概念——数据库事务、消息队列都有类似的"至少一次"语义。在 LangGraph 中,它的含义很简单:

图:持久化执行的三个层次
【AI 生图提示词】请生成一张中文技术概念图,风格简洁、配色柔和、适合教学: - 画面主体:一条横向流程,从左到右展示"保存进度 → 暂停 → 恢复 → 继续执行"四个阶段 - 每个阶段用圆角矩形表示,颜色依次为绿色(保存)、黄色(暂停)、蓝色(恢复)、绿色(继续) - 在"暂停"和"恢复"之间画一条虚线,标注"可能间隔数秒、数分钟甚至数天" - 下方用文字说明三大应用场景:人机交互(HITL)、LLM 超时恢复、网络中断恢复 - 各元素关系:四个阶段是线性串联关系,暂停到恢复之间是时间间隔(虚线表示不确定的等待期) - 关键标注:每个阶段矩形内标注阶段名称,暂停与恢复之间的虚线标注"checkpointer 自动保存/恢复" - 配色建议:浅米色背景,矩形用不同颜色区分阶段,箭头用深灰色 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色
适用场景
- 人机交互(HITL):暂停等人确认,确认后继续
- LLM 超时:大模型响应慢,超时后重试
- 网络中断:断网后重连,从断点恢复
- 长时间任务:多步骤 Agent,中途可能需要数小时
核心前提
- 你已经用了
checkpointer—— 这是一切的基础 - 每次调用都传了
thread_id - 理解回放(replay)机制 —— 恢复时从头重放
- 副作用用
task()包裹
task() 保护副作用。
三个前提
两个启用条件 + 一个编码规范
1. 启用持久化 (启用条件)
编译时传入 checkpointer
// MemorySaver 用于开发
const graph = workflow.compile({
checkpointer: new MemorySaver()
});
2. 指定线程 (启用条件)
每次调用传 thread_id
// 同一任务用相同 thread_id
const config = {
configurable: {
thread_id: "task-001"
}
};
3. 包裹副作用 (编码规范,非启用所必需)
用 task() 包装非确定性操作
import { task } from "@langchain/langgraph";
const result = await task(
"my-api-call",
async () => fetch(url)
);
checkpointer + thread_id(前两项)就能工作;task() 不是启用持久化的前提,而是避免重放时副作用被重复执行的最佳实践。另外,task() 属于 Functional API——它通常配合 entrypoint 一起使用(import { task, entrypoint } from "@langchain/langgraph"),也可以在 StateGraph 节点内直接调用,本教程示例即采用后者。
// 完整示例:满足三个前提的持久化执行图
import { StateGraph, MemorySaver, START, END, task } from "@langchain/langgraph";
import { Annotation } from "@langchain/langgraph";
const State = Annotation.Root({
url: Annotation<string>,
result: Annotation<string>,
});
const fetchNode = async (state) => {
// 用 task() 包裹网络请求——重放时自动跳过
const html = await task("fetch-url", async () => {
const resp = await fetch(state.url);
return await resp.text();
});
return { result: html.slice(0, 100) };
};
const graph = new StateGraph(State)
.addNode("fetch", fetchNode)
.addEdge(START, "fetch")
.addEdge("fetch", END)
.compile({ checkpointer: new MemorySaver() }); // 前提 1
const config = { configurable: { thread_id: "crawl-1" } }; // 前提 2
task() 能让 LangGraph 记住"这个操作已经做过了",重放时跳过。
回放机制
最重要的概念:恢复时从头重放,不是从断点继续

图:回放机制 —— 恢复时的执行路径
【AI 生图提示词】请生成一张中文技术流程图,风格简洁、配色柔和、适合教学: - 画面分上下两行: - 上行"原始执行":START → A → B → C(崩溃!) ✕ → D(未执行) - 下行"恢复执行":A(重放/跳过) → B(重放/跳过) → C(重新执行) → D(正常执行) - 上行的 A、B 用橙色,C 用红色带叉号,D 用灰色虚线 - 下行的 A、B 用蓝色半透明(表示重放但跳过副作用),C 用橙色加粗(表示真正执行),D 用橙色 - 中间用向下箭头连接两行,标注"用相同 thread_id 重新调用" - 各元素关系:上下两行是时间先后关系——先崩溃,后恢复。下行中 A、B 是重放(跳过副作用),C 是真正重新执行,D 是首次执行 - 关键标注:上行标题"第一次执行(崩溃)",下行标题"恢复执行",C 节点标注"断点恢复从这里开始",A/B 标注"task() 包裹的副作用被跳过" - 配色建议:浅米色背景,正常执行用橙色,崩溃用红色,重放用蓝色,强调恢复点 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色
task() 包裹的操作在重放时会被跳过——这就是为什么要用它包裹副作用。
重放时发生了什么
- 代码从图起点开始重新执行
- 已完成的节点:通过检查点快速重放
task()包裹的操作:自动跳过- 崩溃的节点:重新执行
如果没有 task() 会怎样
- 节点 A 发了一封邮件,重放时再发一封
- 节点 B 写了一条数据库记录,重放时再写一条
- 副作用不受控制,可能造成严重后果
编码原则
理解了回放机制后,写代码时要遵循三条原则
原则一:避免重复工作
所有有副作用的操作(API 调用、数据库写入、发邮件)必须用 task() 包裹。这样重放时 LangGraph 知道跳过它们。
// 错误:直接调用
const data = await fetch(url);
// 正确:用 task() 包裹
const data = await task("fetch-data",
async () => await fetch(url)
);
原则二:包裹非确定性
随机数、时间戳、UUID 等非确定性操作也要用 task() 包裹。否则每次重放都会生成不同的值,导致状态不一致。
// 错误:每次重放结果不同
const id = crypto.randomUUID();
// 正确:重放时使用缓存的结果
const id = await task("gen-id",
() => crypto.randomUUID()
);
原则三:幂等操作
即使用了 task(),也无法 100% 保证不重复。设计操作时尽量让它执行多次和执行一次效果相同。
// 使用幂等写入:upsert 而非 insert
await task("save-result",
async () => db.collection.updateOne(
{ _id: taskId }, // 唯一键
{ $set: { result } },
{ upsert: true }
)
);
task()(防止重放重复) + 幂等设计(防止极端情况重复) + checkpointer(保证状态一致)。三者配合才是完整的持久化执行。
持久化模式
三种模式控制检查点的保存时机和频率
| 模式 | 保存时机 | 安全性 | 性能 | 适用场景 |
|---|---|---|---|---|
| exit | 仅在图执行完毕时保存 | 低 | 最快 | 短任务、不需要中途恢复 |
| async | 异步持久化(默认模式) | 中 | 较块 | 大多数场景的默认选择 |
| sync | 每步同步保存检查点 | 最高 | 最慢 | 关键任务、不能丢失任何状态 |
// 三种模式的使用方式——只在调用时指定
const config = { configurable: { thread_id: "task-1" } };
// exit 模式:只在图结束时保存
await graph.stream({ input: "test" }, { ...config, durability: "exit" });
// async 模式:异步保存(默认)
await graph.stream({ input: "test" }, { ...config, durability: "async" });
// sync 模式:每步同步保存——最安全
await graph.stream({ input: "test" }, { ...config, durability: "sync" });
如何选择
- 开发调试:用
exit,跑得快,不用管中途状态 - 一般场景:用
async(默认),平衡安全和性能 - 金融 / 医疗 / 关键业务:用
sync,每一步都确认保存
性能影响
sync每步都要等写入完成,速度最慢但最安全async在后台写入,几乎不影响执行速度exit只在结束时写一次,中途崩溃会丢失中间状态
durability,默认值取决于你使用的 checkpointer。大多数情况下默认值就够用。只有在明确需要更强保证时才切换到 sync。
task() 详解
用 task() 包裹副作用,让重放时自动跳过已执行的操作
// task() 的签名
import { task } from "@langchain/langgraph";
// 第一个参数:唯一名称(同一个节点内不能重复)
// 第二个参数:实际要执行的异步函数
const result = await task("fetch-url", async () => {
const response = await fetch(state.url);
return await response.text();
});
在节点中使用 task()
import { task } from "@langchain/langgraph";
const researchNode = async (state) => {
// 任务 1:获取网页内容(副作用 → 用 task)
const html = await task("fetch-page", async () => {
const resp = await fetch(state.url);
return resp.text();
});
// 任务 2:调用 LLM 分析(副作用 + 花钱 → 用 task)
const analysis = await task("analyze", async () => {
const llm = new ChatOpenAI({ model: "gpt-4o-mini" });
const result = await llm.invoke(html.slice(0, 3000));
return result.content;
});
// 纯计算不需要 task()——没有副作用,重放也无害
const summary = analysis.slice(0, 200);
return { result: summary };
};
const graph = new StateGraph(State)
.addNode("research", researchNode)
.addEdge(START, "research")
.addEdge("research", END)
.compile({ checkpointer: new MemorySaver() });
什么时候用 task()
- 网络请求(fetch、API 调用)
- 数据库读写
- 文件系统操作
- 发送邮件 / 通知
- 调用 LLM(花钱的操作)
- 生成随机值(UUID、时间戳)
什么时候不需要
- 纯计算(字符串处理、数学运算)
- 状态读取(
state.xxx) - 条件判断(if/else)
- 数据转换(map、filter)
console.log(重放时重复打印无害)
task()。task 的名称在同一个节点内必须唯一。
恢复场景
两种核心恢复方式:暂停/恢复 和 故障恢复
| 场景 | 触发方式 | 恢复方式 | 典型用途 |
|---|---|---|---|
| 暂停/恢复 | 节点内调用 interrupt() |
graph.invoke(null, { ...config, command: new Command({ resume: value }) }) |
人机交互、等待审批 |
| 故障恢复 | LLM 超时、网络中断、进程崩溃 | 用同一个 thread_id 重新调用 invoke / stream |
自动重试、服务重启 |
| 状态回滚 | 手动调用 updateState |
修改检查点后重新执行 | 修正错误、调试 |
场景一:暂停与恢复(HITL)
import { interrupt, Command } from "@langchain/langgraph";
const reviewNode = async (state) => {
// 暂停,等待人工审核
const humanDecision = interrupt("请审核这个结果:" + state.draft);
// humanDecision 就是恢复时传入的值
if (humanDecision === "approve") {
return { status: "approved" };
}
return { status: "rejected", feedback: humanDecision };
};
// 恢复执行——传入人工决策
await graph.invoke(
null, // 不需要新输入
{
...config,
command: new Command({ resume: "approve" }),
}
);
场景二:故障恢复
// 第一次调用——假设在节点 C 崩溃了
try {
await graph.invoke({ input: "start" }, config);
} catch (e) {
console.log("崩溃了,准备恢复...");
}
// 恢复:用同一个 thread_id 重新调用,自动从断点恢复
const result = await graph.invoke(
null, // 不需要新输入,检查点已有状态
config // 同一个 thread_id
);
console.log(result); // 从崩溃点继续执行
thread_id。区别在于:暂停/恢复需要传 Command({ resume });故障恢复只需重新调用即可。
排错指南
持久化执行中的常见问题与修复方案
副作用重复执行
现象:恢复后发现邮件发了两次、数据库写了两条。
原因:忘记用 task() 包裹副作用操作。
修复:所有 API 调用、数据库写入、发邮件等操作都用 task("name", fn) 包裹。
task 名称冲突
现象:报错 "Duplicate task name"。
原因:同一个节点内有两个 task() 用了相同的名称。
修复:确保每个 task 的第一个参数在同一个节点内唯一,例如 "fetch-url-1"、"fetch-url-2"。
恢复后状态不对
现象:恢复后得到的状态和预期不一致。
原因:用了不同的 thread_id,或者恢复时传了新的输入。
修复:恢复时用同一个 thread_id,且传 null 作为输入(除非确实需要新输入)。
恢复后直接结束
现象:调用恢复后,图直接返回了结果,没有继续执行。
原因:图在崩溃前已经执行完毕(到达 END),next 为空数组。
修复:用 getState() 检查 next 字段,确认是否还有未执行的节点。
随机值不一致
现象:恢复后 UUID 或时间戳变了,导致下游逻辑异常。
原因:生成随机值的代码没有用 task() 包裹。
修复:把随机值生成也包在 task() 里,重放时使用缓存值。
sync 模式太慢
现象:图执行速度明显变慢。
原因:用了 durability: "sync",每步都等检查点写入完成。
修复:大多数场景用 "async"(默认)就够了,只在关键业务用 "sync"。
动手练习
构建一个带 task() 和故障恢复的可靠 Agent
练习 1:基础 task()
- 构建一个三节点图:
fetch→analyze→report - 在
fetch中用task()包裹 HTTP 请求 - 在
analyze中用task()包裹 LLM 调用 - 故意让
report抛异常,观察恢复行为
练习 2:暂停与恢复
- 在
analyze节点中加interrupt() - 调用图,观察暂停行为
- 用
getState()查看next字段 - 用
Command({ resume })恢复执行
练习 3:三种模式对比
- 分别用
exit、async、sync运行同一个图 - 在每个节点加
console.time对比耗时 - 中途终止,观察恢复后状态差异
// 参考答案:练习 1 & 2
import { StateGraph, MemorySaver, START, END, task, interrupt, Command } from "@langchain/langgraph";
import { Annotation } from "@langchain/langgraph";
const State = Annotation.Root({
url: Annotation<string>,
content: Annotation<string>,
analysis: Annotation<string>,
report: Annotation<string>,
});
const fetchNode = async (state) => {
const html = await task("fetch-page", async () => {
const resp = await fetch(state.url);
return resp.text();
});
return { content: html.slice(0, 500) };
};
const analyzeNode = async (state) => {
// 等待人工确认分析内容
const approved = interrupt(`内容预览:${state.content.slice(0, 100)}...确认分析?`);
const result = await task("llm-analyze", async () => {
// 模拟 LLM 调用
return `分析结果:${state.content.length} 字符`;
});
return { analysis: result };
};
const reportNode = (state) => {
return { report: `报告:${state.analysis}` };
};
const graph = new StateGraph(State)
.addNode("fetch", fetchNode)
.addNode("analyze", analyzeNode)
.addNode("report", reportNode)
.addEdge(START, "fetch")
.addEdge("fetch", "analyze")
.addEdge("analyze", "report")
.addEdge("report", END)
.compile({ checkpointer: new MemorySaver() });
const config = { configurable: { thread_id: "exercise-1" } };
// 第一次调用——会在 analyze 节点暂停
await graph.invoke({ url: "https://example.com" }, config);
// 查看暂停状态
const snap = await graph.getState(config);
console.log("下一步:", snap.next); // ["analyze"]
// 恢复——传入确认
await graph.invoke(null, { ...config, command: new Command({ resume: "yes" }) });
本章回顾
持久化执行让长时间运行的智能体变得可靠
核心要点
- 持久化执行:保存进度 + 暂停 + 恢复 + 继续
- 三个前提:checkpointer + thread_id + task()
- 回放机制:恢复时从头重放,已完成的节点快进跳过
- task():包裹副作用,重放时自动跳过已执行的操作
- 三种模式:exit(最快)、async(默认)、sync(最安全)
- 两条原则:副作用用 task()、操作尽量幂等
关键 API 速查
| 操作 | 方法 |
|---|---|
| 包裹副作用 | task("name", fn) |
| 暂停执行 | interrupt("message") |
| 恢复执行 | Command({ resume: value }) |
| 故障恢复 | 同 thread_id 重新调用 |
| 指定模式 | { durability: "sync" } |
| 检查状态 | getState() |
延伸阅读
继续深入持久化执行,或探索 LangGraph 的其他核心能力
官方文档
- Durable Execution 概念 — 持久化执行的完整说明
- Functional API 实践指南 — task() / entrypoint() 用法与最佳实践
- LangGraph.js 官方文档 — 全部 API 参考
进阶主题
- 人机交互(HITL) — interrupt + resume 的完整模式
- 时间旅行 — 回放和分叉过去的检查点
- 生产部署 — PostgresSaver + sync 模式的最佳实践
- 容错设计 — 结合 task() + 幂等 + 重试策略