W06 · S36—S42 · 总 Day 126—132
Agent 恢复:请求重试不等于副作用重做
模拟副作用成功后丢失确认,观察接收方幂等去重。
作者准备的学习示例 · 不计真实学习进度 · 不代表生产 / GPU / 真机结果
核心问题
Agent 发出了“创建审核草稿”的请求,接收方已经完成,确认消息却丢了。Agent 超时后看到的不是“失败”,而是“不知道外部是否成功”。如果直接再做一次,就可能创建两个草稿。
对应 S36~S42。把 checkpoint、重试与幂等拆开,理解它们分别保护什么。
1. 一次失败发生在哪个间隙
保存 ready checkpoint
→ 接收方提交副作用
→ 确认丢失 / 调用方崩溃
→ 从 ready 恢复
→ 使用同一 operationId 重试
→ 接收方返回已有 receipt
→ 调用方保存 done
Checkpoint 描述调用方恢复到哪个状态;接收方幂等记录描述某个业务动作是否已经做过。只有调用方保存状态,无法覆盖“外部已成功、内部尚未记账”的窗口。
本例的 operationId 是租户、案例、动作和版本组合,不是每次 HTTP 尝试的随机 ID。相同业务动作多次尝试应复用同一个键;真正的新动作则需要新的业务身份。接收方还比较 payload:同一键不能静默对应不同内容。
2. 运行并跟踪两个计数
npm run learning:p2 -- w06
stableOperationKey 中第一次 reused=false,第二次 reused=true,两次拿到同一 receipt,effects=1。freshKeyEveryRetry 每次换键,输出 effects=2。不同之处不是是否进行了重试,而是接收方能否识别这是同一业务动作。
示例把 ready 状态序列化为 JSON 再恢复,接收方 Map 在恢复调用方时保留。它模拟的是“调用方状态丢失、接收方仍活着”,没有真实重启进程、磁盘持久化或数据库事务。每次重新运行脚本,所有状态都会重置。
3. 不要轻易说 exactly once
生产中,接收方通常需要在同一事务里完成“检查键、写副作用、写 receipt”,并处理并发请求、唯一约束、保留期和故障恢复。这里的同步 Map 只能在当前单进程顺序执行下展示去重。
如果先写副作用、后写去重记录,中间崩溃仍可能重复。如果先记已处理、后写副作用,中间崩溃可能丢动作。Outbox 能帮助连接本地事务与消息发送,但接收方仍需明确重复消息的语义。补偿是新的业务动作,通常不能让时间倒流。
对于本来不支持幂等键的外部工具,可以先查询业务状态、改用可幂等接口,或在不确定时交给人处理。不能单靠在 Prompt 里写“不要重复”解决外部一致性。
4. 轻量修改
保持 operationId 不变,只改变第二次 payload,观察接收方拒绝。再思考去重记录被清空会怎样:如果接收方忘了历史,同一键也可能再执行。写一句“当前保证依赖于……”比再做一套框架更有价值。
可选源码对照:仓库 src/agent/durable/checkpointMachine.ts 是另一份教学状态机。比较它保存的状态与这里接收方保存的 receipt,不把二者都叫“持久化”就结束分析。
5. 专业阅读与未来连接
- Temporal Activity Definition:重点阅读 Activity 幂等性,理解 workflow 恢复并不取消外部副作用的不确定性。
- MIT 6.5840:选 RPC、事务或复制相关讲义,只研究失败窗口,不做完整分布式课程作业。
长任务 Agent 会重复遇到相同问题。具身动作更不能把“没有收到确认”直接理解为“机械臂没有动过”。这里创建的是内存 receipt,没有真机动作与安全控制;迁移时必须重新设计状态观测、过期命令和人工接管。