一个 Agent 连续调用模型和工具,看起来已经形成了 Loop:
提示词
模型决策 → 调用工具 → 写回结果 → 再次决策 → 完成
但只要把进程重启、网络超时或人工等待放进来,这条顺滑的箭头很快会断掉:
模型已经规划完,Worker 重启后却从头再问一次;
工单已经写入,但响应在返回途中丢失,系统又写了一遍;
一个临时超时被无限重试,Token、时间和下游配额一起耗尽;
用户在审批页面点了取消,新 Worker 恢复后仍继续执行;
Worker A 的租约已经过期,Worker B 接管后,A 又醒来提交了一次旧写入;
系统最后显示 failed ,但没人知道失败前到底做到了哪一步。
这些问题并不只属于 Agent。它们是长流程、任务队列和分布式系统反复面对的可靠性问题。Agent 的特殊之处在于:中间步骤可能昂贵、非确定,Context 会变化,工具还会修改真实世界。
本文是 AI Agent 工程进阶 第 6 篇。上一篇 Tool Engineering 已经为工具补上权限、审批、幂等和结构化错误;这一篇继续回答: 当一次 Agent 运行被打断时,系统怎样从最近的可信边界继续,而不是失忆、重做或重复产生副作用。
Durable Loop 原理、故障恢复与可复现实验
已经实现 Agent Loop、后台任务或人工审批,开始处理长任务、重启与重复写入的开发者
Python 3.10+,零第三方依赖,不需要 API Key
04c5d41
文件型 RunState、故障注入器、稳定动作 ID、回执恢复、重试策略、取消和 fencing 测试
重建 Runner 并从磁盘恢复 JSON;不杀真实 OS 进程,不测网络、数据库或多机一致性
本文不承诺 exactly-once
跨进程、网络和外部系统时,“某段代码只运行一次”通常不是一个可以轻易得到的承诺。更现实的目标是:步骤可以重放,副作用可以去重,结果未知时可以查询或停住,恢复行为可以由证据解释。
一分钟概览
如果只保存这篇文章的结论,可以记住十二点:
普通 Loop 只描述控制流,Durable Loop 还要定义故障后的恢复语义。
模型 Context、RunState、Sandbox 工作区和业务回执是四种不同状态。
检查点要保存“下一步是什么”,也要保存“哪些步骤已经完成”。
写入前保存 pending action,写入后保存 receipt。
稳定 action_id 必须跨重试、跨 Worker、跨进程保持不变。
错误分类先于重试。
参数、权限和策略错误不应靠等待恢复;瞬时错误也只能在次数、deadline 和成本预算内重试。
Timeout 不等于失败。
查到业务回执就恢复为成功;查不到且无法证明未执行,就进入 waiting_reconciliation 。
取消必须进入持久状态,并在每个副作用前重新检查。
Lease 防止多个 Worker 主动处理同一任务,fencing token 防止过期 Worker 迟到写入。
Durable Runtime 不能替工具补幂等。
故障测试要验证副作用和恢复路径,不只看最终状态。
completed 可能掩盖重复写入, failed 也可能掩盖已经成功的业务动作。
图 1:旧 Worker 在模型步骤后崩溃,新 Worker 从持久检查点恢复,查询回执并继续完成 Agent Loop
1. 普通 Loop 和 Durable Loop 差在哪里
一个最小 Agent Loop 大概是:
代码
messages = [user_message] while True: response = call_model(messages, tools) if response.final_output: return response.final_output result = call_tool(response.tool_call) messages.extend([response.message, result])
这段代码在单进程、短任务和只读工具里可能很好用。问题是,它把几件重要事情都放在了进程内存里:
提示词
当前执行到哪一步 模型已经做过哪些决策 哪个工具动作正在进行 这次动作是否已经批准 一次超时是否应该重试 用户是否请求取消 外部写入是否已生效
进程退出以后,这些事实一起消失。
Durable Loop 不是把 while 换成某个框架 API,而是在 Loop 外增加一组明确协议:
提示词
普通 Loop = decide + act + observe + stop Durable Loop = 普通 Loop + persisted state + checkpoint boundaries + retry policy and budget + stable action identity + result reconciliation + cancellation + lease / fencing + explicit terminal states
它要回答的不是“正常时下一步做什么”,而是:
如果恰好在任意两行代码之间断电,下一次运行依据什么事实继续?
2. 先分清四种“状态”
很多恢复设计失败,是因为所有东西都被叫作 state。实际上,至少要分四层。
图 2:模型上下文、RunState、Workspace 与业务回执分别恢复对话、执行、产物和外部副作用
2.1 Model Context:下一次模型看见什么
它通常包含:
提示词
用户消息 模型输出 工具调用与结果 当前任务说明 被选中的证据 历史摘要
它解决的是推理连续性。上一篇 Context Architecture 已经讨论过 Context Packet 的来源、权限、新鲜度和预算。
但 Context 里写着“工单已更新”,并不能证明业务数据库真的存在这次更新。那可能只是模型生成的文字,也可能是旧工具输出。
2.2 RunState:Harness 执行到哪里
它至少应该包含:
提示词
run_id status current_step completed_steps attempts per step pending_action approval / cancel state next_retry_at failure code lease owner / epoch event cursor
它解决的是执行连续性:重启后不需要靠模型重新阅读所有消息,猜测之前做到哪一步。
2.3 Workspace:文件和产物是否还在
对 Codex、Claude Code 或其他代码 Agent,工作区可能包含:
提示词
源代码修改 下载资料 生成报告 测试结果 运行环境 Sandbox snapshot
OpenAI 的 Sandbox Agents 文档 特别区分了 RunState 、serialized session state 和 snapshot:前者恢复 Harness 侧执行位置,session state 用于重连同一个 Sandbox 会话,snapshot 用保存的文件内容启动新工作区。
这层解决产物连续性,但仍然不能替代业务回执。例如仓库里有一份“已发布”记录,不代表文章真的发布到了远端平台。
2.4 Business Receipt:外部世界到底发生了什么
生产化回执通常可以与稳定动作 ID 绑定:
代码
{ "action_id": "durable::run-42::record-followup", "request_fingerprint": "sha256:...", "status": "committed", "resource_id": "T-102", "result": {"recorded": true}, "committed_at": "2026-07-31T12:30:00Z" }
它解决副作用连续性:重试时能判断这是同一个动作,结果未知时能查询原始结果,同一个 key 配不同参数时能拒绝冲突。
四层状态可以互相引用,但不能互相冒充。
3. 一次写操作周围,有三个故障窗口
只说“工具调用失败了”不够。故障发生在副作用之前还是之后,会改变整个恢复策略。
图 3:一次写操作从请求前检查点到业务提交、回执和状态保存之间存在三个不同故障窗口
窗口 A:请求发出前崩溃
此时业务副作用尚未发生。如果输入仍然有效,恢复后通常可以重新执行。
提示词
checkpoint saved process crashes request never sent
这也是为什么写入前要保存 pending_action 。恢复器看到它以后,至少知道原本准备执行哪个动作。
窗口 B:业务提交成功,响应丢失
这是最危险的窗口:
提示词
request sent business write committed network connection lost runtime sees timeout
对读操作,重试通常只是多花时间。对发邮件、记账、下单、发布内容或创建资源,盲目重试可能重复产生副作用。
此时 timeout 只能说明客户端没有收到确定答复,不能说明服务端没有执行。
窗口 C:响应已收到,检查点未保存
进程已经拿到成功结果,但来不及把 current_step 和 receipt 写入 RunState。恢复后,运行时可能再次进入同一步。
稳定动作 ID 和持久回执在这里发挥作用:同一动作再次到达业务系统时,应返回第一次的结果,而不是重新写入。
因此,比“是否重试”更基础的问题是:
提示词
这一步是否有副作用? 结果是 known success、known failure,还是 unknown? 同一动作能否被可靠识别? 业务系统能否查询或重放原始回执?
4. 检查点不是把整个 Python 对象 dump 下来
检查点要同时满足两个目标:
足够稳定,能够跨版本、跨进程和跨 Worker 读取。
本篇 Lab 的核心状态经过简化:
代码
@dataclass class DurableRunState: run_id: str case_id: str strategy: str status: str = "ready" current_step: int = 0 completed_steps: list[str] = field(default_factory=list) step_outputs: dict[str, dict[str, Any]] = field(default_factory=dict) attempts: dict[str, int] = field(default_factory=dict) pending_action: dict[str, Any] | None = None cancel_requested: bool = False failure_code: str | None = None next_retry_ms: int | None = None lease_owner: str | None = None lease_epoch: int = 0 events: list[dict[str, Any]] = field(default_factory=list)
完整实现见 agent_lab/durable.py 。
4.1 保存逻辑游标,不保存不可重建对象
优先保存:
提示词
稳定 ID 枚举状态 JSON 输入与输出 版本号 来源引用 错误分类 时间戳或逻辑时间
谨慎保存:
提示词
数据库连接 文件句柄 协程对象 闭包 只在当前进程有效的 SDK 实例 明文 Secret 无法版本化的任意对象图
4.2 每个检查点要有清楚语义
不是“想到就存一下”,而是围绕可恢复边界:
提示词
run_started after_model before_retry_wait before_write after_write human_wait cancel_requested completed / failed / reconciliation
4.3 检查点粒度有代价
检查点越细:
检查点越粗:
LangGraph 的 Thinking in LangGraph 说明,其 durable execution 在节点边界创建检查点;中断节点恢复时会从该节点开头重新执行。节点越大,故障后可能重复的工作越多。因此节点粒度也是恢复粒度。
本篇 Lab 使用 JSON 文件和临时文件替换:
代码
def save(self, state: DurableRunState) -> None: target = self.root / f"{_safe_name(state.run_id)}.json" temporary = target.with_suffix(".json.tmp") temporary.write_text(json.dumps(state.to_dict(), indent=2)) temporary.replace(target)
这只演示文件边界上的原子替换。生产环境还要处理数据库事务、并发更新、版本迁移、备份、可见性和持久化保证,不能把一个 JSON 文件当作生产 Durable Store。
5. 重试先分类,再谈退避
“失败就重试三次”容易写,也容易制造更大的故障。
图 4:Durable Loop 先判断永久错误、外部副作用、结果状态、幂等与预算,再选择恢复动作
5.1 永久错误不应该自动重试
典型例子:
提示词
invalid_arguments permission_denied approval_rejected resource_not_found under a stable ID policy_violation unsupported_operation
等待 100 ms 不会让缺失参数自己出现,也不会让权限自动增加。正确动作通常是修输入、请求授权、改变计划或明确失败。
5.2 瞬时错误也不是无限重试
典型例子:
提示词
429 throttling gateway timeout temporary dependency unavailable connection reset before any request was accepted
它们可以进入有限重试,但至少要有:
提示词
max_attempts per-step deadline whole-run deadline cost / token budget backoff jitter cancel check
5.3 Backoff 和 Jitter 解决不同问题
指数退避可以写成:
提示词
delay = min(base × 2^(attempt - 1), max_delay)
如果很多 Worker 同时失败,它们按相同时间表重试,仍可能同时冲击下游。Jitter 用随机扰动打散重试时间。
本篇 Lab 为了让报告可重复,使用固定的 100 ms 、 200 ms 逻辑时间,不加入随机抖动,也不真实等待。生产实现应根据下游接口建议加入 jitter,并把 Retry-After 等服务端信号纳入策略。
5.4 重试预算要跨步骤和跨恢复保存
如果 attempts 只存在 Worker 内存里,每次进程重启都会重新获得三次机会,所谓最大重试次数就没有意义。
因此 Lab 把尝试次数写进 RunState:
代码
attempt = state.attempts.get("model", 0) + 1 state.attempts["model"] = attempt if attempt >= self.max_attempts: self._fail(state, "retry_exhausted", detail)
6. 结果未知时,不要猜
本篇最值得收藏的边界,是 write_result_unknown 。
图 5:写入超时后通过稳定 action_id 查询业务回执,查到则恢复成功,查不到则进入人工对账
Lab 为每个逻辑写入生成稳定 ID:
代码
action_id = f"{state.run_id}::record-followup" logical_operation = f"followup::{case.case_id}"
它不能在每次 retry 时重新生成。否则:
提示词
attempt 1 → key-a → 写入成功,响应丢失 attempt 2 → key-b → 业务系统认为是新动作,再写一次
正确恢复路径是:
代码
try: receipt = effects.record_followup(action_id=action_id, ...) except ResultUnknownError: receipt = effects.lookup_receipt(action_id) if receipt is None: state.status = "waiting_reconciliation" state.failure_code = "result_unknown" checkpoint(state) return # 原写入已成功,恢复结果,不再次执行副作用 state.step_outputs["write"] = receipt
这里有两个容易混淆的结论。
6.1 查到回执,不是“忽略错误”
回执证明第一次写入已经提交。把当前步骤恢复为成功,是依据外部事实纠正客户端观察。
6.2 查不到回执,不等于证明没有写入
有些系统的回执落库和业务写入不在同一事务边界,或者外部 API 根本不支持按幂等键查询。此时“没有回执”可能只是回执延迟或系统不可用。
所以 Lab 选择:
提示词
status = waiting_reconciliation
而不是自动重放。
这也是 Durable Engineering 一个不太讨喜但很重要的原则: 可靠系统不一定自动完成所有任务,但必须诚实表达自己不知道什么。
AWS 的 Idempotency and retries 明确区分 at-least-once 与 at-most-once,并提醒 replay 和 retry 都可能让同一操作运行多次。其建议同样是让外部副作用支持幂等键,或使用条件写、唯一约束和追加日志等数据库模式。
7. 取消是持久状态,不是关闭连接
用户点取消时,常见实现是终止 HTTP 请求或取消当前协程。对后台任务,这通常不够:
更稳妥的方式是 cooperative cancellation:
提示词
1. 把 cancel_requested 持久化 2. 记录取消原因和请求者 3. 在每次模型调用前检查 4. 在每个副作用前再次检查 5. 在 retry wait 醒来后检查 6. 已进入结果未知时先对账,再决定 cancelled / completed / reconciliation
Lab 的 cancel-at-human-wait 案例这样运行:
提示词
模型步骤完成 → 进入 waiting_human → 保存 checkpoint → 用户请求取消 → cancel_requested 写入磁盘 → 新 Worker 恢复 → 写入前检查取消 → status = cancelled → side effects = 0
OpenAI Agents SDK 的 Human-in-the-loop 指南 使用可序列化 RunState 暂停审批,并在另一个时间或进程中恢复。它解决的是 SDK 运行状态与审批连续性。应用仍然要定义自己的取消语义、业务回执和外部副作用边界。
OpenAI Background mode 则允许一个长时间 Responses 请求异步运行、轮询状态和取消。它有助于避免客户端连接断开导致模型请求丢失,但不能自动让由多个模型调用、数据库写入和人工步骤组成的整个业务流程变得 durable。
8. Lease 与 fencing token 解决的是两个问题
当任务由多个 Worker 消费时,我们通常不希望两台机器同时推进同一个 Run。
Lease 可以表达:
提示词
run-42 当前由 worker-b 持有 租约在某个期限后失效 持有者需要 heartbeat 或续租
但 Lease 只能说明“谁现在应该工作”。它不能保证旧 Worker 已经停止。
图 6:Worker B 以更高 lease epoch 接管后,业务存储用 fencing token 拒绝 Worker A 的迟到写入
考虑这个顺序:
提示词
Worker A 获得 lease,epoch = 41 A 因长暂停或网络分区失去响应 lease 过期 Worker B 接管,epoch = 42 B 正常继续 A 恢复,以为自己仍然有权写入
如果只在调度器检查 Lease,A 的迟到写入仍可能到达业务系统。
Fencing token 的做法是:
提示词
每次接管产生单调递增 epoch 每次副作用携带 epoch 业务存储记住已接受的最高 epoch 低于最高值的迟到写入被拒绝
Lab 中的业务存储边界是:
代码
if fence < self.highest_fence: raise StaleWorkerError( f"fence={fence} is older than active fence={self.highest_fence}" )
Martin Kleppmann 在 How to do distributed locking 中用递增 fencing token 解释了相同问题:锁或租约有超时以后,旧客户端可能在暂停后恢复;真正接收写入的存储必须拒绝落后的 token。
现实系统还要处理:
本篇 Lab 只模拟 lease takeover 和迟到写入,不声称实现了完整分布式锁服务。
9. 一个可用的 Durable 状态机
状态名要帮助运营和恢复,而不是只保留 running / failed 。
本篇使用:
running
含义: Worker 正在推进
是否自动继续: 是
waiting_human
含义: 等待审批或人工输入
是否自动继续: 收到决定后继续
waiting_reconciliation
含义: 外部结果未知,缺少安全重试证据
是否自动继续: 否,先对账
completed
含义: 目标和副作用均有完成证据
是否自动继续: 否
failed
含义: 已知失败,带稳定 failure code
是否自动继续: 由策略决定是否新建 Run
cancelled
含义: 持久取消已在安全边界生效
是否自动继续: 否
还可以按业务需要加入:
提示词
retry_wait compensating paused_budget expired dead_letter
状态不要为了漂亮而增加。每个状态至少要回答:
提示词
谁能把它迁移到下一状态? 迁移前要检查什么? 要保存哪些证据? 运营人员看到它后能做什么?
10. 本篇 Lab:把故障变成固定测试集
这次没有换项目,仍然沿用同一个 Agent Reliability Lab。
版本从 0.5.0 升到 0.6.0 ,新增:
提示词
agent_lab/durable.py agent_lab/durable_reporting.py datasets/durable-cases.jsonl tests/test_durable.py reports/durable-comparison.json reports/durable-comparison.md reports/durable-failures.md reports/durable-runs.jsonl
两组对照是:
process-loop-v1
故意保持简单的进程内控制组;重启从头、错误统一重试、取消不持久、没有 fencing
durable-loop-v1
文件型 RunState、检查点、错误分类、稳定动作 ID、回执恢复、持久取消和 lease epoch
这不是在比较 Python 与某个框架,也不是在证明某种 Agent 拓扑更聪明。模型服务是固定脚本,工具输入也是固定的。实验只验证: 相同故障到来时,运行时是否遵守声明的恢复契约。
九个故障案例
提示词
01 clean-run 02 restart-after-model 03 model-transient-retry 04 model-permanent-error 05 retry-budget-exhausted 06 write-receipt-recovery 07 write-unknown 08 cancel-at-human-wait 09 stale-worker
图 7:Durable Loop Lab 的九个案例,以及进程内基线和 Durable Loop 在恢复、重试、对账、取消与 fencing 上的结果
11. 跟着运行 Lab 0.6.0
方式一:直接下载
下载 Durable Loop Lab 0.6.0 ZIP
解压后进入:
提示词
phase-7-agent-engineering/agent-reliability-lab
方式二:从 GitHub 检出固定提交
命令
git clone https://github.com/RalfNick/ai-agent-learn.git cd ai-agent-learn git checkout 04c5d41cd450fa737611e2b7680d77123fa90f83 cd phase-7-agent-engineering/agent-reliability-lab
第一步:运行故障测试
命令
python run_lab.py fault-test --output reports/local
预期退出码是 0 ,终端摘要应包含:
代码
{ "version": "0.6.0", "baseline": { "cases": 9, "passed_cases": 1, "duplicate_side_effects": 2, "blind_retries": 8 }, "candidate": { "cases": 9, "passed_cases": 9, "duplicate_side_effects": 0, "blind_retries": 0 }, "gate_passed": true }
第二步:运行全部测试
命令
python -m unittest discover -s tests -v
本文固定提交上的预期结果:
第三步:不要只看摘要
先看对照表:
提示词
reports/local/durable-comparison.md
再看失败台账:
提示词
reports/local/durable-failures.md
最后挑一个案例检查完整事件:
提示词
reports/local/durable-runs.jsonl
例如搜索:
命令
python -c "import json; from pathlib import Path; rows=[json.loads(x) for x in Path('reports/local/durable-runs.jsonl').read_text(encoding='utf-8').splitlines()]; print(json.dumps(next(r for r in rows if r['strategy']=='durable-loop-v1' and r['case']['id']=='write-receipt-recovery'), ensure_ascii=False, indent=2))"
你应该能在事件里看到:
提示词
write_started checkpoint_saved (before_write) write_result_unknown receipt_recovered write_completed checkpoint_saved (after_write) run_completed
12. 怎样读这组结果
完整报告见 reports/durable-comparison.md 。
案例通过率
process-loop-v1: 11.1%
durable-loop-v1: 100.0%
模型调用尝试总数
process-loop-v1: 16
durable-loop-v1: 13
重复副作用
process-loop-v1: 2
durable-loop-v1: 0
盲目重试
process-loop-v1: 8
durable-loop-v1: 0
明确终态率
process-loop-v1: 100.0%
durable-loop-v1: 100.0%
12.1 1/9 不代表普通 Loop 平时只有 11.1% 成功
这 9 个案例不是线上自然流量抽样,而是一组故意覆盖恢复边界的契约测试。正常案例只有一个,所以进程内基线通过 1 个并不意外。
这个数字只能解释为:
在本篇声明的 9 个固定恢复契约里,进程内控制组满足 1 个,候选实现满足 9 个。
不能解释为:
提示词
Durable Loop 让模型质量提升 88.9% 某个框架比另一个框架可靠 9 倍 线上故障率会按同样比例下降
12.2 两个 completed 可能完全不同
write-receipt-recovery 里,两种策略最终都显示 completed:
如果只测最终状态,这个严重差异会被隐藏。
12.3 waiting_reconciliation 不是失败的自动化
write-unknown 案例中,候选没有追求 completed,而是停在对账状态,副作用为 0。
这里的价值不是“更自动”,而是系统没有在证据不足时制造第二次写入。
12.4 模型尝试次数下降来自错误分类
基线对永久错误也重试三次;候选第一次遇到 invalid_request 就停止,因此总尝试数从 16 降到 13。
这不是模型变聪明,而是 Harness 不再把不可恢复错误当作瞬时故障。
13. 把代码换成真实基础设施时,要补什么
本篇 Lab 有意保持零依赖,因此很多生产责任只是接口雏形。
13.1 把 JSON Store 换成可靠状态存储
至少考虑:
提示词
乐观并发控制或 compare-and-set schema version 与迁移 事务边界 状态与事件的一致性 备份和恢复 按 run_id / tenant 隔离 加密与敏感字段脱敏 归档和删除策略
13.2 把进程内队列换成有所有权协议的调度
至少需要:
提示词
任务领取 lease / heartbeat 到期接管 fencing token 最大并发 优先级 dead-letter / reconciliation queue
13.3 把回执放到正确事务边界
如果业务写入和幂等回执分别写入两个不一致的存储,仍然会出现:
提示词
业务已提交,回执未提交 回执显示成功,业务事务回滚
优先选择:
13.4 用真实故障替代模拟故障
继续增加:
提示词
在写入前 kill Worker 在业务提交后、状态保存前 kill Worker 让状态库短暂不可用 让下游返回 429 / 500 / timeout 让两个 Worker 竞争同一个 run 在 retry wait 中取消 在部署新版本后恢复旧 RunState
并检查真实副作用数量,而不只检查日志。
14. OpenAI、LangGraph、Temporal、Anthropic 与 Google 各自提供什么
这些资料讨论的是相通问题,但抽象层并不相同。
OpenAI Background mode
主要提供: 单个长时间 Response 的异步执行、轮询与取消;通过 HTTP 游标恢复已启用 streaming 的 background Response
仍需应用定义: SDK 的流恢复能力仍需按版本核对;它不负责多步骤业务状态、工具幂等和跨系统事务
OpenAI Agents SDK RunState
主要提供: Agent 运行和人工审批的序列化、暂停与恢复
仍需应用定义: 业务回执、租约、生产状态存储与运营策略
OpenAI Sandbox session / snapshot
主要提供: 工作区会话重连与文件快照
仍需应用定义: 外部业务副作用和整个工作流的 durable orchestration
LangGraph
主要提供: Checkpointer、节点级恢复、interrupt、持久工作流
仍需应用定义: 节点粒度、幂等副作用、部署和业务契约
Temporal
主要提供: Workflow Event History、重放、Activity、重试和定时器
仍需应用定义: Activity 可能执行多次,应用仍要定义幂等、业务错误分类和外部事务
AWS Lambda Durable Functions
主要提供: Checkpoint / replay、Step、wait、retry 和长时间挂起
仍需应用定义: Step 的 at-least-once / at-most-once 语义、幂等键和外部副作用仍要显式选择
Anthropic long-running harness
主要提供: 通过 Git、进度文件和结构化产物跨 Session 继续工程任务
仍需应用定义: 通用业务工作流、回执、租约和分布式一致性
Google Agent Executor
主要提供: 事件日志、快照、恢复与分布式 Actor 方向
仍需应用定义: 仍处于早期开发,具体生产边界需按版本核对
LangGraph 的 Functional API 文档 明确要求入口与 task 输出可序列化,并建议把 API 调用放在 task 中、让副作用幂等,因为中断任务恢复时可能重新执行。
Temporal 把易失败、非确定的外部交互放进 Activity,并由 Workflow 保存执行历史。其 Activity 文档 明确说明:Activity 完成业务动作后、向服务端报告完成前仍可能崩溃,因此 Activity 可能再次执行,幂等仍是应用责任。
AWS Lambda Durable Functions 同样基于 checkpoint 和 replay,但它把外部动作放进 Step,并允许为每次 retry attempt 选择 at-least-once 或 at-most-once。后者也不自动等于整个工作流 exactly-once;是否重试、是否使用稳定幂等键,仍要与业务副作用一起设计。
Anthropic 的 Effective harnesses for long-running agents 使用 claude-progress.txt 、Git 历史、功能列表和可重复启动脚本,让新 Session 快速理解进度。这对代码 Agent 很实用,但它主要解决长任务的上下文与工程产物交接,不应直接等同于跨业务系统的 exactly-once 工作流。
Google 在 2026 年发布的 Agent Executor 把事件日志、快照、恢复和分布式执行放到 Agent Runtime 层。其仓库同时明确提示项目仍处于早期开发、核心恢复协议可能有破坏性变化,因此适合作为方向参考,不适合在没有版本评估时当成稳定事实依赖。
我的判断是:
提示词
先用本篇的状态与故障清单定义业务语义; 再选择框架承接持久化、重放和调度; 不要先选框架,再假设默认行为等于你的业务正确性。
15. 什么时候不需要 Durable Loop
并不是每次模型调用都要引入 Durable Runtime。
更适合保持简单的情况:
开始值得引入 Durable Loop 的信号:
提示词
一次运行会跨分钟、小时或人工等待 存在不可忽略的写操作 Worker 重启后必须继续 重做模型调用明显昂贵 同一任务可能被多个 Worker 接管 用户需要取消和状态查询 运营人员需要知道卡在哪一步
先从最小状态和一个真实故障开始,不需要第一天就引入大型编排平台。
16. 45 分钟练习:给自己的 Agent 加一个恢复边界
先在 Fake、测试租户或 staging 中练习
不要直接对生产邮件、正式文章、真实 CRM 或支付接口注入“提交后断线”。最小安全做法是使用本地 Fake Store,或者创建可清理的测试数据,并记录测试前后的真实副作用数量。只有在重复写入、未知结果和取消路径都通过集成测试后,才把同一恢复协议接到生产工具。
选一个当前 Agent 中真实存在的写工具,例如:
提示词
发布文章 发送邮件 创建 issue 写入 CRM 生成并上传报告
第 1 步:画出副作用时间线
标出:
提示词
before request request accepted effect committed receipt returned run state saved
产物:三个故障窗口及各自恢复动作。
第 2 步:定义稳定动作 ID
回答:
提示词
它由 run_id + step_id 组成,还是业务主键? 跨重试是否保持不变? 同 key 不同参数怎样报冲突? 回执保存在哪里?
产物:idempotency contract。
第 3 步:持久化最小 RunState
至少包含:
提示词
status current_step completed_steps attempts pending_action cancel_requested failure_code
产物:一份可序列化状态样本。
第 4 步:注入一次“提交后断线”
让测试执行:
提示词
业务写入成功 → 不返回响应 → 重建 Runner → 以相同 action_id 恢复
验收标准:真实副作用数量仍为 1。
第 5 步:再注入一次取消
在人工等待或 retry wait 时写入取消标记,重启 Worker 后恢复。
验收标准:后续写工具没有被调用,终态明确为 cancelled。
17. Durable Loop 收藏清单
状态与检查点
[ ] RunState 可以序列化并带 schema version
[ ] 尝试次数、预算和 deadline 跨进程保存
[ ] Secret 不进入持久 RunState
副作用与结果未知
[ ] Timeout 被区分为 known failure 与 unknown result
[ ] 无法证明安全时进入 reconciliation,而不是盲目重试
重试与取消
[ ] 瞬时错误受 max attempts、deadline 与成本预算控制
[ ] 生产重试使用 backoff 与 jitter
多 Worker
[ ] 失去 lease 的 Worker 会停止后续步骤
验证与运营
[ ] 事件能重建 checkpoint、retry、resume 和 reconcile 顺序
[ ] 运营人员能区分 failed、cancelled 与 waiting_reconciliation
[ ] 旧版本 RunState 有迁移或拒绝策略
[ ] 已用真实 kill、网络故障和并发接管做过集成测试
结语:可恢复,比一直运行更重要
一个长时间 Agent 不可能依靠“进程永远不挂、网络永远不抖、用户永远及时审批”获得可靠性。
Durable Loop 的目标也不是让所有事情自动重试到成功,而是建立一组可解释的恢复规则:
提示词
已完成的步骤不白做; 可安全重试的步骤有限重试; 永久错误尽早停止; 结果未知时先查外部事实; 没有证据时进入对账; 取消跨进程生效; 过期 Worker 不能迟到写入; 每个终态都有可检查证据。
如果只记住一句:
Durable Loop 不是保证代码永远只跑一次,而是让每次重放都有身份、每次副作用都有证据、每次中断都有明确恢复路径。
下一篇进入 Agent Tracing 。Durable Loop 已经会保存状态和事件,但当线上一次 Run 变慢、走错工具或停在 reconciliation 时,我们还需要用统一 Trace 回答:当时看见了什么、在哪一步等待、花了多少预算,以及失败最早从哪里开始。
参考资料
OpenAI
OpenAI Agents SDK:Human-in-the-loop
OpenAI Agents SDK:RunState
OpenAI Agents SDK:Results
Durable Execution 与长任务 Harness
LangGraph Functional API:Durable execution、determinism 与 idempotency
LangGraph:Thinking in LangGraph
Temporal:Workflow Execution
Temporal:Activity Definition 与幂等
AWS Lambda:Durable Functions
AWS Durable Execution:Idempotency and retries
Anthropic:Effective harnesses for long-running agents
Anthropic:Harness design for long-running application development
Martin Kleppmann:How to do distributed locking
本文代码与证据
Agent Reliability Lab 04c5d41
Durable Loop comparison report
Durable Loop failure ledger