上个月中旬,财务同事在工单系统里圈我,问我们是不是新上了什么"温馨提醒轰炸"功能——同一位客户,一个下午收到了七条一模一样的缴费提醒,措辞都懒得换。我第一反应是有人手滑连点了发送,翻了操作日志,发送记录里干干净净只有一条。调出 Agent(智能体,让大模型自己规划步骤、调用工具干活的系统)的执行轨迹,我盯着看了半分钟:模型给短信网关发的那条调用指令,重试了六次。
网关那边其实第一次就发成功了,只是响应慢了点,我们这边等了两秒就判定超时,重试逻辑自动补了一枪。这一补,就是六条短信。
这事说出来挺好笑,但当时我后背是发凉的。发短信算轻的,顶多客户被骚扰,赔个不是。可我想起这个 Agent 还接了别的工具:能创建工单,能改客户备注,还能调用退款接口。发短信重复了是骚扰,退款重复了是什么?同一位客户账户里躺着一笔双倍退款,等财务月底对账才发现——光是想想就睡不好。写惯了业务系统的人对这些词汇不会陌生:创建订单、扣款、发消息,这一类"执行一次和执行两次结果完全不一样"的操作,业界统称非幂等(non-idempotent,指同一个操作重复执行会产生额外副作用)。而我们大多数人把模型接上这些工具的时候,压根没想过"重试"这两个字会和它们撞在一起。
模型为什么会让工具重复执行?说穿了不神秘。模型侧的超时重试,只是最常见的一条路。我后来把能想到的都列了一遍:网关响应慢,重试一次,这怪网络;工具返回的结果带了个不常见的错误码,模型把它理解成"没成功",自己再调一次,这怪模型;最冤的是第三种——我们自己把重试写了两层,框架层一次、业务层一次,网络抖一下,两次重试接力跑,客户就收到两条短信。还有更隐蔽的:模型看到工具返回异常,出于"把任务完成"的好心,换个措辞再试一回,报错文案变了,业务上却还是那一次重复。
哦对了,岔开说一句,别以为只有 Agent 会犯这种错。我们祖传的支付系统里,重试捅娄子的事故每年都有人写复盘文档,幂等键(idempotency key,给同一个业务操作发一张唯一编号的凭证,服务端看到相同编号就不再重复执行)这个发明,就是被这种事故逼出来的。问题在于,业务系统里这套纪律是老工程师一代代传下来的,而大模型对这些纪律一无所知——它只看见"工具没返回成功",就本能地再敲一次门。
想明白这一层,解法其实就摆在桌面上,而且大半是从支付行业抄来的作业。头号方案是给每个有副作用的工具加幂等键:Agent 每次生成一次调用,就带一个唯一编号,服务端拿编号去重,同一个编号第二次进来直接返回上次的结果,一句话都不多说。模型再怎么手抖,数据库里也只有一条记录。Stripe 这类支付平台的 API 就是这么设计的,你在请求头里带上密钥,同一次业务操作重发一百遍,也只扣一次款。
不过这里有个容易想当然的坑,我第一次设计的时候就在这摔的:幂等键得跟着"业务动作"走,不能跟着"请求"走。我最初把幂等键放在了网关的请求层,以为网关能拦住重复——结果模型换了个措辞重新发起调用,请求内容变了,网关判定是新请求,照样放行。真正的做法是在模型的规划层就生成动作 ID,一次"给客户 X 发提醒"的意图,从头到尾只有一个编号,重试、换措辞、跨流程,全都认这一个。
兜底手段也得跟上。我在每次工具调用前后都落一条审计日志(audit log,记录谁在什么时间对什么数据做了什么操作的流水),跑了个小脚本盯着"同主体同动作短时间内的重复调用",一有苗头就报警。有这个兜底,幂等键万一因为 bug 失效,至少能当场拦住损失,而不是等客户投诉或者财务对账。退一步讲,就算不改代码,把"创建类"工具的返回设计得啰嗦一点也有用——明确告诉模型"已创建成功,编号是 12345,请勿重复提交",模型收到明确成功信号后,手抖的概率小得多。
说来有趣,把这套东西铺完之后,我们组里反而冒出一个新争论:这些防重复的纪律,到底该焊死在 Agent 框架里,还是该写进给模型的提示词里,指望它自己"懂事"?焊框架里稳,但每个新工具都得登记属性,工程量不小;写提示词里轻巧,可模型总有不听话的时候——安全这件事上,"通常没问题"约等于迟早出问题。我们目前是三七开:涉及钱和对外消息的,一律框架强制,提示词只是锦上添花。
回头看财务同事那七条短信,损失不大,却是我今年上过最贵的一课:大模型把工具用起来的那一刻,它就成了你的一个不会看规章制度的实习生——热情、勤快、指哪打哪,但没人告诉它"扣款按钮按一次就够了"。你不能怪实习生手快,插座得你自己装保险丝。
你们的 Agent 接了几个会"动真格"的工具?退款、发消息、删数据这种,重试的时候打算怎么防重复——靠幂等键、靠审计报警,还是赌自己网络一直稳定?评论区聊聊,我挺想知道有多少人和我一样,是收到第七条短信才想起来装保险丝的。