☰
避免越省越贵:LLM调用的token优化与重试成本控制
2026/10/10 10:54:34 网站建设 项目流程

做AI应用这一年多,我见过最冤的线上事故:为了省token把上下文砍得太狠,结果模型一次回答不完整,触发重试,重试把之前省下的token全吐回去了,还倒贴不少。这类问题在LLM API调用里特别典型,项目一上线,token用量和重试次数就像坐过山车,账单数字离谱,日志里还躺着一堆“请求失败,稍后重试”的标记。今天把这块的账算清楚,再聊聊到底怎么在不压垮质量的前提下做token优化。

1. 先算一笔账:token不是省出来的,是算出来的

很多人一看到大模型按token计费,第一反应就是削上下文、砍输出,觉得只要prompt够短就省钱。这个方向没错,但边界感没拿捏好,就会陷入“省小钱、赔大钱”的循环。核心原因在于:token账单不是一个线性函数,它由“输入token × 单价 + 输出token × 单价”决定,但重试会让这个公式爆炸性放大。

1.1 一次重试到底吃掉多少成本

先给出一个基准场景:某个对话项目用的是普通商用模型,假设输入价格是0.15美元/M tokens,输出是0.6美元/M tokens。

正常设计下,一次请求带10k上下文,模型正常输出2k结果。成本大约是:

输入成本:10,000 / 1,000,000 × 0.15 = 0.0015美元
输出成本:2,000 / 1,000,000 × 0.6 = 0.0012美元
单次总成本:0.0027美元

某一天你为了“省钱”,把上下文硬砍到2k tokens,想着反正模型也能看懂个大概。结果模型因为缺少关键背景,只吐了500 tokens的残缺回答就被语义断裂卡住了。这次调用的成本确实低:

输入成本:2,000 / 1,000,000 × 0.15 = 0.0003美元
输出成本:500 / 1,000,000 × 0.6 = 0.0003美元
单次成本:0.0006美元

看上去比原来便宜了四倍多。但问题来了:这个回答根本没法用,必须重试。

重试时,你又得把完整的上下文补回来——因为你已经意识到刚才砍太多了。于是重试请求带着10k上下文发出去,模型这次输出2.5k结果:

重试输入成本:10,000 / 1,000,000 × 0.15 = 0.0015美元
重试输出成本:2,500 / 1,000,000 × 0.6 = 0.0015美元
重试成本:0.003美元

再看总账:0.0006 + 0.003 = 0.0036美元,比一开始就没有省过的0.0027美元高出33%。也就是说,省过头的那次调用,非但没省钱,反而让单次需求的总成本上涨了三分之一。如果重试两次,成本直接翻倍都不止。

这个例子最扎心的是,我们往往只记着“省了10k上下文”,却不愿意看“重试后重新发了10k上下文”。省下的没有消失,只是变成了重试的经费。

1.2 为什么“省”反而更贵:隐藏的固定成本

token成本只是明面上的账。一次重试背后还有三类隐藏成本:

第一,延迟成本。正常一次请求200ms搞定,重试意味着最少再加一次完整的网络往返,如果遇到排队、限流,延迟可能扩大到秒级。用户的等待体验直线下降,在线服务尤其伤。

第二,系统资源成本。每一次请求都要经过网关鉴权、负载均衡、日志采集、监控上报。重试不是单纯多调一次大模型接口,而是整个调用链路上所有组件都要再跑一遍。这些资源开销在高并发下会被无限放大。

第三,业务状态成本。如果重试发生在写操作或者有状态任务里,没有幂等设计就会出现重复扣费、重复通知、重复创建。这会带来更严重的数据问题,远不是token能衡量的。

所以我们做优化时,一定要把重试概率作为核心指标。降低重试率比单纯压低单次token量更有全局价值。

2. 省token省过头:常见的三种翻车现场

在实际项目里,“省过头”通常不是一次性决策,而是在代码里默默埋下的雷。这里整理三种最高频的翻车情况,都是我踩过或者帮别人排查过的。

2.1 截断上下文导致模型“失忆”

对话型Agent最常干的事:维护一个滑动窗口,只保留最近N轮对话,更早的系统指令和用户核心需求被强行截断。

一开始效果还行,但当你跟模型说“按照之前约定的JSON格式输出”时,模型会愣住——因为之前约定的格式已经被截掉了。于是它自由发挥,输出一个残缺的JSON结构,解析失败,触发重试。重试时你把完整历史又补回去,token自然就飙了。

另一个典型是RAG场景。检索到的文档被截断到很短片段,模型拿到的信息不够回答用户问题,只能含糊其辞,然后你发现回答质量不达标,又要重新检索、重新生成。实际上,为了省几百token的检索片段,赔上的可能是整段回答的重新生成成本。

