GPTs 调用第三方 API 失败后,重试策略怎么设置才能避免重复扣费?
GPTs 本身不提供重试次数或退避曲线配置,避免重复扣费要靠三处配合:Actions 的 OpenAPI Schema 声明幂等键与可重试错误码,后端 HTTP 客户端只对 429 与 5xx 做有限次指数退避,扣费侧以幂等键建唯一约束、只在业务最终成功时结算。上游接口不支持幂等语义时,这套做法只能退化为本地去重加对账。
GPTs 本身没有可以填写重试次数、退避曲线的配置项,要避免重复扣费,必须把策略分别落实在三处:Actions 的 OpenAPI Schema 里声明幂等键和可重试错误码,后端 HTTP 客户端只对 429 与 5xx 做有限次指数退避,扣费侧用幂等键建唯一约束、只在业务最终成功时结算。只改其中一处都不够,缺了幂等键的重试本质上是在赌上游不会重复执行;如果上游接口本身不支持幂等语义,这套办法也不成立,只能退化为本地去重窗口加人工对账。
重复扣费在 GPTs 场景里有两种,一种在模型侧,一种在业务侧,混在一起谈必然漏掉一半。模型侧指每次重发请求,整段上下文会被重新计费,重试三次就是三次 token;业务侧指订单、支付、额度被重复创建或重复扣减,钱和账都多了一笔。前者靠减少无谓重试和压缩请求体控制,后者靠幂等键和数据库唯一索引根治,两条链路要分开设计、分开验收。
可重试与不可重试的边界必须先划清,否则退避逻辑越勤奋,重复扣费越多。429、500、502、503、504 以及连接超时、读超时属于可重试,因为它们是瞬时故障,重发有机会成功;400、401、403、404、409、422 属于不可重试,因为请求本身有问题,重发只会拿到同样的错误,却可能已经在上游产生了一次副作用。做法是在 HTTP 客户端里维护错误码白名单,不在白名单内的一律直接返回,并把结构化的 code 与 message 交给模型,让它在对话里改写参数或提示用户。
退避参数要服从 GPTs 的会话节奏,而不是照搬后台服务的经验值。建议 300 到 500 毫秒起步、按 2 倍递增、最多 3 次、总耗时压在 30 秒以内,并加入随机抖动。原因是 Actions 调用有等待上限,退避太久用户看到的是超时,而超时之后模型很可能在同一轮对话里再发起一次调用,实际重试次数会翻倍;抖动则是为了避免并发高峰时所有请求同时打满上游配额。写操作建议只重试一次甚至不重试,恢复动作交给幂等重放,而不是靠不停重发。
幂等键是避免业务侧重复扣费的唯一可靠手段,必须在客户端生成,而不是由服务端在收到请求后再生成。标准做法是每次工具调用生成一个唯一请求号,放在 Idempotency-Key 或 X-Request-Id 头里随请求下发,服务端把它和请求指纹、处理状态、返回结果一起入库。重试请求带着同一个键进来时,服务端直接返回上次的结果,不重复执行、不重复扣费,这叫重放而不是重做。键如果由服务端生成,每次重试都是新键,幂等就形同虚设。
幂等键里必须包含足够区分请求的要素,并且保留期要覆盖整个重试窗口。推荐把租户标识、业务单号和请求体参数的哈希值拼进指纹,避免不同用户在不同会话里复用同一个键造成误命中;保留期以 24 小时为宜,短了覆盖不住跨会话的补偿重试,长了存储成本上升且容易误伤正常的二次下单。幂等键只需强制加在 POST、PUT、PATCH 这类写操作上,GET 天然幂等,不必额外处理。
GPTs 还有一层特有的风险:模型会在调用失败后自己决定是否再试一次,而这个决定不受你控制。所以 Instructions 里要明确写清失败处理顺序,例如先读返回的 error code,不可重试的错误直接向用户说明,不要立刻重新调用,也不要换个参数绕过去。同时把 Schema 里每个 operation 的响应结构定义清楚,让模型能区分业务失败和传输失败。测试面板里的重复点击也会产生真实调用,联调阶段就要按正式流量对待。
扣费侧的正确姿势是两阶段:先冻结或预扣,业务最终成功后再结算,失败或超时走释放。这样即使请求被重试多次,用户看到的也只有一笔冻结,最终只落一笔结算。数据库层面给请求号加唯一索引,从存储层杜绝重复流水,比在代码里判断是否存在更可靠,因为它能挡住并发下的双写。对账时用请求号做关联键,把调用日志、扣费流水、订单三条数据串起来,出现一调多扣就能立刻定位到是哪一层没去重。
模型 token 侧的浪费主要来自重试而非单价,减少重试必要性比优化重试算法更有效。给每次调用设置合理的 max_tokens,用流式读取,收到首个 token 即视为成功,避免因为尾部超时而整段重发;失败重试时不要原样重放整段长上下文,能截断就截断、能摘要就摘要。上游返回 429 时优先降并发、排队,而不是立刻重试,配额被打满时的重试成功率极低,白白多烧一遍输入 token。
如果调用链路中间还有一层 API 中转或聚合网关,必须先问清它对失败请求是否代为重试、重试几次、按几次计费。否则你的客户端重试 3 次,网关再各重试 3 次,上游实际收到的是 9 次调用,账单自然对不上。可接受的配置是网关透传不重试、只做路由与鉴权,并把请求号原样传给上游;如果网关必须重试,就要求它复用同一个幂等键,并保证重放结果一致。
验收不能靠感觉,要能构造故障并观察计数。常见做法是在测试环境对目标接口注入 503 与超时,分别跑单次调用和带重试的调用,然后核对三件事:上游实际收到几次请求、扣费流水生成几条、订单表新增几行。理想结果是请求多次而流水一条。上线后把请求号、重试次数、最终状态打进同一条日志,配一个周期性对账脚本比对调用量与扣费条数,偏差超过阈值就告警,重复扣费才有机会在用户投诉前被发现。
这套策略也有明确的适用边界,滥用反而增加复杂度。纯查询类接口不需要幂等键,只需要限制重试次数;上游明确不支持幂等键的接口,例如部分短信、部分支付渠道,只能在本地维护去重窗口,窗口之外的重试无法从原理上保证不重复,此时应改为对账补偿而非继续重试。反过来,只要业务涉及金额、额度、库存、名额这类不可逆变更,重试就必须带幂等键,宁可让这一次调用失败返回,也不要让它在后台悄悄执行两遍。