☰
省token却赔在重试上:LLM成本优化与重试策略实战
2026/10/10 4:29:49 网站建设 项目流程

前阵子给团队做内部工具,为了把 token 成本砍下去,我把系统提示词压到只剩一句话,历史消息只留最近两条,连工具定义都精简了一版。单次调用确实便宜了三分之一,我还专门做了个成本曲线发到群里。结果第二天就翻车:用户问了句稍微带点上下文的话,模型因为没有历史背景答偏了,客户端按照“失败自动重试”的逻辑立刻重试了一次,重试前带的是同样的残缺上下文,自然还是错。我翻后台账单,两天省下来的 token,十分钟重试就全还了回去。

后来我在工位便签上写了句话:省 token 省过头,一次重试就全赔回去了。这篇文章就是想认真聊聊 token 成本和重试机制之间的关系。正在做 LLM 应用接入、AI Agent 开发、或者帮团队优化 API 成本的同学都适用。内容全是我实际踩坑后的复盘,不绕弯子,直接算账。

1. 一次重试为何能清空你的token余额

1.1 先搞懂 token 账单是怎么组成的

每个 LLM API 的计费,拆开看基本是三部分:输入 token、输出 token、缓存命中 token。输入指你发过去的 prompt、系统提示、历史消息、工具定义;输出指模型生成的回复;缓存命中指的是支持 prompt caching 的平台,对重复输入的折扣价格。很多平台缓存命中的输入 token 价格只有原来的 10% 到 50%,但缓存写入本身也要计费。

这里有个容易被忽略的事实:输出 token 通常比输入 token 贵好几倍。大多数人在优化成本时只盯着输入,拼命压缩 prompt,却忘了重试时真正烧钱的是多出来的那几次输出。重试不会只重发一遍输入,它会把上一次的错误输出、修正指令、追加的上下文全部重新算一遍。总成本公式从来不是单次调用成本乘以调用次数,而是单次调用成本乘以(1 + 重试率)再乘以调用次数。只要重试率不是零,你的真实成本就永远高于乐观估算。

1.2 算一笔账:压一次 prompt,赔三次重试

我们用一组示例价格来算:假设输入 token 每百万 $0.5,输出 token 每百万 $2。一个正常的任务,完整 prompt 需要 8000 个输入 token,让模型输出 1500 个 token 完成任务,单次成本是 0.004 + 0.003,也就是 $0.007。

为了省 token,你把 prompt 压到 5000 个输入 token,正常成功一次的成本是 0.0025 + 0.003,合计 $0.0055,每次调用省下 $0.0015。看起来很不错,省了 20% 以上。但压得太狠,模型少看了关键信息,第一次输出只有 500 个 token 而且答错了。业务层自动重试,重试时输入变成“原压缩 prompt + 错误输出 + 修正指令”,总共 5500 个 token,输出 1500 个 token。两次调用加起来,总成本是 0.00525 + 0.004,约 $0.0093。

场景输入 token输出 token总成本(示例价)
完整 prompt 正常成功80001500$0.0070
压缩 prompt 正常成功50001500$0.0055
压缩 + 1 次重试105002000$0.0093
压缩 + 2 次重试165003500$0.0153

看明白了吗?压缩 prompt 正常成功省了 $0.0015,但只要重试一次,总成本就成了 $0.0093,比不省 token 的正常调用还贵 $0.0023。如果是两次重试,成本直接翻倍。也就是说,你省的是单次调用的钱,赔的是重试放大后的钱。重试次数越多,赔得越狠。

1.3 隐藏成本:并发、配额和用户耐心

token 账单只是最直观的部分。每次重试都在消耗你的 API 配额,尤其是 RPM 和 TPM 这类限流指标。团队共享一个账号时尤其明显:某个任务反复重试,占用的配额会让其他低优先级请求排队,然后触发更多 429,接着又有更多重试,形成恶性循环。热词里那句“您最近作出的请求太多了,请稍候,然后重试”,就是这种循环的典型信号。

用户耐心也是成本。一次重试意味着用户多等一轮推理时间,两次重试用户就可能关掉页面了。如果是本地部署小显存模型,比如 6G 显存跑量化模型,表面上 token 免费了,但一次重试等于整次推理再跑一遍,吞吐直接砍半。所谓 token 自由,其实只是把成本从显式账单转移到了隐性时间和算力上。