我的建议很简单:能做摘要就不要截断。上下文可以压缩,但压缩后的内容必须保留关键事实。宁可上下文长一点,也不要在信息不完整的情况下硬跑。

2.2 输出长度设置太紧,一句话断在半路

这种翻车更隐蔽。有些人调max_tokens的时候,喜欢把值卡得很死,比如设置成200,觉得回答一般不会超过200。但模型是概率生成的,可能这次回答要用450个token才能解释清楚。一旦达到上限,生成会被强制中断,输出直接断在一个词中间,JSON解析必挂,回答也会显得极其不自然。

解析挂了之后,常见处理就是直接重试。重试时同样的上下文再发一遍,输出重新生成一次。这个时候你会发现,你省的是那200 token的“保险空间”,赔的却是整次重试的输入输出token。

还有一种情况更离谱:把max_tokens设置成0或者1,想用贪婪解码逼模型给出非常短的答案。结果模型确实只吐了一个token,但这个token要么是“是”,要么是“否”,要么干脆是逗号,完全不可用。再重试时,输出预算被一次用完,输入上下文还在,成本一点没省。

2.3 滥用缓存与复用导致状态污染

现在很多SDK支持缓存历史对话或prompt片段。有同学图省事,直接把上一次的完整回复塞进下一轮请求当引导,或者缓存key设计得过于粗糙,导致不同用户复用了同一段缓存。

比如你把A用户的对话摘要临时存在本地,B用户的请求因为key撞了,误用了A的上下文。模型顺着错误上下文生成,答非所问,用户自然会追问,追问又触发更多错误上下文,等发现问题时已经多消耗了好几轮token。

这不仅仅是token问题,更是正确性问题。缓存设计必须严格校验上下文命中的唯一性,宁可不命中也不能命中错误的数据。关于缓存与token的关系,后面单独展开。

3. 正确的省钱姿势:在关键节点做减法

既然“省过头”会翻车,那到底怎么省才稳?核心原则是:宁可减少调用次数,也不要压缩单次质量。下面几个方向是我实践下来性价比最高的。

3.1 上下文裁剪的合理边界

所有上下文信息,我习惯按重要性分成三层:

  • 核心层:系统指令、用户当前明确需求、必须遵守的格式约束。这部分一条都不能少。
  • 支撑层:最近的对话历史、相关的检索片段、中间计算结果。按需保留,能摘要就摘要。
  • 扩展层:早期对话细节、相似案例、调试信息。基本可以丢弃,或仅保留摘要。

实操时,我会设定一个硬性保护窗口。比如token上限是8k,我只允许动态区占用6k,保留2k给核心层。任何裁剪逻辑都不能触碰核心层。日常中,我还会记录“完整上下文包含哪些关键字段”的元信息,在裁剪后补一段“你已经拥有以下事实”的提示,让模型明确知道遗漏了什么。

这种方式看起来没用——因为它实际上变长了。但它能大幅减少模型因缺信息而重试的概率。实验下来,长期总token消耗反而下降了。

3.2 用摘要代替原始历史

滚动窗口面对长会话时,最省钱的替代方案是摘要。当对话历史超过一定轮次,触发一次子任务:让模型把之前的对话压缩成一段结构化的摘要,只保留时间线、决策点、未完成事项。

摘要自身会消耗token,但它是一次性开销。之后每一轮请求都可以用摘要代替全部历史,省下来的是重复多轮的全程历史输入成本。

例如,一个10轮对话原始历史约8k token。压缩成摘要1k token。之后每轮请求都省7k。只要后面还有超过一轮的真实交互,摘要就是划算的。如果只有一轮,直接把原文发过去反而更省。所以触发条件很关键:至少要保证会话还会继续到第3轮以上才做摘要。

压缩摘要时有一个细节:摘要模型和被压缩的对话是同一模型时,本身调用也要消耗token,所以摘要生成要控制输出长度,同时标记摘要生成时间,便于后续清理。

3.3 输出预算要留安全边际

max_tokens设置,我的经验值是取预期输出长度的1.5到2倍,并且不低于一个安全下限。如果预期回答是200 token,那就设到400。如果预期是800,那就设到1200。

如果模型支持流式输出,配合max_tokens来做token耗尽检测就更好办。流式返回时,当发现生成接近max_tokens边界但内容尚未完整闭合(比如JSON的括号还没闭合),就主动结束当前请求,用部分结果做修正,避免等到请求超时或解析失败后再重试。

还有一个小技巧:JSON模式或结构化输出时,把max_tokens设置到足以容纳完整结构的最小值以上,而不是盯着字符数预估。我的经验是,JSON模式输出往往比自然语言多30%到50%的标记开销,因为键名、引号、括号都在消耗token。

