一次点击可能触发多次模型请求,但“连接失败”本身不能证明重复扣费。 排查费用上涨时,先数清 SDK、业务代码和队列各自发了几次请求,再逐次对照用量记录;涉及发信、写订单等动作时,还要独立防止重复执行。
本文适用于服务端调用 LLM API 的排错。官方资料复核日期为 2026-10-06。下面的次数、时间预算和任务记录均为教学假设,不是本站付费推理结果、厂商限额或省钱承诺。只需要估算正常用量时,先看 API 成本预算;这里专门处理失败后的重发与对账。
先区分请求、费用与业务动作
把一次“生成并发布商品说明”拆成三件事:向模型请求文字、验收结果、把结果发布到商店。三者需要分别计数。
| 观察到的现象 | 还缺什么证据 | 下一步 |
|---|---|---|
| 页面只点了一次,后台调用很多次 | 每层的实际尝试数 | 画出调用链,找出谁在重试 |
| 客户端超时,没有拿到完整答案 | 服务端是否受理、是否有用量 | 查请求标识及供应商记录,费用暂记未知 |
| 拿到了两份答案 | 两次请求的实际计费记录 | 分别对账,不能只保留最后一次成功 |
| 商店出现两份相同商品说明 | 发布动作的业务标识及执行记录 | 修复业务去重,不能只调模型重试参数 |
响应丢失、业务重复和计费异常可能一起出现,也可能各自发生。不要用“最终只展示了一份答案”证明后台只执行了一次。
数清三层重试如何放大请求
从入口向外检查:队列或定时任务 → 业务重试循环 → SDK → 代理或供应商。同时记录 SDK 版本、显式配置和默认配置,不能只搜索自己写的 retry。
一个假设的最坏路径:SDK 最多尝试 3 次,业务代码最多调用 SDK 3 次,队列最多投递 2 次;如果每层都走完失败分支,出口最多发出 3 × 3 × 2 = 18 次请求。这里所有数字都包含第一次尝试。它不是 18 次已扣费的证明;实际费用仍需逐次核对。
参数名也可能造成误读。当前 Claude 错误文档 说明官方 SDK 默认会重试两次,max_retries 可配置或禁用;AWS SDK 重试文档 的 max_attempts 包含第一次请求。AWS 页面还注明部分新行为需要选择启用,不能将它的默认值套到其他 SDK。
先填自己的表,再改配置:
| 层级 | 谁负责 | 总尝试上限是否包含首次 | 失败是否交给下一层 | 实际观察证据 |
|---|---|---|---|---|
| 队列 / 调度器 | 任务平台 | 待查 | 待查 | 投递 ID、任务状态 |
| 业务代码 | 应用 | 待查 | 待查 | 业务任务 ID、尝试序号 |
| SDK / HTTP 客户端 | 所用版本 | 待查 | 待查 | 出口请求数、重试设置 |
| 代理 / 路由层 | 网关 | 待查 | 待查 | 请求记录、最终处理方 |
看不到网关内部次数时写“未知”,不要把客户端次数当成完整链路次数。使用聚合接口时,还要检查 供应商与模型回退边界。
根据错误含义决定是否重发
下面是建议的应用策略,需结合所用接口的错误类型和正文,不能只匹配 HTTP 数字。
| 情况 | 默认处理 | 恢复前要确认什么 |
|---|---|---|
| 参数、身份或权限错误 | 停止原样重发 | 修改请求或权限后作为明确的新尝试 |
| 明确的临时限流 | 在总预算内等待后重试 | 服务端等待提示、剩余时间、并发是否过高 |
| 余额或额度耗尽 | 暂停任务并报告 | 额度恢复;循环等待不等于恢复 |
| 临时服务故障 | 有上限地退避重试 | 操作可安全重放,且次数与时间均未耗尽 |
| 连接错误或读超时 | 结果可能未知 | 请求是否已受理、是否已有可查询结果 |
| 流式输出中断 | 标记答案不完整 | 重新生成的内容不可直接接在旧输出后充当完整答案 |
Claude 文档也提醒:429 可能涉及支出上限;流式响应在 HTTP 200 之后仍可能报错。因此,“所有 429 都重试”和“200 就完成任务”都不足以作为验收规则。具体接口的 错误说明 应与日志一起查看。
让一个层级管理总次数和截止时间
对需要跨队列记录的业务,可以让应用层统一管理,并关闭内层隐式重试;简单调用也可以由 SDK 管理,但外层不能再无条件循环。关键是同一业务任务只有一份可核对的总预算,队列重新投递后不会从零开始。
例如为一个交互任务约定“最多 3 次出口尝试、从入队起最多 45 秒”。这是示例策略,长推理任务需要另定预算,不应照抄。实现时逐项落实:
- 在发送前,原子地检查并占用一次尝试额度,把截止时间与计数持久化。并发消费者不能各自领取完整预算。
- 按错误类型判定是否允许继续;对于结果未知且不可安全重放的操作,转入核对状态。
- 等待时间采用带随机抖动的退避,并尊重有效的服务端等待要求。若要求的等待超过剩余时间,结束本轮,而非缩短等待抢先重发。
- 每次发送的超时不超过剩余任务时间,等待结束后再检查截止时间。客户端取消不代表供应商已停止处理或不计费。
- 明确输出“完成”“失败”或“待核对”,保留尝试记录。用户手动重试不能绕过尚未核对的业务动作。
次数和时间预算只是请求控制,不是货币硬限额。要保证金额约束,还需要供应商能力、并发预留、用量结算和超额停止机制;不能从“最多三次”推导“绝不会超过某个金额”。
用三种标识对账,不把未知写成零
业务任务 ID 在同一任务重试时保持不变;每一次出口请求另有尝试 ID;收到供应商请求或 generation ID 后再关联它们。不要记录 API 密钥或不必要的原始用户文本。
下面是虚构的日志结构示例,不是调用结果:
| 业务任务 | 尝试 | 客户端结果 | 供应商标识 | 用量 / 费用状态 | 业务发布 |
|---|---|---|---|---|---|
| task-demo | attempt-1 | 读超时 | 未收到 | 未知,待核对 | 未执行 |
| task-demo | attempt-2 | 返回并通过校验 | 示例标识 A | 待供应商记录确认 | 已完成 action-demo |
| task-demo | 队列再次投递 | 命中已完成任务 | 不发新请求 | 无新增请求;之前未知仍待核对 | 返回既有结果 |
第二次成功不能抹去第一次的未知费用。如果知道 OpenRouter 的 generation ID,可通过 Generation 查询接口 查询 GET /api/v1/generation?id=<generation-id>,核对 model、provider_name、total_cost。没有 ID 时,先保留时间窗、账户和客户端记录,使用有权限的后台或支持渠道核对,不编造 ID。
同一供应商记录只记账一次,但同一业务任务下不同的模型请求都要保留。分别统计总请求数、未知费用条目数、已确认费用和验收完成的任务数;还有未知项时,不报告“完整任务成本”。
业务幂等需要存储和恢复流程
给日志加一个任务 ID 不会自动去重。对于“生成后发布”的动作,可以用 租户 + 业务动作 + 内容版本 作为稳定键,在持久存储中建立唯一约束,记录执行中、已完成和待核对状态;重复入口返回已完成结果,并发入口不能同时执行。正文变化后必须区分版本,避免误吞合法更新。
最容易漏掉的失败点是:远端发布成功,本地还没写“完成”就崩溃。恢复后直接重做仍会重复发布。下游支持幂等键时,按其接口契约复用同一键;不支持时,应先查询业务对象或进入人工核对。不要假定任意 LLM 接口都支持 Idempotency-Key,更不能把模型生成去重和业务发布去重混为一谈。
用本地桩验收这些失败路径
以下是给项目实施的验收清单,本文没有执行这些故障注入或付费 API 测试。本地桩与可控时钟适合检查代码;它们不能证明供应商真实计费规则。
| 注入情况 | 应检查的结果 |
|---|---|
| 返回明确的身份或参数错误 | 不原样重试,日志保留错误分类 |
| 临时限流要求等待,等待超出截止时间 | 本轮停止,不提前重发 |
| 连续可重试故障,随后队列重复投递 | 总尝试仍不超过业务预算 |
| 供应商接收后客户端超时 | 费用记未知;业务动作不会被盲目重放 |
| 流式响应中途失败 | 部分输出不作为成功结果,也不与新答案拼接 |
| 两个消费者同时处理一个任务 | 只准一个执行同一业务动作 |
| 远端动作成功后、本地完成写入前崩溃 | 恢复走幂等查询或核对,不重复发布 |
保留一次修复前后的配置、桩调用计数与任务日志,再用小规模真实业务请求核对供应商记录。判断改善时,同时比较成功任务、总尝试、未知结果和已确认费用,避免只是通过丢弃任务让账单下降。接入方案的选择可继续看 OpenRouter 与直连 API。