2. 什么情况会触发重试:高频报错排查清单

2.1 按错误码分类,哪些值得重试

我在实际运维中把重试触发原因归成几类,每一类的处理方式完全不同。一张表先给你摸个底:

错误类型典型报错是否适合重试
429 限流“请求太多了,请稍后再试”可以,但必须退避和降低并发
5xx 服务端错误500 / 502 / 503可以,指数退避,带幂等键
网络超时“网络异常,请稍后重试”分情况,优先考虑流式输出
400 参数错误“400 | trace id: xxx”不该重试,先修入参
401/403 鉴权失败“access token could not be refreshed”不该重试,先刷新凭证
refresh_token 非法“invalid 'refresh_token': empty string”百分百不该重试,是代码 bug
上下文超长“maximum context length exceeded”不该重试,先截断
输出格式不合规返回空、非 JSON不应盲目重试,改 prompt 或换模型

429 重试要有退避,不然就是在服务器最忙的时候不停敲门。5xx 可以重试,但要带上同一个请求标识,方便服务端知道你是在重试同一个请求。400、401、403 这类错误重试一万次结果都一样,因为它们不是瞬时故障,是请求本身有问题。

2.2 “省 token”为什么会制造报错

很多报错跟 token 压缩没有直接关系,但我在排查时发现,根源恰恰出在“省”这个动作上。一是把 prompt 压到临界,模型缺少关键上下文,答非所问,业务层只能重试;二是为了省一次参数校验,直接带着非法参数调 API,结果 400;三是把 access token 刷新逻辑简化过头,refresh_token 传了空字符串,所有用户被迫重新登录;四是为了少打日志,把 trace_id 吞掉,出了问题只能靠用户反复重试碰运气。

省 token 如果省的是冗余,那是优化。省的是关键信息,那是在给重试率做加法。重试本身不创造价值,它只是在为上游的错误买单。

2.3 重试次数不是越多越好

有些同学为了图省事,直接在 SDK 里配置 maxRetries = 5,失败就重试。这样做的后果很典型:遇到 400 错误时重试 5 次,等于把同一个 bug 的副作用放大 5 倍;遇到服务端临时故障时,5 次固定间隔的重试大概率会打到限流窗口,把原本能恢复的请求弄成 429。

正确做法是分层:先判断错误码是否可重试,可重试的再走退避策略,并且限制最大重试次数。重试时使用同一个 trace_id,日志里才能把多次尝试串成一条完整链路。

3. 重试不是无脑再来一次:策略设计

3.1 指数退避和抖动,别自己把并发打满

重试策略里最基础也最关键的,是指数退避加抖动。指数退避的意思是每次重试间隔翻倍,比如第一次等 1 秒,第二次等 2 秒,第三次等 4 秒。抖动是在间隔上加一个随机量,防止几十个客户端同时醒来发起重试,造成重试风暴。

import random import time import logging MAX_RETRIES = 3 BASE_DELAY_SECONDS = 1.0 RETRYABLE_STATUS = {408, 429, 500, 502, 503, 504} request_id = "req_xxx" # 同一请求多次重试保持同一个幂等标识 def call_llm_with_retry(call_func): for attempt in range(MAX_RETRIES + 1): try: return call_func(request_id=request_id) except APIError as err: if err.status_code not in RETRYABLE_STATUS: logging.warning("错误码 %s 不可重试,直接失败", err.status_code) raise if attempt >= MAX_RETRIES: raise delay = BASE_DELAY_SECONDS * (2 ** attempt) + random.uniform(0, 0.5) logging.info("第 %d 次重试,延迟 %.2fs,trace_id=%s", attempt + 1, delay, err.trace_id) time.sleep(delay)

这段代码的核心是两层判断:先判断错误码能不能重试,再判断有没有超过最大次数。退避和抖动必须一起用,只有退避没有抖动,集群场景下反而会在同一时刻造成请求高峰。

3.2 幂等键和 trace_id:重试不能重复扣费

重试最容易被忽略的细节是幂等。很多 API 平台支持请求级幂等键,也就是你传同一个 request_id,服务端能认出这是同一次请求的重试,避免重复扣费。但在业务层面,幂等同样重要。如果你的重试是由于前端网络抖动导致的,但服务端其实已经处理成功,重试就会产生重复任务。

