Part B · 横切深挖 · B08

Retries, idempotency and recovery错误恢复与幂等

口述 2 min 追问 7 层 适合 垂直 Agent 研发, FDE
Prompt 题面

先想清楚再开口

你讲完一个会调用外部工具的 Agent,面试官接着问:「工具返回 429、调用超时、跑到一半进程被重新调度。你的 Agent 会不会把同一张工单开两次、同一封邮件发两次?重启之后它怎么知道自己做到哪一步了?」

题面为本书自拟的训练题,不代表任何公司的真实面试题或真实系统。

约束

约束取值(练习设定)
任务一个采购助理 Agent(虚构):核对需求后在工单系统开采购单、更新内部 ERP 的预算占用、给供应商发确认邮件
工单系统支持在请求里带幂等键,同一个键在保留期内重复提交只生效一次(保留期多长待核实);限流时返回 429 并带 Retry-After
ERP不支持幂等键,但可以按业务单号查询是否已写入
邮件发出去就收不回;发送接口只返回一个消息 ID
运行环境Agent 跑在可被随时重新调度的 worker 上;有一个持久化的数据库可用
代价重复开单、重复占预算要财务人工冲销;重复邮件会让供应商重复备货;漏发邮件只是延误
答到一半再点开:面试官补了一句

「ERP 下个月升级成异步接口:提交只返回 202 和一个任务号,结果几分钟后才能查到。」

Answer 参考回答

先口答,再展开

展开参考回答

该先问的问题

  1. 每个工具的写入是不是幂等的?支持幂等键吗,保留期多长、过期后同一个键会被怎么处理?能不能按业务单号查询结果,写入后多久能查到?这决定每一步用去重、查询还是对账。
  2. 重复执行和漏执行,哪个更贵?这决定结果未知时是倾向重试还是倾向停下来问人。
  3. 有没有持久化存储可以放执行账本?这决定重启后能不能从账本恢复,而不是从对话记录里猜。
  4. 限流是按调用方、按租户还是全局?同时有多少个 Agent 在跑?这决定退避要不要全局协调。
  5. 失败到什么程度要交给人?交接时对方需要看到什么?这决定升级条件和交接包内容。

一句话框架

先把错误分类,再按动作的副作用选策略:读可以放心重试,写要有稳定的操作 ID、先查后重试、查不清就对账或交人;进度记在会话外的执行账本里,重启从账本恢复。

设计取舍表