3.4 重试参数的工程化配置

重试不是无限度的。合理的策略应该包含三部分:

第一,重试上限。一般不超过3次。超过3次还失败,说明问题大概率不在token或模型,而在prompt本身或服务端故障。

第二,退避策略。每次重试间隔指数递增,比如初始1秒,之后2秒、4秒,直到最大值60秒。这能有效规避限流,避免重试风暴。

第三,幂等设计。每个请求带上全局唯一的request_id,服务端对相同request_id做去重。这样即使客户端超时后重发,也不会让模型重复执行写操作或重复扣费。

很多开发者忽略最后这一点,结果在支付回调、工单创建这类场景里,一次重试直接造成业务数据重复,那成本就不是token能衡量的了。

4. 常见报错与排查清单

线上跑久了,你会遇到一批和token、重试相关的奇怪报错。有些是模型服务端返回的,有些是网关层的。我这里整理了一份亲测有效的排查流程。

4.1 那些和token、重试相关的典型报错

模型接口层常见的:

  • “token exchange failed: token endpoint returned status 403 forbidden”:一般是鉴权失败,认证token没有正确刷新或者权限过期。
  • “failed to refresh token: 400 bad request: invalid 'refresh_token': empty string”:refresh_token为空或者过期,引发连续重试。如果刷新逻辑里没有做空值判断,就会进入无限刷新循环。
  • “您最近作出的请求太多了。请稍候,然后重试。”:限流,通常会伴随429状态码。此时必须做退避,而不是立即重试。
  • “出现问题需要重试我们将转到...”:网关侧的通用错误,多半是服务端瞬时故障,适合指数退避重试。

这些报错的核心问题集中在三处:认证token的过期与刷新、限流处理策略、上下文载荷异常。所以排查时从这三个方向入手效率最高。

4.2 实战排查流程

我通常按下面几步排查token相关故障:

  1. 看日志里的重试次数:如果单条请求重试超过2次,立刻拉取对应时间段的上下文快照。
  2. 对比完整上下文和实际发送的上下文:检查是否有截断、摘要字段缺失、缓存错误命中。
  3. 检查max_tokens设置:与本次模型输出的token数进行比对,确认是否因为输出长度上限被截断。
  4. 检查认证token的过期时间与刷新逻辑:确认refresh_token是否有为空、过期、并发刷新等问题。
  5. 统计重试带来的额外token:在监控面板上针对“原始调用输入token + 所有重试输入token + 最终输出token”做总量统计,这个数字才是真实成本。

这套流程配合日志和监控,能在几分钟内定位绝大多数因token优化引发的问题。坚持下来,你会建立起“重试率优先”的思维,而不再只盯着单次成本。

4.3 重试幂等设计

幂等设计这件事我再强调一下。每次请求体里增加一个UUID,同时在业务层创建一个“请求状态表”。收到请求时先查表:

  • 若request_id已存在且状态为成功,直接返回上次的结果,不发起新调用。
  • 若状态为处理中,则等待或直接提示客户端稍后重试。
  • 若不存在,则记录状态为处理中,然后发起真实调用。

所有重试都基于同一个request_id,服务端不会第二次执行写操作。这就能把“重试”从一次风险操作变成一次纯读操作。实践中的成本开销极小,但对业务的保护价值极高。

值得一提的是,部分模型API本身也支持读取已有结果,你可以利用这一点进一步减少重复生成的开销。

5. 我的个人实操体会

做这一行久了,越来越觉得token优化是一场“成本与质量的博弈”。很多人一上手就追求极致压缩,但经验告诉我,把“重试率”挂在你显示器旁边,比把“token价格表”挂上去有用得多。每次想减上下文之前,先问一句:这样减完,模型还能不能稳定输出正确结果?如果不能,那省下来的token迟早会在重试里加倍还回去。

我还养成了一个习惯:每个项目上线后,把“正常调用token”和“重试额外token”分开统计。用监控看板展示单项成本占比。凡是重试占比超过10%的场景,一律暂停对上下文的继续压缩,优先排查请求质量。这个方法帮我避开了好几次“越省越贵”的坑。

最后分享一个小技巧:在做prompt工程时,可以在prompt末尾加一句“不要遗漏关键信息,如果上下文不足请主动说明”,让模型在信息缺失时主动请求补充,而不是尽力硬答。这个小改动看似占了一点点token,但能大幅减少因答错引发的后续重试,综合收益很可观。

token优化的天花板不是把prompt压到最短,而是让每一次请求都一次成功。这个目标,远比想象中值钱。

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

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

立即咨询