```html Agent退款重试致双退款:审核修正 Agent退款重试致双退款:审核修正
大多数Agent演示都走一条顺路:模型挑一个工具,工具返回200,Agent继续往下走。生产环境不是这样。生产环境里,工具调用有时候什么都不返回。
![]()
问题就出在这里——一个正确的决策,变成了一个错误的结果。从“决定要做”到“真的做完”,中间隔着一道沟。
设想一个再普通不过的场景。Agent处理一张客服工单,判断该给客户退款,于是调用 POST /refunds。支付服务处理了退款,写进自己的账本,开始往回发响应。网络抖了一下,Agent的HTTP客户端撞上超时,抛出一个错误。
在Agent看来,这次调用失败了。在支付服务看来,它成功了。Agent的循环看到错误,最自然的动作就是重试。
模型的推理没有任何问题,它选对了动作,参数也对。更好的提示词、更大的模型、更聪明的规划器都救不了这个场景,因为失败发生在推理结束之后。
把超时当成“没发生”
超时只说明一件事:你没拿到答案。它不说明动作没有执行。把工具错误统一包装成“工具失败了,决定下一步怎么办”的Agent框架,把三种完全不同的情况压成了一种:
- 请求根本没到达服务
- 请求到达了,被拒绝
- 请求到达了,成功了,但响应丢了
只有前两种可以放心盲目重试。
缺少幂等键
如果Agent带了幂等键,第二次退款调用会变成一个空操作,直接返回第一次的结果。关键在于这个键由谁生成、存在哪。它必须在第一次尝试之前由应用生成,并在同一意图的每次重试中复用。如果让模型来生成,或者每次尝试都重新生成,它什么都保护不了。
代码里的重点不是try/except,而是那个键的生命周期:意图记录时创建一次,随意图一起存储,重试时原样带回去。成功之后标记confirmed,超时之后标记unknown——注意,不是failed。
状态只活在上下文窗口里
如果“我已经尝试给订单4812退款”这条记录只存在于对话历史中,那么一次崩溃、一次上下文截断、一次worker重启,它就没了。意图必须在执行之前写入持久化存储,这样无论哪个进程接手,都能看到它。
没有“结果未知”这个状态
多数动作模型只有两个终态:成功和失败。可靠的动作模型至少需要四个:
- submitted:已发出,还没有答复
- confirmed:下游系统已确认
- failed:下游系统明确拒绝
- unknown:说不清楚
unknown才是真正重要的那个。它应该拦住自动重试,转而触发对账。
没有对账路径
动作落进unknown之后,得有东西去查真相:用幂等键或订单ID去问支付服务,和账本比对,然后把意图推进到confirmed或failed。这可以是一个后台任务、一次定时检查,或者高价值动作的人工队列。它不能是Agent自己判断“大概没事”或者“大概失败了”。
换个角度看这件事
Agent的工具调用,本质就是远程过程调用,只不过由LLM来决定调哪个过程。不可靠网络上做RPC的那套老知识全都适用——幂等、持久化的意图日志、显式的不确定性、对账。
干净的分工是这样的:模型提出动作,应用拥有执行状态——键、意图记录、状态、重试策略。
“再试一次”对读操作是个好默认值。对任何会改变状态的操作,只有在你确认第一次没有落地,或者下游系统保证重试不会重复生效之后,它才是安全的。
自建还是集成
如果你已经在支付或订单流程里跑着幂等键和outbox/账本模式,那基本只需要把Agent的动作路由进同一套机制,而不是让Agent直接调API。如果没有,那就是该先建(或先采用)的那块东西——在给Agent任何能动钱、动数据的写权限之前。
你们的Agent工作流里,“我们不知道它到底发生没发生”这种情况是怎么处理的?是拦住重试、对账,还是升级给人工?
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.