Agent重试没做好,一个超时能变成十次扣款

📅 2026/10/27 ✍️ 小文 📖 约 1 分钟

重试是Agent工程里最被低估的风险点。本文用三个真实事故场景说明为什么LLM调用的幂等性比HTTP更复杂,并给出可落地的幂等键设计与状态机写法。

后端工程师对重试都很熟:超时就重试,加上指数退避。但把同一个模式搬到 Agent 系统上,会撞上一个截然不同的问题——

LLM 调用不是无副作用的。 一次“重试”可能触发两次退款、两封邮件、两次库存扣减。

三个真实事故

事故一:超时但已成功

Agent 调用“创建退款工单”工具,工具内部超时 30 秒。Agent 判定失败,重试。第二次成功。

结果:两张退款工单,用户被退了两次。

根因:超时是客户端的判断,不是服务端的事实。 服务端可能已经处理完了,只是响应没回来。

事故二:并发重放

用户连点两下“帮我下单”。前端没有防抖,发出两个请求。两个 Agent 实例同时执行,各自读到相同的库存,各自下单。

结果:买了两份。

事故三:多步链路的中间态丢失

Agent 执行三步:验券 → 扣券 → 发货。第一步成功,第二步超时,Agent 从头重试整个流程。

结果:券被扣了两次。

这个最典型——粒度过粗。重试的粒度必须是“最小副作用单元”,而不是整条链路。

幂等键:不是加个UUID就完了

大部分团队的第一反应是“加个 request_id 去重”。这在单一服务里够用,但在 Agent 场景不够,因为:

1. 幂等键的生成位置很关键。

必须在副作用意图产生的那一刻生成,而不是在 HTTP 请求发起时。如果 Agent 决定“要退款”是一个事件,那幂等键应该绑定这个决策,从决策点到工具调用之间的所有重试都复用同一个键。

推荐:idempotency_key = hash(conversation_id + intent_type + intent_params)

2. 参数必须规范化后参与hash

{"amount": 100} 和 {"amount": "100"} 必须产生同一个键。浮点数要按精度归一(100.00 → 100),数组要排序,缺失字段要补默认值。

这一条不做,去重形同虚设——因为重试时 LLM 可能给出形式上略有不同的参数。

3. 幂等窗口要有边界

去重记录不能永久保存,但也不能只存5分钟。合理值:覆盖一次会话的最长生命周期,通常 24 小时。同时区分“处理中”和“已完成”:

  • 键存在且状态=已完成 → 直接返回缓存结果
  • 键存在且状态=处理中 → 返回 409,让调用方等待而非重试
  • 键不存在 → 执行

处理中 这个中间态是防并发重放的核心,很多实现漏了它。

状态机:把链路拆成可恢复的节点

比幂等键更根本的做法,是让整个流程可恢复而非可重试。

给每个任务维护一个持久化状态:

STEP_1_VERIFY_COUPON   done
STEP_2_DEDUCT_COUPON   done   [key: abc123]
STEP_3_SHIP            pending

失败时不是从头再来,而是从第一个 pending 节点继续。每个节点的完成状态落库,重试只重跑未完成的部分。

这带来的额外好处:你可以知道“卡在哪一步”、可以人工介入补完、可以做部分回滚。

什么工具绝不能自动重试

给工具打三个标签:

风险级别例子重试策略
幂等读查订单、搜文档随意重试
幂等写设置字段为某值(PUT语义)可重试
非幂等写下单、退款、转账、发邮件禁止自动重试,只能人工确认或先查后补

第三类工具,正确的处理不是“重试”,而是先查询状态再决定:

超时 → 查询“该幂等键对应的操作是否已存在”
  ├─ 存在 → 视为成功
  ├─ 不存在 → 安全重试
  └─ 查询也失败 → 报错并转人工

这就是“查询补偿”模式。它把不确定性收敛到一个可控动作上。

Agent 特有的坑:LLM 自己会重试

除了框架层重试,LLM 本身也会重试。当工具返回错误信息时,模型很可能换个参数再来一次。

所以错误信息必须设计成阻止重试的形状:

  • ❌ "Error: transaction failed" → 模型会再试
  • ✅ "该订单已于 2026-10-27 14:02 完成退款(工单号 RF-2931)。请勿重复提交,直接告知用户结果。"

把“不要重试”写进工具返回,比在框架里加限制有效得多。

一条检查清单

上线前对着过一遍:

  1. 每个写操作工具都有幂等键吗?键的生成位置在决策点还是请求点?
  2. 幂等记录包含“处理中”中间态吗?
  3. 是重试整条链路,还是从断点恢复?
  4. 非幂等写工具,超时后的动作是查询还是一律重试?
  5. 工具错误信息的措辞,会不会诱导模型再试一次?
  6. 前端/入口有防连点吗?

重试的正确姿势,本质是承认**“失败”是一个模糊的判断,而“副作用”是一个确定的事实**。系统设计要建立在事实上,不能建立在判断上。

📤 分享到