已决定 生成 op_id,先写账本 已提交 请求带同一个 op_id 已确认成功 记下外部返回 成功返回 明确未受理的 429:按 Retry-After 退避,同一 op_id 重提 明确失败 不可重试 → 交人 结果未知 超时 / 断连 / 模糊 5xx 按 ID 查询 op_id 或业务单号 补偿 自带 op_id 参数错 / 无权限 查到已生效 明确终态失败,或 仍在去重期内:重提 后续失败 对账 / 交人 仍不明:带着账本,不自动重提 刚查不到 / 不明:过对账窗口再查 每次状态变化先写执行账本; 重启后从账本恢复,不从对话记录推断; 租约只管本方;下游无校验时,执行权有争议就停下对账。 确定性步骤(不经过模型) 重复写入的高发点 交给人
一个有副作用的动作的状态机。橙色框「结果未知」是整张图的关键:超时之后不直接重试,先按操作 ID 或业务单号查询;只有下游明确返回终态失败、或仍在下游的去重保护期内,才重提;刚超时查不到不算没生效,过了对账窗口仍不明就交人。
设计点采用否决的备选什么证据会让我改
错误分类按「能不能重试」和「有没有可能已经生效」两个维度分:明确未受理的 429 可按 Retry-After 重试;参数错、权限不足不可重试;超时、连接中断和含义不明的 5xx 都算「结果未知」,单独处理所有错误统一重试 N 次下游文档明确某类 5xx 表示「未受理」时,才把它挪到可重试
退避指数退避加随机抖动,明确未受理的 429 按 Retry-After 等;每个动作有重试次数和总时长上限;同一上游的并发调用共享一个限流预算固定间隔立即重试多个 Agent 同时撞上限流、重试互相放大时,把限流预算提到全局协调
操作 ID在决定执行某个具体动作时生成稳定的 operation_id,先写进账本,所有重试和下游调用都复用它;支持幂等键的接口直接用它去重。本地 ID 和账本只管住自己这一侧,外部副作用只发生一次要靠下游去重、与业务写入同事务的唯一约束,或能查到下游终态每次重试临时生成新键,或用「会话 + 时间戳」拼键下游去重有保留期时,账本记下保护截止时间;过期后(worker 迟迟恢复、人工重放)先按业务单号对账,确认不了就禁止自动再提交
结果未知超时后先按 operation_id 或业务单号查询:查到成功就记成功;只有下游明确返回终态失败,或仍在下游去重保护期内,才重提。刚超时查不到不等于没生效(异步处理、索引延迟都会让它暂时查不到),过了约定的对账窗口再查,仍不明就交人超时就重试;或者「查不到」就重提不改;「超时」和「刚查不到」都不等于「没生效」,这是重复写入最常见的来源
不支持幂等的动作邮件这类发出去收不回的动作,放在流程最后,先在账本记「待发送」再发送,发送后记下消息 ID;结果未知时默认不重发,交人确认(漏发比重发便宜)和其它步骤一样自动重试业务方明确说漏发比重发更贵时,改为重发前先查已发送记录
部分失败已经生效的步骤不回滚成「没发生过」,而是执行事先定义好的补偿动作(冲销预算占用、作废工单),补偿本身也有操作 ID;补偿不了的交人让模型临场决定怎么撤销某个补偿动作的失败率高,把那一步改成先占位、确认后再生效
恢复与升级执行账本记录每个动作的 operation_id、状态和外部返回;重启后从账本恢复,对「已提交、结果未知」的动作先查询。执行权用租约管住本方;只有下游记录并校验单调递增的 fencing token 时,旧 worker 迟到的提交才会被拒(见 How to do distributed locking,Martin Kleppmann,2016-02-08)。本题的 ERP、工单和邮件都没有这种校验,所以外部副作用只能靠下游幂等(工单),或经验证在写入处强制执行的业务唯一约束(ERP 尚未验证);做不到的,遇到结果未知或执行权争议就停下对账,不自动重提。同一动作连续失败或 K 轮内没有进展就停下,带着账本交人重启后把对话记录喂回模型,让它自己判断做到哪了不改恢复方式;升级阈值按误升级率和烧掉的预算调
Two-minute English 口述

两分钟,先结论后展开

展开 2 分钟英文口述稿

First I classify errors along two axes: can I retry it, and could it already have taken effect? Rate limits and brief outages are retryable. Bad parameters and permission errors are not. Timeouts are special: the result is unknown.

For retries I use backoff with jitter, honor Retry-After on a clean 429, and cap attempts and total time. When many agents share an upstream, they share one rate-limit budget, so retries don't amplify each other.

For writes, the key is a stable operation ID, created when we decide to do a specific action and written to a ledger before the call. Every retry reuses it. After a timeout I look the operation up first. An empty lookup right after a timeout doesn't prove it failed, so I resend only on a definite failure or inside the downstream's dedup window; otherwise I check again after a reconciliation window, then hand it to a person.

Actions that can't be undone and don't support deduplication, like sending an email, go last. If the outcome is unknown, I don't resend by default, because a missed email costs less than a duplicate.

Partial failures get predefined compensations, not improvised rollbacks. And recovery reads the ledger, not the chat history. If the same action keeps failing or nothing moves for several turns, the agent stops and hands the ledger to a human.

Follow-ups 追问

