AI Agent 断点续跑设计:事件日志、检查点与副作用幂等
一个内容 Agent 已经完成检索、核验、写作和两张配图,最后上传封面时进程崩了。页面上只有一个“重试”按钮。
如果重试等于从第一步重新执行,问题就来了:搜索结果可能变化,事实表可能产生新版本,图片会重复计费,更危险的是,某个已经成功的外部发布动作可能被执行第二次。
所以,retry() 不是恢复策略。真正的断点续跑至少需要三层状态:追加式事件日志、经过验收的阶段快照,以及不可重复副作用的幂等边界。
Google 在 2026 年 5 月公开 Agent Executor 时,直接指出长时间 Agent 工作流脆弱、难以可靠管理,并把 event log、snapshot 与 resumption 作为运行时能力。它的开源仓库同时明确警告项目仍在早期开发,协议还会变化。因此本文不把 AX 当成现成答案,而是提炼一套小团队可以自己实现的最小协议。
一、先区分三种“状态”
很多实现只保存消息数组:
[
{"role":"user","content":"发布这篇文章"},
{"role":"assistant","content":"已经开始处理"}
]
它能恢复对话,却不能回答:哪张图片上传成功了?草稿 ID 是多少?最后一次发布请求有没有真正到达平台?
建议把状态拆成三类:
| 状态 | 作用 | 是否能直接重放 |
|---|---|---|
| Conversation | 保留意图和解释 | 可以作为上下文,但不能当事实凭证 |
| Execution Event | 记录工具调用、结果和状态迁移 | 只追加,不覆盖 |
| Artifact Snapshot | 保存已验收的状态与产物引用 | 恢复的起点 |

事件可以用一份简单结构表示:
type RunEvent = {
runId: string;
seq: number;
stage: "research" | "draft" | "visual" | "prefill" | "publish";
type: "STARTED" | "TOOL_CALLED" | "TOOL_RESULT" | "VERIFIED" | "FAILED";
inputVersion: string;
payloadRef?: string;
artifactRefs?: string[];
idempotencyKey?: string;
occurredAt: string;
};
关键不是字段多,而是 seq 单调、事件只追加、重要输入有版本、外部结果有引用。
二、检查点不是定时保存,而是验收通过的边界
每隔 30 秒保存一次内存,并不等于有可用检查点。进程可能恰好在半写入、半上传或外部请求未确认时被快照。
更稳妥的检查点有两个条件:
- 当前阶段的输出可独立验证;
- 恢复后不需要猜测上一阶段是否完成。
例如,一篇文章的检查点可以是:
checkpointId: run-9269:visual-verified
runId: run-9269
lastSeq: 48
stage: visual
inputs:
facts: artifact://facts/v3
draft: artifact://draft/csdn-v2
artifacts:
- uri: artifact://images/cover.jpg
sha256: 7c1d...
- uri: artifact://images/body-1.jpg
sha256: 2b4a...
verifiedBy:
- rule: image.count == 3
- rule: image.watermark == false
nextStage: prefill
这份清单说明“配图阶段已完成并通过检查”,而不是仅仅记录“Agent 说图做好了”。
三、恢复流程应该先重放事实,再唤醒模型
恢复时不要先把旧对话塞给模型,问它“你做到哪一步了”。模型的回答不是事务日志。
推荐顺序:
load run
-> scan event log
-> find latest VERIFIED checkpoint
-> verify referenced artifacts still exist
-> reconcile external side effects
-> acquire single-writer lease
-> resume next stage
伪代码如下:
async function resume(runId: string) {
const events = await eventStore.scan(runId);
const checkpoint = latestVerifiedCheckpoint(events);
await assertArtifacts(checkpoint.artifacts);
await reconcileEffects(events);
return lease.withLock(runId, async () => {
return executeFrom(checkpoint.nextStage, checkpoint);
});
}
single-writer lease 很重要。恢复任务和原任务可能同时醒来;如果两边都继续执行,就会产生双写。Google AX 的公开仓库也把 single-writer architecture 列为一致性能力之一。
四、把纯计算和外部副作用分开
生成摘要失败了,可以安全重跑。发布文章、支付费用、发送邮件、删除文件则不同。
可以给每个动作声明重放策略:
type ReplayPolicy = "SAFE" | "IDEMPOTENT" | "RECONCILE_FIRST" | "NEVER";
const policy = {
generateDraft: "SAFE",
renderImage: "IDEMPOTENT",
uploadAsset: "IDEMPOTENT",
publishArticle: "RECONCILE_FIRST",
chargeCard: "NEVER"
} satisfies Record<string, ReplayPolicy>;
对 RECONCILE_FIRST 动作,恢复器必须先查询外部系统:如果文章 ID 已存在且标题、内容哈希匹配,就记录成功并继续,不能再次点击发布。

一个发布请求可以携带稳定幂等键:
deliveryKey = sha256(accountId + platform + canonicalDraftId + revision)
幂等键的作用域必须包含账号和平台,否则不同账号可能误判为同一次投递;revision 变化后也应该生成新键。
五、恢复后不要悄悄改变输入
最隐蔽的错误不是重复执行,而是“恢复后换了资料”。例如研究阶段使用 facts-v3,故障恢复时系统自动拿到 facts-v4,却继续沿用旧验收结果。
恢复协议必须二选一:
- 严格恢复:继续使用检查点绑定的输入版本;
- 显式升级:创建新 run 或新分支,重新验证受影响阶段。
不要把输入升级藏在重试按钮后面。
六、最小落地版本
小团队不需要先造分布式运行时。可以从四张表开始:
runs(run_id, status, current_stage, version)
events(run_id, seq, type, payload_ref, created_at)
checkpoints(checkpoint_id, run_id, last_seq, manifest_ref)
effects(delivery_key, target, status, external_id, updated_at)
再加四条纪律:
- 状态只通过追加事件推进;
- 阶段验收通过后才能写检查点;
- 高风险工具调用前先写 effect intent,调用后回写 external ID;
- 恢复前先做 reconcile,不允许盲目重放。
这也是我们做 Tipkay 定时任务和多岗位接力时很在意的边界。面向一人公司和小团队的 AI 员工,不应该只会生成一次答案;研究、文案、配图、排版和发布准备之间的产物要能保留,中途失败时也不该让用户从头培养一遍。按实际使用量计费的前提之一,就是尽量别让同一段已完成工作被无意义重跑。
边界同样要承认:十秒钟能重做、没有外部副作用的低风险转换,重新执行可能比维护检查点更便宜。检查点优先放在耗时长、成本高、可验收或会改变外部世界的阶段。专业不是给每一步加复杂基础设施,而是知道哪一步绝不能“再来一次”。
参考资料
更多推荐

所有评论(0)