外观
Agent 重复副作用与幂等补偿故障复盘
30 秒复盘结论: 这是一场故障演练:Agent 调用高风险工具已经完成外部副作用,但响应在回写前丢失,编排器把“结果未知”误判为“未执行”并重试,造成同一业务动作执行两次。止损顺序是暂停自动重试、冻结高风险工具并按
request_id对账;长期修复是将idempotency_key、执行台账、显式状态机与补偿任务组成不可绕过的副作用门禁。当前只有演练设计和合成证据,没有真实事故、实测成功率或个人线上处置记录。
目录
- 1. 复盘摘要
- 2. 小白先看懂
- 3. 故障教学图片
- 4. 事实边界与证据
- 5. 影响、时间线与指标
- 6. 技术栈与故障域架构
- 7. 故障与恢复调用流程
- 8. 止损与根因分析
- 9. 长期修复与横向选型
- 10. 验证与防复发
- 11. 面试表达
- 12. 总结
1. 复盘摘要
| 项目 | 内容 |
|---|---|
| 事故类型 | 故障演练,不是真实生产事故 |
| 用户现象 | 同一付款、通知、建单或删除动作出现两次 |
| 直接原因 | 工具成功后响应丢失,编排器自动重试 |
| 根本原因 | 系统没有把“结果未知”建模成独立状态,也没有统一幂等门禁 |
| 临时止损 | 暂停重试、冻结工具、按执行台账对账、人工接管 |
| 长期修复 | 幂等键、执行台账、状态机、补偿与对账任务 |
| 当前状态 | 文档设计完成;故障注入和维护者审图待执行 |
面试官真正关注的不是“知道幂等”这一个词,而是能否区分工具调用失败、响应失败和副作用结果未知,并给出不会扩大损失的恢复顺序。
2. 小白先看懂
想象小王通过代办员订酒店。酒店已经扣款并预订成功,但回执在路上丢了;代办员看到“没有收到成功消息”,又订了一次,于是产生两笔订单。此时最危险的动作不是等待,而是继续点“重试”。
| 生活场景 | 技术映射 |
|---|---|
| 小王的请求 | Agent 任务与业务意图 |
| 代办员 | Agent 编排器 |
| 酒店扣款 | 外部工具副作用 |
| 回执丢失 | 超时、网络中断或结果回写失败 |
| 订单号 | idempotency_key 与外部 effect_id |
| 查酒店订单 | 执行台账和对账查询 |
专业机制上,“调用返回异常”并不能证明“副作用没有发生”。系统必须保留 UNKNOWN 状态,先按幂等键查询既有结果,再选择返回旧结果、补偿或人工接管。
类比没有覆盖并发 Agent、工具自身不支持查询、不可逆动作和跨系统最终一致性;这些边界正是工程验证重点。
3. 故障教学图片
图:故障教学图片|Agent 重复副作用的证据链与幂等恢复
替代文本: Agent 请求经过编排和工具调用产生外部副作用,响应丢失后重试造成重复;request_id、幂等键和执行台账支持止损、状态机与补偿恢复,事实边界为故障演练。
教学图片待人工审图Agent 请求经过编排和工具调用产生外部副作用,响应丢失后重试造成重复;request_id、幂等键和执行台账支持止损、状态机与补偿恢复,事实边界为故障演练 暂不公开,正文与 Mermaid 图可正常阅读。
读图结论: 外部动作返回超时后,第一步是确认副作用是否发生,而不是立即重试。
橙色回路表示“副作用已发生但回执丢失”,证据链负责关联同一业务意图;红色区域先停止风险扩散,绿色区域再恢复状态。图片只建立直觉,精确状态和调用顺序以下文 Mermaid 与表格为准。
图片生成记录: model=gpt-image-2,generated=2026-07-15,prompt_version=v1;查看 Prompt。PNG C2PA 记录 softwareAgent=gpt-image、version=2.0。
质检状态: Agent 已逐字检查标题、标签、箭头与事实边界;维护者人工复核尚未完成,因此保持 generated-awaiting-review。
4. 事实边界与证据
| 结论 | 类型 | 证据或验证动作 |
|---|---|---|
| 工具成功后响应可能丢失 | 工程机制 | 用故障注入在提交副作用后断开返回链路 |
| 缺少幂等门禁会导致重复执行 | 示例假设 | 并发发送相同 idempotency_key 并核对副作用计数 |
| 暂停自动重试可以缩小影响 | 工程建议 | 比较冻结前后的新增重复动作数,不预填数字 |
| 本文描述真实线上事故 | 不成立 | 没有工单、日志、Trace 或业务损失证据 |
合成 Trace 仅用于定义字段,不代表真实请求:
json
{
"request_id": "req-drill-02",
"agent_run_id": "run-drill-02",
"tool_call_id": "call-01",
"idempotency_key": "tenant:order:intent-01",
"effect_id": "effect-unknown",
"state": "UNKNOWN",
"retry_count": 1,
"evidence_boundary": "failure-drill"
}5. 影响、时间线与指标
演练范围覆盖可产生付款、发信、建单、删除和授权变更的工具;只读查询不在副作用影响域,但仍需受超时预算约束。未知项包括具体工具是否原生支持幂等键、查询结果和撤销接口。
| 相对时间 | 事件 | 决策 |
|---|---|---|
| T0 | 工具收到调用并完成副作用 | 外部系统生成 effect_id |
| T0+1 | 返回链路被注入中断 | Agent 记录 UNKNOWN,不得写 FAILED |
| T0+2 | 旧策略触发自动重试 | 演练复现重复风险 |
| T0+3 | 冻结工具并对账 | 根据幂等键查询既有结果 |
| T0+4 | 修复后重放 | 验证返回旧结果或进入补偿 |
核心指标先定义口径:
text
duplicate_effect_rate = 同一业务幂等键对应多个有效 effect_id 的数量 / 已确认产生副作用的幂等键数量
unknown_state_age = 当前时间 - 首次进入 UNKNOWN 的时间
reconciliation_success_rate = 被对账任务收敛到终态的 UNKNOWN 数 / 进入对账的 UNKNOWN 总数本文不填写阈值和改善比例;需要在具体业务的可接受损失、工具 SLA 和人工接管能力下确定。
6. 技术栈与故障域架构
6.1 技术点清单
| 技术点 ID | 技术点/环节 | 类型 | 采用方案 | 链路职责 | 版本/证据边界 |
|---|---|---|---|---|---|
| TP-01 | 副作用身份 | 协议 | 业务级 idempotency_key | 关联同一意图的多次调用 | 演练 Schema |
| TP-02 | 执行台账 | 存储 | 唯一约束加状态记录 | 保存请求、结果与外部 effect_id | 设计稿 |
| TP-03 | 状态控制 | 状态机 | PENDING/RUNNING/SUCCEEDED/UNKNOWN/COMPENSATING/FAILED | 防止把未知结果当失败 | 设计稿 |
| TP-04 | 恢复机制 | 服务 | 对账任务加显式补偿 | 查询外部事实并收敛终态 | 待故障注入 |
6.2 故障域架构
图:架构|Agent 副作用、执行台账与恢复边界
替代文本: 用户请求进入 Agent 编排器,副作用门禁先读取执行台账,再调用外部工具;状态机、对账器和补偿器共享台账,日志与 Trace 贯穿调用链。
图表加载中…
读图结论: 幂等能力必须位于所有副作用工具之前,并以外部业务事实而非 Agent 内存判断最终结果。
7. 故障与恢复调用流程
图:技术调用流程|响应丢失后的停止重试、对账与补偿
替代文本: Agent 用幂等键调用门禁,外部工具完成副作用但返回丢失;门禁记录 UNKNOWN,停止自动重试并由对账器查询外部结果,存在结果则复用,不存在才按策略重试或补偿。
图表加载中…
读图结论: UNKNOWN 是需要取证的独立状态,既不能直接重试,也不能直接宣告失败。
8. 止损与根因分析
止损按风险从高到低执行:暂停高风险工具自动重试;冻结新副作用请求;保留只读查询;导出 UNKNOWN 台账;按外部事实对账;不可逆动作进入人工接管。
| 假设 | 证据 | 结论 |
|---|---|---|
| Agent 规划了两次动作 | tool_call_id 与计划 Trace | 若只有一次计划,排除 |
| 工具第一次没有执行 | 外部 effect_id 与业务记录 | 已存在则排除 |
| 客户端重复提交 | 上游幂等键与请求日志 | 同键请求可定位,但门禁仍应兜底 |
| 响应丢失触发重试 | 工具提交 Span 成功、返回 Span 中断 | 演练主假设 |
- 直接原因: 副作用成功后响应丢失,编排器自动重试。
- 根本原因: 没有统一副作用协议和
UNKNOWN状态,工具各自处理幂等。 - 促成因素: 重试策略只看异常类型,不看副作用阶段;缺少跨系统对账。
- 非原因: LLM 输出随机性不是本演练的直接原因;相同工具参数仍需幂等保护。
- 防线失效: 测试只覆盖“调用前失败”,没有覆盖“提交后响应丢失”。
9. 长期修复与横向选型
| 技术点 ID | 候选方案 | 优点 | 缺点/代价 | 适用场景 | 不适用场景 | 选择结论与依据 |
|---|---|---|---|---|---|---|
| TP-01 | 随机请求 ID | 接入简单 | 重试会生成新 ID,不能表达业务同一性 | 只做追踪 | 幂等控制 | 不采用为幂等键 |
| TP-01 | 业务幂等键 | 能跨重试关联同一意图 | 需定义稳定业务边界 | 有明确业务主键 | 无法识别业务意图 | 默认采用 |
| TP-02 | 内存去重 | 延迟低 | 重启丢失、无法跨实例 | 无副作用的短任务 | 生产副作用 | 仅用于加速,不能做事实源 |
| TP-02 | 持久化唯一约束 | 跨实例、可审计 | 增加存储写入 | 生产副作用 | 极低价值只读请求 | 采用 |
| TP-03 | 成功/失败二态 | 简单 | 无法表达结果未知 | 纯本地原子操作 | 远程副作用 | 不采用 |
| TP-03 | 显式 UNKNOWN 状态机 | 恢复路径清晰 | 状态和迁移更复杂 | 分布式工具调用 | 无恢复需求 | 采用 |
| TP-04 | 自动无条件补偿 | 恢复快 | 补偿也可能重复或不可逆 | 可证明补偿幂等 | 付款、删除等不可逆动作 | 不默认采用 |
| TP-04 | 对账后受控补偿 | 以外部事实为准 | 恢复较慢、需查询能力 | 高风险副作用 | 外部系统完全不可查询 | 默认采用,必要时人工接管 |
10. 验证与防复发
| 注入场景 | 预期行为 | 验收证据 |
|---|---|---|
| 调用前断网 | 可安全重试 | 台账无 effect_id,外部无副作用 |
| 提交后响应丢失 | 进入 UNKNOWN,不自动重试 | 同键只有一个有效 effect_id |
| 两个 Agent 并发同键 | 只有一个执行者 | 唯一约束冲突被复用为既有结果 |
| 对账接口超时 | 保持 UNKNOWN 并告警 | 状态不被误写为 FAILED |
| 补偿任务重复执行 | 补偿本身幂等 | 最终业务状态唯一且可审计 |
防复发资产:CI 增加副作用契约测试;Trace 强制记录 idempotency_key/effect_id/state_transition;告警监控 UNKNOWN 数量与年龄;Runbook 固定“冻结—取证—对账—恢复”顺序;新工具接入必须声明查询、撤销和幂等能力。
难点卡: 最难的不是加一个唯一键,而是在网络结果未知和工具能力不一致时,同时保证不重复执行与可恢复。验证边界是故障注入,不是普通成功用例。
亮点卡: 把重试判断从异常类型升级为副作用状态机,并以外部事实对账;代价是台账、补偿和人工接管流程增加。
反模式: 把所有异常都交给 Agent 自主重试;用 Prompt 提醒“不要重复”代替系统幂等;只记录自然语言思考而不记录业务键和状态迁移。
11. 面试表达
11.1 30 秒
这是一次故障演练:Agent 的工具副作用已成功,但响应丢失后自动重试,可能造成重复付款或建单。我先暂停重试、冻结高风险工具并按幂等键对账,根因是系统没有统一幂等门禁和结果未知状态。长期修复采用执行台账、显式状态机与补偿任务,并通过提交后断链、并发同键和补偿重放验证;当前没有真实事故数据。
11.2 60~90 秒
我把失败拆成调用前失败、调用后失败和副作用已提交但响应丢失三类。第三类不能直接重试,所以 Trace 必须串联 request_id、tool_call_id、idempotency_key 和 effect_id,台账进入 UNKNOWN 后由对账任务查询外部事实。方案没有选择内存去重或成功/失败二态,因为它们不能跨实例恢复结果未知;代价是增加存储和状态治理。验证重点是故障注入后同一业务键只有一个有效副作用,并且不可查询时能转人工接管。
11.3 2~3 分钟展开要点
按“演练边界—工具副作用—响应丢失—取证字段—冻结止损—根因与防线失效—方案横评—故障注入—Runbook”展开。第一人称只用于描述演练方案设计,不能声称处理过真实线上事故。
递进追问:幂等键由谁生成?工具不支持查询怎么办?补偿失败如何恢复?同一业务意图参数变化是否复用同一键?如何处理跨工具事务?为什么 Prompt 不能替代幂等?
12. 总结
一句话记忆: 副作用结果未知时,先对账事实,再决定重试或补偿。
- 调用异常不等于外部动作未发生;
UNKNOWN必须成为显式、可观测、可恢复的状态;- 幂等键、执行台账、对账和补偿共同构成副作用门禁;
- 修复必须覆盖提交后断链、并发重放和补偿重复;
- 本文是故障演练设计,不是已验证生产经历。