我现在的习惯是:每个外部请求都生成一个 request_id,重试时复用同一个。每次调用都在日志里记录 request_id、错误码、trace_id 和耗时。trace_id 是平台返回的定位标识,几个字符而已,但它是事故现场的指纹。没有它,用户反馈报错时,你连这个请求是否发生过都无法判断。

3.3 给重试上一条成本闸门

重试不能没有上限,更不能没有预算意识。我给每个任务设置一个重试预算:单次调用成本乘以允许的重试次数,就是这次任务最多能承受的额外开支。如果模型失败率明明很高,还在那里硬重试,只会让成本成倍放大。

预算闸门通常这么设计:进入重试循环前,先根据当前错误类型预测是否值得重试。可重试错误,最多重试 3 次;任务级调用次数上限,比如说一个 Agent 任务最多调 10 次模型,超过就中断;按成本算,如果重试累计成本已经超过正常成功成本的 50%,就停止重试,走降级逻辑。这些阈值要根据业务容忍度调,但一定要有,否则重试就是无底洞。

4. 从源头减少重试:省token的正确姿势

4.1 压缩 prompt 也要守住底线

prompt 压缩能做,但要有边界。可以压缩的是:重复的语气词、多余的背景描述、过长的 few-shot 示例、堆砌的同义指令。不能压缩的是:工具定义的 schema、输出格式要求、安全边界、关键业务上下文。工具说明和输出格式直接影响模型是否“一次做对”,删掉它们等于逼着模型犯错,然后你再用重试为自己的操作买单。

压缩后的 prompt 必须经过测试集验证,不能凭感觉。我自己的做法是准备一组覆盖核心场景的回归用例,压缩前后各跑一遍,对比任务成功率。只要成功率不掉,压缩才是安全的。如果成功率掉了,哪怕单次调用再便宜,整体期望成本一定是上涨的。

4.2 上下文裁剪:真正的大头在这里

长对话场景里,历史消息才是 token 消耗的大头。省 token 的正确姿势不是把历史全部删掉,而是做滑动窗口加摘要:保留最近几轮完整的原始消息,更早的内容用一段摘要代替。摘要是一次额外的模型调用,本身也花 token,但比起无限膨胀的上下文,仍然划算。

要注意中文的 token 计量。中文字符很多时候接近一个 token 一个字,和英文的分词逻辑不一样。你在按字数估算 token 时很容易产生偏差,最好直接通过 API 的 tokenizer 统计。另外模型对 prompt 开头和结尾的内容注意力更高,中间部分容易被忽略。所以最重要的输出格式要求,要放在 prompt 的尾部,而不是埋在中间。

4.3 用流式输出和 prompt caching 降本

流式输出对降低成本的影响经常被低估。非流式请求在生成完整回复前,客户端和服务器之间是一个长连接,一旦超过网关的超时时间,就会被判定超时,然后自动重试。改用流式输出后,首字返回时间大幅缩短,超时概率下降,重试次数自然减少。对用户来说,等待感知也更友好。

prompt caching 是另一个该用起来的特性。把不变的系统提示词、工具定义放在缓存键的前面,把动态变化的用户消息放在后面。命中缓存后,这部分输入 token 的价格会低很多。但要注意两点:缓存键里如果混入了时间戳这类每次变化的字符串,缓存就永远命中不了;缓存写入本身有成本,所以适合那些系统提示词固定、调用频繁的场景。

4.4 便宜的模型不一定省钱,成功率才是关键

模型选型是省钱还是烧钱的分水岭。小模型、量化模型的单价确实便宜,但在复杂任务上失败率也高。失败后的重试会消耗额外的输入和输出 token,而且这种重试往往是因为模型能力不够,重试多少次都改变不了结果。真正正确的做法是模型路由:简单任务走小模型,复杂推理走大模型,用“完成任务的总成本”来评估,而不是只看单价。

本地部署也是一样。6G 显存跑一个量化模型,说是 token 自由,其实每次推理时间都比云端长好几倍。一旦遇到输出质量不稳定,一次重试就要用户再等很久,服务的吞吐也上不去。token 自由不代表重试自由,在本地部署场景里,重试的成本是以时间和服务能力为代价的。

5. 真实事故复盘:三笔赔穿了的“省token账”

5.1 refresh_token 存成空字符串

