AI Agent 工具调用幂等与防重入生产事故复盘:从重复下单到有界副作用
技术主题:AI Agent(人工智能代理)
内容方向:生产环境事故的解决过程(故障现象、根因分析、解决方案、预防措施)
引言
Agent 上线后,我们遭遇了令人心跳加速的一次事故:在网络抖动与模型重试叠加的场景下,Agent 对“创建订单”工具产生了重复调用,导致少量用户被重复下单与扣款。本文从现场入手,复盘故障、定位根因,并沉淀出一套围绕“幂等、防重入、可观测与补偿”的工程化改造方案。
一、故障现象与影响
- 现象:
- 部分会话在 30~90 秒后出现“补发”行为;
- 同一用户、同一请求内容,被多次调用 create_order 工具;
- 日志中存在同一 trace_id 下的多条工具调用记录。
- 影响:
- 重复下单率峰值 0.3%(分钟级);
- 造成小额重复扣款与库存冻结异常;
- 事故窗口约 17 分钟,涉及请求 ~2.3k。
二、排查步骤
- 还原调用链:按 trace_id → 回溯 LLM 输出 → 工具执行日志 → 订单服务;
- 识别触发条件:高延迟网络 + LLM 超时重试 + 工具侧无去重;
- 核对执行语义:模型输出中确有“确认再次下单”的误导 token;
- 数据指纹:同一用户/商品/金额/时间窗高度相似;
- 对照非幂等型工具:转账、扣款、发货也存在潜在风险。
三、根因分析
- 幂等缺失:工具层未要求 idempotency_key;
- 防重入缺失:同一会话并行分支可能同时触发有副作用的工具;
- 重试语义不区分:LLM/网关的超时重试带来“多次到达”;
- 缺观测:缺少“副作用工具调用账本”,难以及时止损。
四、解决方案(总体思路)
- 协议层:强制所有具副作用工具携带 idempotency_key(基于业务主键或稳定哈希);
- 执行层:在工具网关实现去重存档(SETNX/唯一索引)+ TTL;
- 并发层:会话内的工具调用加围栏令牌(fencing token)与排他锁,防止并行重入;
- 重试层:区分“重放”与“重试”,确保幂等返回旧结果;
- 观测与补偿:建立调用账本与 Saga 补偿,支持回滚/对账。
五、关键代码(Python)
以下示例展示:
- 生成幂等键并在请求链路中透传;
- 用 Redis 实现去重(SETNX + TTL)与结果缓存;
- 围栏令牌(单调递增版本号)避免并发重入;
- 工具网关统一封装与观测。
1 | # pip install redis==5.* |
要点:
- 去重用 SETNX(或数据库唯一索引)比客户端内存判断可靠;
- 结果缓存让重复到达“拿旧结果”,避免再次产生副作用;
- 围栏令牌可与下游资源协调(例如队列/锁)避免并发重入;
- 工具自身也要基于业务主键幂等(双保险)。
六、验证与观测
- 压测:30% 丢包 + 高延迟条件下,重复到达被 100% 去重;
- 观测:暴露指标——重复抑制率、幂等返回比例、平均延迟、首到达失败率;
- 回放:基于调用账本可以重放单次调用并校验返回稳定性;
- 对账:将 create_order 与支付、库存对齐,发现并自动补偿异常。
七、预防清单
- 工具分类:副作用型工具必须启用幂等键;
- 协议约束:LLM 输出 Schema 中强制 idempotency_key 字段;
- 超时与重试:超时-重试-幂等语义成套联动;
- 并发与锁:会话内串行化或围栏令牌 + 排他锁;
- 账本与补偿:建立调用账本与 Saga 补偿队列,异常自动对账。
总结
本次事故的根源不是“LLM 乱说话”,而是工程约束缺位:缺幂等、无防重入、无账本。将幂等键、去重存档、围栏令牌、结果缓存与观测补齐后,Agent 的副作用被严格约束在可控范围内,重复到达也能稳定“拿旧结果”。这套改造不仅止血于下单,也适用于转账、发货、通知等所有具副作用的工具调用场景。