Agent 实践

Agent 超时后不能直接重试:11 个故障场景验证未知终态恢复

用可运行状态机区分请求未发送、写入明确失败和提交后结果未知,验证一次性审批、expectedVersion、幂等键、权威状态核对与人工接管如何共同阻止重复副作用。

发布:2026/07/13更新:2026/07/13
Agent 写操作从计划和审批进入执行边界,提交后超时必须先读取权威状态,只有确认未发送时才能安全重试
图 1:本站依据 2026-07-13 内存故障注入实验绘制;它说明状态迁移,不代表真实网络或分布式事务已经验证。

时间与证据

本实验于 2026 年 7 月 13 日在仓库内运行。它不是某个 Agent 框架的厂商演示,而是一套最小状态机:模拟权威记录存储、一次性审批、版本检查、幂等键、故障注入、终态核对和人工接管。

可检查工件包括:

  • labs/agent-recovery/src/simulator.mjs:工具和 Agent action runner。
  • labs/agent-recovery/test/simulator.test.mjs:12 个自动测试,包含结果工件哈希和确定性 payload 一致性检查。
  • labs/agent-recovery/results/2026-07-13-results.json:11 个固定场景的完整审计轨迹,SHA-256 为 2831d4c8cd6b051c20b06fdbf62cb4bc775c6cd7752ee24b4f35e7cc26c63bc5

证据等级是 reproduced,因为代码、输入、故障点和权威状态都已实际运行;但 transport、数据库和审批存储都在单进程内存里,没有模拟进程崩溃、跨区网络、消息队列重复投递或数据库主从切换。因此本文验证的是 状态语义,不是生产可靠性或 SLA。

本文的场景数字和结论全部来自仓库内实现、测试和权威状态结果。RFC 9110 第 9.2.2 节给出一个关键边界:客户端不应自动重试非幂等请求,除非它知道请求语义本身幂等,或者能检测原请求从未生效。IETF Idempotency-Key draft-07 进一步描述了 payload fingerprint,并明确同一个 key 不得跨不同 payload 复用。实验把这两条方法约束落实成可执行分支,不把标准文本中的原则冒充本站实测结果。

实验设计

决定性问题是副作用,而不是返回文字

假设 Agent 获得批准,把客户工单从 open 改成 review。上游已经提交写入,但响应在网络中丢失。Agent 看到的是 timeout,权威数据库看到的却是成功。如果 Agent 把 timeout 统一解释为失败并自动重试,可能重复退款、重复发信或重复创建云资源。

因此 action 不能只有 success/failed 两个状态。实验至少区分:

planned -> approved -> dispatching
                     ├─ succeeded
                     ├─ failed(上游明确拒绝)
                     └─ unknown(可能已经发生副作用)
                                      └─ reconcile -> succeeded / human required

只有系统能够证明请求 没有越过 dispatch 边界 时,才允许一次受限重试;一旦可能提交,自动重试关闭,先读取权威状态。

动作合同

每个写动作固定以下字段:

  • id:审计中的动作标识。
  • principaltenanttool:谁在哪个租户调用哪项写能力。
  • desiredStatus:希望达到的业务状态。
  • expectedVersion:执行前读取到的权威版本,阻止陈旧覆盖。
  • idempotencyKey:同一业务意图在重放时保持不变。
  • approvalIdnow:写操作必须消费一次仍有效且与主体、租户、工具、目标状态、预期版本完全绑定的审批。
  • fault:实验专用故障点,生产调用者不能控制。

工具保存已完成 idempotency key 的结果和完整意图绑定。再次收到同一个 key 且五个意图字段一致时,返回原结果并标记 replayed: true,不增加 mutation 计数;同 key 改目标状态、版本、主体、租户或工具则抛出 IdempotencyConflict。审批也会原子地从 granted 变为 consumed,因此换一个 key 不能绕过一次性边界。

十一个固定场景

  1. 有效审批后正常写入。
  2. 同一个 idempotency key 和完整意图再次调用。
  3. 同一个 key 改写业务意图,明确失败且不 dispatch。
  4. 已消费审批换新 key 再用,在 dispatch 前阻断。
  5. 审批绑定的租户与动作不一致,在 dispatch 前阻断。
  6. 请求确认未发送时连接失败,允许一次安全重试。
  7. 上游提交后连接中断,读取权威状态发现目标已成立。
  8. 上游提交后又出现后续权威变更,核对不匹配并要求人工处理。
  9. expectedVersion 陈旧,上游明确拒绝。
  10. 没有审批,在 dispatch 前阻断。
  11. 审批过期,在 dispatch 前阻断。

测试组合不只断言 runner 返回值,还覆盖工具 dispatch 次数、mutation 次数、最终记录版本,以及 unknown 之后是否出现新的 dispatch。汇总 oracle 同时检查 automaticRetry: false 和 unknown 事件之后不存在 dispatch,避免只记录“不重试”文字却仍实际重放。

复现命令:

node --test labs/agent-recovery/test/simulator.test.mjs
node labs/agent-recovery/src/simulator.mjs \
  --out /tmp/younis-ai-lab-agent-recovery.json
shasum -a 256 labs/agent-recovery/results/2026-07-13-results.json

结果

固定结果摘要为:

项目 结果 解释
场景数 11 覆盖成功、精确重放、意图冲突、两类连接失败、版本冲突和审批拒绝
succeeded 4 正常成功、幂等重放、安全重试、终态核对成功
blocked 4 审批复用、租户错位、缺少审批和审批过期均未 dispatch
failed 2 幂等键意图冲突与陈旧版本均未改变权威状态
unknown 1 权威状态已被后续变化替换,需要人工判断
执行过 reconcile 2 一次匹配目标、一次不匹配
要求人工接管 1 不把状态不一致伪装成成功或失败
非预期重复 mutation 0 同一 idempotency key 只写一次
unknown 后自动重放 0 两个未知终态都先核对权威状态

结果说明“重试”不是一个统一动作。before-dispatch 故障能证明上游没收到请求,因此 runner 只重试一次;after-commit 故障无法证明是否写入,runner 记录 unknown 并读取权威记录。第一条 unknown 发现 status=review,于是收敛为成功;第二条发现 status=manual-hold、版本已继续变化,保持 unknown 并设置 humanRequired=true

陈旧版本场景也很重要。即使审批本身仍有效,如果权威记录从版本 1 变为版本 2,旧动作不能静默覆盖。实验抛出 VersionConflict,mutation 保持 0。审批证明“谁在什么条件下允许尝试”,不证明业务状态永远没有变化。

另一条失败路径专门改变 desiredStatusexpectedVersion,同时复用已经成功的 idempotency key。runner 返回 IdempotencyConflict,而不是把旧的 status=review 伪装成新请求成功。这个负例保证 key 是业务意图的稳定标识,不是绕过参数核对的缓存键。

失败与边界

第一,所谓“确认未发送”在真实网络中很难获得。只有调用在本地队列、连接建立或序列化之前失败,系统才可能确定没有跨过边界;普通 socket timeout 通常不能提供这项保证。生产实现应保守地把不确定情况标为 unknown。

第二,内存 idempotency store 与业务记录不是同一事务。进程在写入业务状态后、保存 idempotency 结果前崩溃,仍可能产生窗口。生产需要让业务写入、幂等记录与 outbox/审计终态具备明确的原子性或可恢复协议。

第三,权威状态“与目标相同”不总能证明是本次动作造成的。退款金额、收款方和订单版本等高风险操作需要核对业务标识和执行记录,而不只是比较一个 status 字符串。

第四,实验没有模拟审批撤销、跨租户路由、token 过期、并发人工操作和长期任务取消。这些必须加入持久化实现后再测。

第五,11 个场景是分支覆盖,不是故障概率。0 次重复副作用不能解释为生产重复率为零,只能证明这些确定性路径没有重复。

商业价值

最需要这套恢复语义的是允许 Agent 修改 CRM、工单、财务、代码仓库或云资源的团队。客户不会单独为“状态机”付费,他们付费购买的是:自动化可以节省操作时间,同时不会在超时后重复产生不可逆结果。

可以衡量的单位价值是:

受控自动化净价值
= 节省的人工操作时间 + 缩短的处理周期
- 审批与核对成本 - 重复副作用损失 - 事故恢复成本

适合先开放的是低金额、结果可读取、能够幂等化的动作,例如更新草稿状态或创建带唯一业务键的内部任务。不适合直接自动化的是无法查询终态的第三方支付、一次性外部通知和缺少幂等接口的遗留系统。

未来 90 天的可证伪门槛是:在持久化测试环境连续运行至少 10,000 次故障注入,覆盖提交前断线、提交后断线、进程崩溃与并发变更;重复业务副作用必须为 0,unknown 必须全部进入自动核对或人工队列,且没有丢失审计终态。未达到前,不能将这套内存实验宣传为生产 Agent 平台。

复盘

这项实验把“Agent 会不会重试”转换成了四个可检查问题:请求是否确定未发送、业务是否支持幂等、权威状态是什么、无法核实时谁接管。模型可以提出下一步,但不能通过自然语言自信程度决定写操作是否再次执行。

它也与既有全栈能力直接相连:数据库乐观锁变成 Agent 的 expectedVersion,HTTP idempotency key 变成工具重放边界,任务状态机承载 unknown,运维 reconciliation 负责最终收敛。AI 系统新增的是更长、更不确定的执行链,并没有取消事务与审计原则。

下一版将把状态、审批和 idempotency 记录迁移到 PostgreSQL,增加进程 kill、事务边界、outbox 投递与并发测试。只有持久化故障实验完成后,证据等级才继续升级。

方法披露

本文使用 AI 工具协助枚举故障分支、审查测试和绘制 SVG;状态语义、代码、断言、原始结果与最终商业判断由作者逐项确认。实验没有访问真实客户数据,也没有调用外部写接口。

修订记录

  • 2026-07-13:初版发布并完成审计修订;实现 11 个故障场景和 12 个自动测试,加入一次性绑定审批、幂等意图冲突、unknown reconciliation、不匹配人工接管、重复副作用与工件完整性检查。

Reusable projects

关联可复用项目

本文已经进入以下工程项目;项目页提供固定版本、运行命令和结果工件。

Source ledger

来源账本

以下来源用于核对事实、日期与当时可用范围。厂商自报性能不视为本站独立复现。

  1. HTTP Semantics, Section 9.2.2: Idempotent Methods
    RFC Editor官方文档文章资料来源发布:2022/06/06本站核验:2026/07/13
  2. The Idempotency-Key HTTP Header Field, draft-07
    IETF HTTPAPI Working Group官方文档文章资料来源发布:2025/10/15本站核验:2026/07/13

讨论

正在加载评论...