之前有个项目,为了减少存储和代码量,把 refresh_token 初始化成了空字符串,过期后直接去刷新。测试环境因为 token 还没过期,一切正常。上线第二天,存量用户的 access token 陆续过期,刷新接口开始报错:failed to refresh token: 400 bad request: invalid 'refresh_token': empty string. expected a string with minimum length 1, but got an empty string instead.

客户端检测到失败后自动重试,结果同一个 400 错误被刷了好几遍,日志里全是重复堆栈。问题根源不是网络也不是服务端,是代码写错了。重试机制对瞬时故障有意义,对一个注定失败的请求只会放大故障。后来我加了一条铁律:凡是参数校验可以提前发现的错误,一律快速失败,不要让用户带着错误反复排队。

5.2 砍掉工具说明,模型开始“自由发挥”

另一个项目里,我们接入了 deepseek-coder-v2 之类的编码模型,用来做一些自动化任务。为了省 token,我把工具列表里的参数说明删了大半,只留下函数名和一行简介。结果是模型根本不知道参数应该怎么填,频繁返回非法调用。业务层自动重试,每次失败还在产生输出 token,错误输出一样计费。

报错提示是“请切换模型或重试”,但我试了重试,几次结果都一样。问题根本不是模型不稳定,是工具说明被砍得太狠,模型在信息不足的情况下只能猜。后来我把完整的参数 schema 补回去,成功率立刻恢复。工具定义是让模型少犯错的关键输入,不是冗余。凡是影响模型输出格式的信息,都属于底线内容。

5.3 日志没有 trace_id,事故现场直接蒸发

有一次线上接口频繁返回400 | trace id: xxx,用户反馈说功能不可用。我去查日志,发现团队成员为了少打日志、节省存储,把 catch 块里的参数和 trace_id 全吞了,只留一句“调用失败”。那一刻我连用户遇到的是哪个请求都定位不到,唯一的办法是让用户重新操作,再看能不能复现。

那次之后我定了个规矩:trace_id 必须记录,请求摘要必须记录,错误码必须记录。这三样东西加在一起也占不了多少 token 和存储,但它们是排查重试问题的基础。没有 trace_id,你连“这次重试和上次失败是不是同一个问题”都判断不了,所有重试策略都是盲人摸象。

6. 决策速查表与成本预算公式

6.1 遇到错误时先查这张表

把常见的调用错误和处理方式整理成了一张速查表,我建议你直接贴到项目文档里:

报错要不要重试重试姿势
429 限流要指数退避加抖动,降低并发,必要时换 key
5xx 服务端错误要指数退避加幂等键,最多 2 到 3 次
网络超时分情况优先用流式输出,首字超时再重试
400 参数错误不要检查参数、工具 schema、输出格式
401 access token 失效不要先刷新 token,刷新失败则重新登录
403 鉴权失败不要检查密钥权限和账号状态
refresh_token 为空或非法不要这是代码 bug,先修代码
模型输出格式不合规不要直接重试改 prompt 或换模型
上下文超长不要截断或摘要后再调用

这张表的判断逻辑很简单:瞬时类错误可以重试,确定性的逻辑错误绝对不能重试。重试前你还要做一道成本判断:这次重试的预期收益是不是大于预期成本。如果模型在同样的输入下失败两次,第三次大概率还是失败,那就别再烧 token 了。

6.2 长期监控与预算公式

成本监控不要只看单次调用价格,要盯着这几个指标:重试率、超时率、429 率、单任务平均实际成本。其中重试率是最先暴露问题的信号。我一般把重试率超过 5% 当作告警线,超过 10% 基本意味着 prompt 设计或者模型选型出了问题,这时候改代码比继续调重试参数有效得多。

预算公式可以简化成这样:预估成本 = 单次调用成本 ×(1 + 预期重试率)× 任务数。如果你的优化方案能把单次成本降 20%,但预期重试率从 2% 涨到 10%,整体成本反而上升。所以我现在的习惯是:任何 prompt 压缩上线前跑一组回归用例,任务成功率不掉才允许上线;任何重试逻辑上线前算一遍重试预算,不可重试的错误一律快速失败。省 token 真正的意义是省掉低质量请求,而不是把单次调用压到极限。单次调用再便宜,只要成功率撑不住,一次重试就会把你之前省下的全部收走。这个道理,我是真金白银买回来的,希望你不用再买一次。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询