一层一层往下问

  1. 为何不用备选为什么不直接把错误信息给模型,让它自己决定要不要重试?
    先口答,再展开要点
    • 模型能读懂错误文本,但它不知道这次调用有没有已经生效,也看不到其它 Agent 正在撞同一个限流。重试与否取决于副作用和全局状态,这些在执行层才有。
    • 让模型决定,还会把重试逻辑变成不可复现的路径:同一个超时,这次重试、下次不重试,事后没法审计。
    • 模型适合的位置是更高一层:同一个动作反复失败后,换一种做法(换查询条件、换工具),或者写交接说明。这一层也受步数和预算上限约束。
  2. 需求变化ERP 改成异步接口,提交只返回 202 和任务号,结果几分钟后才出来。怎么改?
    先口答,再展开要点
    • 状态机多一个「已受理、处理中」:拿到任务号就写进账本,和 operation_id 绑在一起。
    • 之后按任务号轮询或接收回调,轮询也要退避和总时长上限;超过上限仍无结果,归为「结果未知」,走查询、对账或交人。
    • 后续步骤(发邮件)要等 ERP 结果确认后才执行。流程不能因为拿到 202 就当成功往下走。
    • 提交本身也可能超时:没拿到任务号时,先按业务单号查询是否已受理。ERP 不支持幂等,异步受理后可能要一段时间才查得到,所以刚查不到不重提,过了对账窗口再查,仍不明就交人。
  3. 失败反证工单系统明明支持幂等键,线上还是出现了重复工单。可能是哪里出了问题?
    先口答,再展开要点
    • 最常见的是键不稳定:每次重试都新生成一个键,或者重启后没从账本读回原来的键,等于没用幂等。
    • 其次是保留期:去重记录被清除后,同一个键可能失去保护;过期后是报错还是当成新请求,取决于供应商契约和业务唯一约束。有的接口文档明确写了后者(例如 Idempotent requests,页面未标日期,2026-10-16 查阅:API v1 的键至少保留 24 小时后可被清除,清除后重用会生成新请求)。所以账本要记保护截止时间,过期后先对账,确认不了就不自动再提交。
    • 第三种是「刚查不到就重提」:第一次提交超时,立刻按业务单号查显示不存在,于是重提;20 秒后第一次提交才入库。刚超时查不到不等于没生效,要过对账窗口再查。
    • 还有一种是同一个动作被两个 worker 同时执行:调度器以为前一个 worker 死了,又派了一个。单靠租约不够,旧 worker 恢复后可能还以为自己持有租约。fencing token 只有在下游校验它时才有用;本题的接口不校验,所以工单靠它的幂等键兜底;ERP 只说能按业务单号查询,能查询不等于写入时强制唯一,两个 worker 可能同时查到「没有」再各自提交。只有验证过 ERP 在写入处对业务单号强制唯一,才能据此防重;否则遇到结果未知或执行权有争议,就停下对账,不自动重提。
    • 排查时按 operation_id 把两张工单的请求日志对齐,看键是否相同、间隔多长、来自哪个 worker。
  4. 运行大促那天几十个 Agent 同时撞上工单系统的限流,重试把情况越弄越糟。怎么办?
    先口答,再展开要点
    • 这是重试风暴:每个 Agent 各自退避,但同时开始、同时重试,负载一波一波打上去。
    • 抖动让重试时间错开;更根本的是共享限流预算,所有 Agent 从同一个令牌桶取配额,拿不到就排队而不是重试。
    • 再加熔断:上游连续失败超过阈值,暂停新的写请求一段时间,只放少量探测请求。暂停期间新任务进队列,状态写明「等待上游恢复」。
  5. 恢复进程在「开完采购单、还没更新 ERP」的时候被重新调度。新 worker 接手后怎么做?
    先口答,再展开要点
    • 从执行账本读状态:采购单那一步是「已确认成功」,带着工单号;ERP 那一步是「待执行」或「已提交、结果未知」。
    • 已确认的不再重做;待执行的用账本里原有的 operation_id 执行;结果未知的先按业务单号查 ERP,刚查不到不重做,过对账窗口再查,仍不明交人。旧 worker 可能迟到提交:下游校验 fencing token 时会被拒;本题的接口不校验,所以恢复时要以下游查询结果为准,查不清就停下对账。
    • 不从对话记录推断进度。对话记录可能在压缩时丢了细节,也可能记录了模型「打算做」但实际没做的事。
  6. 补偿采购单开了、预算也占了,最后发现供应商在黑名单上。怎么撤?
    先口答,再展开要点
    • 执行事先定义好的补偿:作废工单、冲销预算占用。每个补偿动作也有自己的 operation_id,同样走查询和重试规则。
    • 邮件如果已经发出,补偿只能是再发一封更正邮件,这一步交人确认措辞。
    • 补偿失败或补偿不了的,带着账本交给财务或采购的负责人,写明哪些已生效、哪些已撤、哪些待处理。
  7. 评估你怎么证明这套恢复机制真的管用?
    先口答,再展开要点
    • 故障注入:在测试环境里按比例注入 429、超时、连接中断和进程被杀,每个关键步骤前后都杀一次。
    • 看结果而不是看日志:跑完后按业务单号对账,统计重复开单、重复扣预算、漏执行、重复邮件的次数,目标是零重复写入,漏执行都进了对账或交接队列。
    • 另外统计恢复成功率、平均恢复时间和误升级率。每次改重试逻辑前后都跑一遍。
