Agent 实践
Agent 超时后不能直接重试:11 个故障场景验证未知终态恢复
用可运行状态机区分请求未发送、写入明确失败和提交后结果未知,验证一次性审批、expectedVersion、幂等键、权威状态核对与人工接管如何共同阻止重复副作用。
时间与证据
本实验于 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:审计中的动作标识。principal、tenant与tool:谁在哪个租户调用哪项写能力。desiredStatus:希望达到的业务状态。expectedVersion:执行前读取到的权威版本,阻止陈旧覆盖。idempotencyKey:同一业务意图在重放时保持不变。approvalId与now:写操作必须消费一次仍有效且与主体、租户、工具、目标状态、预期版本完全绑定的审批。fault:实验专用故障点,生产调用者不能控制。
工具保存已完成 idempotency key 的结果和完整意图绑定。再次收到同一个 key 且五个意图字段一致时,返回原结果并标记 replayed: true,不增加 mutation 计数;同 key 改目标状态、版本、主体、租户或工具则抛出 IdempotencyConflict。审批也会原子地从 granted 变为 consumed,因此换一个 key 不能绕过一次性边界。
十一个固定场景
- 有效审批后正常写入。
- 同一个 idempotency key 和完整意图再次调用。
- 同一个 key 改写业务意图,明确失败且不 dispatch。
- 已消费审批换新 key 再用,在 dispatch 前阻断。
- 审批绑定的租户与动作不一致,在 dispatch 前阻断。
- 请求确认未发送时连接失败,允许一次安全重试。
- 上游提交后连接中断,读取权威状态发现目标已成立。
- 上游提交后又出现后续权威变更,核对不匹配并要求人工处理。
expectedVersion陈旧,上游明确拒绝。- 没有审批,在 dispatch 前阻断。
- 审批过期,在 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。审批证明“谁在什么条件下允许尝试”,不证明业务状态永远没有变化。
另一条失败路径专门改变 desiredStatus 和 expectedVersion,同时复用已经成功的 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
关联可复用项目
本文已经进入以下工程项目;项目页提供固定版本、运行命令和结果工件。
- 已独立复现
本文用 11 个故障场景验证安全重试、幂等意图绑定、权威对账和 unknown 后不自动重放。
Source ledger
来源账本
以下来源用于核对事实、日期与当时可用范围。厂商自报性能不视为本站独立复现。
讨论