Skip to content
预计 1 分钟右侧目录可快速定位

Run 怎样安全暂停再继续:Pause、Checkpoint 与 Pending Writes ​

前置:Retry / Idempotency / Unknown Outcome;Run Lifecycle State Machine
核心问题:进程退出后,Run 为什么还能从正确的位置继续,而不会丢状态或重复已经完成的副作用?

审批可以跨小时:服务释放 worker,保存 pending approval;批准事件到达后新进程从 checkpoint 继续。每个 graph super-step 后 checkpoint,能在 crash 后从最近完成批次继续,而不是重跑全部历史。

Pause / Resume ​

Pause 不是让 Python 协程一直挂着,而是把当前运行转换成一个可以释放进程资源、以后再恢复的 durable state。

例如等待人工审批:

text
RUNNING
→ Tool 需要 approval
→ 提交 checkpoint + approval_request
→ Run = WAITING_APPROVAL
→ Worker 释放

两小时后:
approval event
→ 新 Worker load checkpoint
→ Run = RUNNING
→ 从下一安全边界继续

所以 Resume 的关键不是“重新调用 run()”,而是恢复时能判断哪些结果已经提交、哪些动作尚未发生,避免把已完成副作用再做一次。

Checkpoint Boundary ​

Checkpoint 是一个内部状态彼此一致、可以安全恢复的切面,不是随便把当前对象序列化一下。

如果 Node A 已写入结果,但 Scheduler 的 completed_nodes 还没更新,就不能把这两个不同版本一起保存成 checkpoint。恢复后 Runtime 会得到一个自相矛盾的世界。

边界越细,恢复时重复工作越少,但写放大越高;边界越粗,存储便宜,但 crash 后需要重新计算更多内容。选择粒度时要同时考虑恢复成本和外部副作用风险。

🔍 源码对照:Checkpoint 是 prepare → persist → commit 的恢复边界 ​

精确定位

ts
async function persistCheckpoint(ctx, checkpoint, finalize) {
  await transcriptMirror?.prepareCheckpoint(ctx, checkpoint, finalize)
  try {
    await agentStore.handleCheckpoint(ctx, checkpoint)
  } catch (e) {
    await transcriptMirror?.abortCheckpoint(ctx)
    throw e
  }
  await transcriptMirror?.commitCheckpoint(ctx)
}

对照理解

  • 这里不是简单 save(checkpoint):先 prepare transcript mirror,再提交 agent state;主存储失败时 abort 已准备的 mirror。
  • 这体现了 checkpoint 是一致恢复边界,而不是定时序列化 Python 对象。
  • 如果主 checkpoint 已持久化但 transcript commit 失败,源码专门抛 TranscriptAppendAfterCheckpointError,说明“两个事实之间的 crash window”必须显式建模。

源码说明

这个案例有 conversation state、durable file policy、storage、crash guard 多条持久化相关边界,恰好适合反向检查本文的核心问题:Checkpoint 不是“把内存对象 pickle 一下”,而是要决定哪些事实必须在 pause/crash 后仍然成立。阅读源码时不要追求找到一个名为 CheckpointStore 的类,而要找 durable fact 的写入边界。

工程边界

从 reconstruction 看到的持久化材料不能直接证明它实现了通用 exactly-once resume;尤其外部 side effect 仍需独立 execution record / reconciliation。

Shadow Harness 对照

  • Checkpoint 只保存恢复所需的 authoritative state + version,不把临时 task/future 当 durable state。
  • WAITING_APPROVAL 等 pause reason 与 pending side-effect metadata 必须可恢复。

验证

在 tool side effect success 与 checkpoint commit 之间注入 crash;恢复时必须识别 outcome unknown,而不是无脑重新执行。

Pending Writes ​

Pending writes 记录同一并行 step 中已成功子任务的 durable output,即使整个 step 因 sibling 失败尚未形成完整 checkpoint。恢复时据此避免重跑成功节点,LangGraph 当前 persistence 就使用这一思想。

要确保这些写与执行记录有稳定 identity。

一个 Approval Pause 的完整状态快照 ​

假设并行 step 中:search_logs 已成功,prepare_patch 已生成 patch,apply_patch 需要人工批准。此时 Runtime 不应该让 worker 挂几个小时,而是保存类似:

json
{
  "run_id": "r-42",
  "state_version": 17,
  "status": "WAITING_APPROVAL",
  "committed": ["search_logs"],
  "pending_writes": {
    "prepare_patch": "artifact://patches/p-9"
  },
  "waiting": {
    "type": "approval",
    "node_id": "apply_patch",
    "approval_id": "a-7"
  }
}

然后释放 worker。两小时后 approval event 到达,新进程读取 checkpoint,把 prepare_patch 的 durable output 视为已经存在,只继续 apply_patch。

这里最容易出错的是 crash window:Tool 已经成功,但结果还没进入 checkpoint。如果 Tool 有外部副作用,resume 不能仅凭“checkpoint 里没有成功记录”就盲目重试;还需要 execution identity、pending write 或 reconciliation 来判断真实世界是否已经发生过动作。Checkpoint 解决“从哪里恢复”,并不自动解决“外部动作是否恰好执行过一次”。

Failure Modes ​

Checkpoint 没有形成一致恢复边界 ​

症状: snapshot 已写但 node/tool result 没写,或反过来。

为什么危险: resume 不知道哪一步已提交,可能重复副作用或跳过工作。

处理: checkpoint 与必要 execution record/pending writes 在明确一致边界提交;resume 只调度未提交部分。

如何验证 ​

Case断言
Checkpoint 没有形成一致恢复边界在不同提交点注入 crash,恢复后 logical node 已提交副作用最多一次。

关联源码 ​

自测 ​

  1. Tool 已成功产生副作用,但进程在写 checkpoint 前 crash。恢复时为什么既不能假设没执行,也不能直接重放?
  2. Checkpoint 必须和哪些 pending writes / execution identity 一起形成一致恢复边界,才能避免丢状态或重复副作用?

Sources ​

AI Engineering · Agent · Harness · Backend Systems