Failure Modes 失败模式

这些地方最容易栽

幂等键在重试时变了
  • 症状:接口声称支持幂等,线上仍然出现重复工单;两张工单的请求带着不同的键。
  • 根因:键在每次调用时临时生成,或者重启后没从账本读回;幂等只在键稳定时才起作用。
  • 防线:决定执行时生成 operation_id 并先写账本,所有重试和恢复都从账本读;测试里专门杀进程再恢复,检查键是否一致。
超时被当成失败
  • 症状:偶发的双份采购单、双倍预算占用,时间上总是紧挨着一次超时。
  • 根因:超时之后直接重试,或者刚查不到就重提;而第一次请求其实已经生效,只是响应没回来,或者还没入库。
  • 防线:超时、断连和含义不明的 5xx 归为「结果未知」,先按 operation_id 或业务单号查询;只在明确终态失败或仍在下游去重期内才重提,否则过对账窗口再查,仍不明就交人。
从对话记录恢复进度
  • 症状:重启后 Agent 重复做了已经做过的步骤,或者跳过了其实没做成的步骤。
  • 根因:进度只存在对话里。对话被压缩后细节丢失,模型「说过要做」和「真的做了」混在一起。
  • 防线:进度以执行账本为准,每个动作的状态和外部返回都落库;恢复时对「结果未知」的动作先查询再继续。
重试风暴
  • 症状:上游一限流,失败率反而越来越高,恢复时间被拉长。
  • 根因:各 Agent 独立重试,没有抖动,也没有共享预算,重试流量叠加在正常流量之上。
  • 防线:退避加抖动、共享限流预算、熔断;重试次数和总时长都有上限,超过就排队或交人。
Cross-references 回链

原理去哪里补

  • AI 6.6 提交点、稳定 operation_id 与幂等重试 · AI 系统工程手册
  • AI 6.5 上游故障、限流与降级策略 · AI 系统工程手册
  • AI 4.2 循环何时该停:步数与预算上限 · AI 系统工程手册
  • DV ch16 调试与错误处理 · CCDV 备考手册
  • DV ch12 工具实现:错误如何回传给调用方 · CCDV 备考手册
  • AR ch14 可靠性工程:重试、升级与人工介入 · CCAR 备考手册
  • A01 改代码的 Agent:反复失败的测试何时停下
  • A03 客服退款:operation_id 与账本原子提交的具体例子
  • A04 失败时保留原件与未判条目,按时限交人
  • A05 浏览器 Agent:点击后页面没反应,是没点上还是已提交
  • A06 长任务:账本、预算与打转检测
  • B02 Tool 接口设计:错误类型怎么回传给调用方
Your Turn 本人填写

只写真实经历

只写真实经历,没有就空着,不编。公开版只有空表;填好的版本存在自己的私用练习记录里。

场景当时发生了什么我亲自做了什么(团队 / AI 做了什么)怎么发现的,结果和证据之后怎么防
重试或重跑导致重复写入
进程或会话中断后丢了进度

练习记录

日期口述用时脱稿?答不上的追问下一轮修正(≤2)