Agent 生产事故 #001:Retry 为什么会导致重复下单?
凌晨两点,客服群里开始有人截图。
同一个用户,同一个购物车,出现了两张订单。金额一样,收货地址一样,创建时间差 18 秒。业务同学第一反应是接口重复提交。后端第一反应是前端按钮没防抖。Agent 团队看了 trace,沉默了几分钟。
案发链路是这样的:
用户:帮我把购物车里的东西下单
↓
Agent 调用 create_order(cartId=998)
↓
HTTP client 30 秒超时
↓
Tool Runtime 返回 TIMEOUT
↓
Agent 推理:订单创建失败,重试一次
↓
再次调用 create_order(cartId=998)
↓
两张订单
这不是模型'突然发疯'。它只是按你给它的观察继续行动。
你告诉它:工具超时了。你没有告诉它:这个请求可能已经到达服务端,也可能已经创建成功,只是响应没回来。你也没有给它 idempotency key,没有 tool ledger,没有恢复查询,没有把这次失败标成 SIDE_EFFECT_UNCERTAIN。
所以它做了一个很像人的判断:失败了,再试一次。
坏就坏在这里。
Retry 本来是可靠性手段
在普通后端系统里,retry 很常见。
网络抖一下,重试。上游 503,退避重试。模型 provider 短暂限流,等几秒再请求。读缓存失败,换节点。大多数工程师都写过这种代码:
async function retry<T>(fn: () => Promise<T>, maxAttempts = 3) {
let lastError: unknown;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
await sleep(500 * attempt);
}
}
throw lastError;
}
这段代码在很多地方没问题。
但它有一个隐藏前提:你重试的操作要么没有副作用,要么有办法识别'这是同一个业务请求'。
HTTP 规范里,GET、HEAD、PUT、DELETE 这类方法被定义为幂等方法。注意,幂等不等于'没有副作用',而是同一个请求执行多次,预期效果和执行一次相同。GET 理论上安全,PUT 通常可以重复设置同一个资源状态,DELETE 重复删除同一个资源也应该得到相同结果。
POST 就麻烦得多。创建订单、发邮件、扣款、发券、创建 issue、合并 PR、执行数据库脚本,很多都是 POST 风格的动作。重复一次,世界真的会多一个东西。
Agent Tool Runtime 的坑在于:模型看不见这些语义。它只看见一个工具名和一段 observation。
如果 observation 只有:
{ "ok"

