M07 持久执行、幂等与恢复
本章产出:明确的崩溃窗口分析、稳定操作标识、事务内去重与外部结果核对。你应能说明系统保证了什么,以及保证不成立的边界。
1. 最重要的故障:成功了,但不知道成功了
Worker提交退款请求。退款服务已入账,响应却在网络中丢失。Worker只看到超时。此时存在两个与本地观测相容的世界:请求根本没到;请求已成功但响应丢了。
仅凭这次超时,客户端不能区分两者。盲目重试可能重复退款;不重试可能永远不完成。解决路径需要外部接口配合:稳定幂等键、按操作ID查询、业务唯一约束,或者进入人工核对。换一个更复杂的Agent框架不能消除信息缺失。
退款提交后断电
把未知响应与业务事实分开,用同一意图恢复。
合成教学数据 · 未运行真实模型静态推演与完整规格
I14 退款提交后断电
关联章节:M07 · 交互类型:fault-injection · 状态:提供纯逻辑参考函数,UI待实现
教学目标
把未知响应与业务事实分开,用同一意图恢复。
初始状态
{
"approved": false,
"expired": false,
"ledger_count": 0,
"committed": false,
"status": "PENDING",
"amount_cents": 10000
}
操作控件
批准
正常提交
提交前崩溃
提交后丢响应
同参重试
改变金额重试
推进至过期
重置
状态变化规则
批准只改变approval,不产生账本。
before_commit不留下业务写入;after_commit账本+1但响应状态UNKNOWN。
同参重试有已确认结果时只返回,不再加账本。
同意图不同金额拒绝;过期只禁止新执行。
黄金案例与反馈
lost-response
输入:
[
"approve",
"crash_after_commit",
"retry"
]
期望:
{
"ledger_count": 1,
"status": "CONFIRMED"
}
恢复读取已有操作结果。
precommit
输入:
[
"approve",
"crash_before_commit"
]
期望:
{
"ledger_count": 0,
"status": "RETRYABLE"
}
本地事务完整回滚。
unapproved
输入:
[
"submit"
]
期望:
{
"ledger_count": 0,
"status": "APPROVAL_REQUIRED"
}
正常模型输出也不能绕过批准。
完成条件
完成两种崩溃对照
保持重复重试账本=1
反馈层次
提交预测后显示结果;允许跳过预测但记录跳过,不阻止阅读
结果同时显示状态变化、原因与被拒绝动作;不是只给红绿灯
第一次错误给边界提示,第二次提供最小反例;参考推导始终可主动打开
重置恢复固定初始状态并保留历史尝试供用户手动清除
无障碍与静态替代
全部操作有原生button/input/select替代,不依赖拖拽
图与动画提供同步文字状态表;支持prefers-reduced-motion
状态改变用aria-live=polite摘要,不逐字朗读长日志
焦点不跳走;错误提示关联输入;颜色不是唯一编码
无JavaScript时直接展示初始输入、规则和黄金案例表,读者仍能完成纸面推演。状态动画仅作解释,不能成为获取答案的唯一途径。
持久化与重置
使用版本化键agent-course:1:I14,仅保存本地学习状态。支持单卡重置和全站清除;存储失败时以内存模式继续,不阻断阅读。
必须避免的误导
模拟的是单数据库事务;远程服务须依赖其幂等/查询契约。
执行边界
纯本地教学状态机;不执行任意代码,不连接真实资金/模型/宿主Shell。
逻辑参考
web/reference/engines.mjs中的refundStep;对应测试在web/reference/engines.test.mjs。UI可复用或等价移植,但不得改变黄金结果。
2. 把一项操作拆成可检查的阶段
课程退款意图具有稳定operation_id与参数摘要。执行状态为PREPARED、SUBMITTED、UNKNOWN、CONFIRMED或REJECTED。这些描述操作结果,不等于run的状态。
| 崩溃点 | 已知事实 | 恢复动作 |
|---|---|---|
| 保存意图前 | 没有可恢复操作 | 从任务决策重新开始,尚无授权提交 |
| 意图已保存、请求未发出 | 有稳定键与有效提案 | 检查授权后使用同一键提交 |
| 请求已发出、未获确定结果 | 外部可能成功 | 查询同一操作;必要时以同一键重试 |
| 外部已确认、本地结果未保存 | 外部有事实 | 通过原键取回结果并保存 |
| 本地已确认、Checkpoint未推进 | 操作已完成 | 复用保存结果,不新建业务意图 |
如果供应商没有幂等或查询能力,只能更保守地阻止自动重试并标记UNKNOWN。课程不伪造一种本地数据库魔法来保证任意外部API恰好执行一次。
3. 幂等键代表意图,不代表一次网络尝试
同一业务意图的每次重试使用同一键。用户明确发起另一笔退款,必须经过新资格计算和审批,生成新意图。不要用“参数相同”自动认定重复:用户可能真的想订两件相同商品。
键的作用域至少包含租户/调用者与操作类型。保存参数摘要;同一键搭配不同参数应返回冲突,不能返回旧结果掩盖调用错误。去重记录保留多久,也属于接口契约:过早清理会让迟到重试产生第二次效果。Amazon Builders Library对意图标识、原子记录和参数不一致有直接讨论。S29
课程使用服务端生成的operation_id。attempt_id和工具call_id不能替代它:模型重新规划可能产生新的call_id,Worker重启也会产生新attempt,但业务意图不应因此改变。
4. 同一数据库内可以做出的强保证
如果订单余额、审批、操作记录和退款账本都在同一个支持事务的数据库,我们可以把检查、扣减余额、写账本、写操作结果放在一个事务里。唯一约束阻止同一operation写入多次;参数摘要阻止错用键;条件更新阻止余额超限。
参考实现用SQLite的写事务演示这个不变量。它证明的是教学范围内的原子性,不是PostgreSQL多节点服务或真实支付网络的吞吐/可用性。生产系统还要处理备份、故障转移、事务重试和幂等记录保留。
错误示例是先在Redis写“已处理”,再在数据库退款。两步之间崩溃会使操作永远被跳过;反过来先退款再写Redis会重复。若两个系统没有共享事务,就需要定义协议,而不是声称“用了锁所以一致”。
5. Checkpoint、事件重放与业务去重
Checkpoint保存执行进度快照;事件重放根据持久历史恢复状态;幂等性保证重复尝试不重复产生业务效果。三个概念相关,但不能互相替代。
只保存messages,无法知道某次写操作是否已成功。只保存“当前节点=退款”,恢复时仍可能再次退款。只使用外部幂等键,则执行状态和用户审批仍可能丢失。
对模型调用同样要考虑重复:模型已产生响应但结果没有可靠保存,恢复可能再次调用,增加费用且得到不同答案。若框架能复用已记录结果,可以减少重复;但结果记录之前的故障窗口仍需按照实际平台语义分析,不能承诺零重复计费。
6. Temporal应该怎样学习
Temporal把可重放的工作流控制逻辑与执行I/O的Activity区分开。学习重点是历史、重放、重试和外部副作用之间的边界,而不是把所有Python函数套上装饰器。S22
把模型请求或退款提交放进Activity后,故障时Activity仍可能再次尝试,因此外部操作要有幂等保护。审批可以成为持久工作流接收的事件;它不需要一个进程持续占用线程等用户。S23
本课程要求你用同一组故障情景比较手写执行器与Temporal或LangGraph中的一种,不要求三套完整重复实现。对照报告必须写清:恢复点在哪里、哪些代码重跑、哪些结果可复用、哪些副作用由业务层去重。框架升级也要重跑这组测试。
7. 租约过期后,旧Worker可能仍在运行
假设A拿到租约和fence=7,发生长时间暂停;租约过期后B拿到fence=8。A恢复后仍可能继续写。如果只检查“我曾经有锁”,A会覆盖B。
fencing token是单调递增的所有权代次。受保护的资源必须检查token,拒绝低于当前代次的写入。只在日志里记录fence毫无作用。本地状态提交可以用WHERE fence=8;外部系统若不支持fencing,不能因此宣布外部副作用已受保护,仍要靠稳定意图键和业务约束。
租约检查、授权和业务写入之间也可能有竞态。对同库资源,检查与写入必须处在合适的事务或条件写里;对远端资源,需要其接口参与。交互卡展示这个边界,而不是把token画成万能盾牌。
旧Worker为什么还会写入?
lease到期与资源拒绝旧拥有者是两个机制。
合成教学数据 · 未运行真实模型静态推演与完整规格
I15 旧Worker为什么还会写入?
关联章节:M07 · 交互类型:lease-race · 状态:提供纯逻辑参考函数,UI待实现
教学目标
lease到期与资源拒绝旧拥有者是两个机制。
初始状态
{
"current_token": 1,
"lease_until": 10,
"now": 0,
"workers": {
"A": 1,
"B": null
}
}
操作控件
推进时钟
B接管并递增token
A尝试写
B尝试写
关闭资源端token检查(危险对照)
状态变化规则
A在t0持有token1,lease至10。
t10后B接管得到token2;资源写入要求token相同且now<lease_until。
危险对照只演示如果不检查会产生的错误,不改变正确答案。
黄金案例与反馈
old-owner
输入:
{
"token": 1,
"current": 2,
"now": 11,
"lease_until": 20
}
期望:
{
"allowed": false
}
lease过期不会物理停止A,资源必须拒绝旧token。
new-owner
输入:
{
"token": 2,
"current": 2,
"now": 11,
"lease_until": 20
}
期望:
{
"allowed": true
}
当前拥有者在有效lease内。
boundary
输入:
{
"token": 2,
"current": 2,
"now": 20,
"lease_until": 20
}
期望:
{
"allowed": false
}
到期边界不可写。
完成条件
构造A/B竞争并拒绝A
说明外部工具也需相应边界
反馈层次
提交预测后显示结果;允许跳过预测但记录跳过,不阻止阅读
结果同时显示状态变化、原因与被拒绝动作;不是只给红绿灯
第一次错误给边界提示,第二次提供最小反例;参考推导始终可主动打开
重置恢复固定初始状态并保留历史尝试供用户手动清除
无障碍与静态替代
全部操作有原生button/input/select替代,不依赖拖拽
图与动画提供同步文字状态表;支持prefers-reduced-motion
状态改变用aria-live=polite摘要,不逐字朗读长日志
焦点不跳走;错误提示关联输入;颜色不是唯一编码
无JavaScript时直接展示初始输入、规则和黄金案例表,读者仍能完成纸面推演。状态动画仅作解释,不能成为获取答案的唯一途径。
持久化与重置
使用版本化键agent-course:1:I15,仅保存本地学习状态。支持单卡重置和全站清除;存储失败时以内存模式继续,不阻断阅读。
必须避免的误导
fencing不等于自动取消旧线程,且不能保护未验证token的远程资源。
执行边界
纯本地教学状态机;不执行任意代码,不连接真实资金/模型/宿主Shell。
逻辑参考
web/reference/engines.mjs中的canWrite;对应测试在web/reference/engines.test.mjs。UI可复用或等价移植,但不得改变黄金结果。
8. 补偿不是时间倒流
订票后酒店失败,可以尝试取消机票,但取消可能有费用、时限或失败。这是新的业务动作,不是把世界回滚到过去。记录补偿状态、权限与结果,并准备人工处理。
退款本身通常不能通过“负数退款”直接撤回。课程不允许这种工具;若业务需要追回款项,那属于另一套明确授权的流程。
9. 必做故障矩阵
L06覆盖提交前崩溃、提交后响应丢失、本地结果保存前崩溃、重复审批、两个Worker竞争、旧Worker迟到、参数变化复用键、审批过期、订单版本变化。
一个合格实现必须让账本事实成为判据:同一意图最多一笔退款;合法重试返回同一结果;不同参数被拒绝;无法判断的外部结果进入核对而非谎报完成。参考实现与模拟器都在包内,真实支付系统不在实验范围内。
K16 · 持久工作流的真正价值和边界
K16 持久工作流的真正价值和边界
类型:可靠后端 · 关联章节:M07 · 阅读方式:先做下面的推演,再看参考讨论。
原文在说什么
Temporal的AI教程展示持久工作流与活动的组织;AWS的幂等实践强调安全重试需要可识别的调用意图。
对本课程的具体启发(课程推论)
持久执行减少重启后协调状态丢失的问题;外部副作用是否可重复,仍取决于活动及其下游服务。两种机制需要组合,而不是互相替代。
两分钟推演
退款Activity提交成功后Worker退出,描述重放时允许重跑哪部分、必须去重哪部分。
参考讨论
调度/协调可以按框架语义恢复;实际退款继续使用同一operation_id,并由业务服务返还已有结果。框架历史不是支付账本。
不能从这里推出什么
课程参考仅证明本地事务语义;接入Temporal时需验证所选版本的重放与重试行为。
原始来源
- S22 · How To Build a Durable AI Agent with Temporal and Python(Temporal;核查 2026-10-27)
- S23 · AI Cookbook(Temporal;核查 2026-10-27)
- S29 · Making retries safe with idempotent APIs(Amazon Builders Library;核查 2026-10-27)
本卡是原创中文概括与教学延伸,不是全文翻译,也不表示原作者认可本课程。不要把原文中的结果替换成本项目实测结果。
独立阅读 →