调用大模型API按Token计费,这个商业模式决定了用量即成本。一个中等规模的客服系统,每天处理上万轮对话,如果每轮对话携带完整的上下文历史,Token消耗会呈滚雪球式增长。很多团队上线第一个月就被账单吓一跳:明明用户量没有爆发式增长,费用却翻了数倍。问题的根源往往不在模型本身,而在于整个推理链路缺乏成本意识的设计。本文将从工程实践角度,拆解一套经过验证的Token消耗优化方案。

一、先搞清楚Token都花在了哪里
优化的第一步永远是测量。在动手压缩Token之前,需要把每一笔消耗归类清楚。一般业务系统的Token消耗分布大致是:系统提示词占20%到40%,历史对话上下文占30%到50%,用户实际输入占10%左右,模型输出占20%到30%。这个分布揭示了一个关键事实——真正承载用户意图的Token其实很少,大头被固定的、重复的内容吃掉了。
建议在请求层加一个中间件,记录每次调用的输入Token数、输出Token数、模型版本和业务场景,落到日志系统里做聚合分析。OpenAI的接口响应中自带usage字段,国内主流模型API也有类似设计。跑一周数据之后,你会发现几个明显的浪费点:比如系统提示词写得又臭又长,每次请求都原样发送;比如历史对话从不截断,到第20轮时上下文已经膨胀到几千Token。找到这些点,优化就有了明确靶子。
import json
from collections import defaultdict
# Token消耗统计中间件示例
class TokenTracker:
def __init__(self):
self.stats = defaultdict(lambda: {"input": 0, "output": 0, "calls": 0})
def record(self, scene, usage):
self.stats[scene]["input"] += usage["prompt_tokens"]
self.stats[scene]["output"] += usage["completion_tokens"]
self.stats[scene]["calls"] += 1
def report(self):
for scene, s in self.stats.items():
print(f"场景: {scene} | 调用次数: {s['calls']} | "
f"输入Token: {s['input']} | 输出Token: {s['output']}")二、提示词压缩与上下文管理:砍掉最大的浪费源
系统提示词是很多团队忽视的成本黑洞。一个写得冗长的角色设定加业务规则,动辄一两千Token,而它会在每一次请求中重复发送。压缩系统提示词有几个实用技巧:第一,删除所有客套话和重复强调的表述,模型对简洁指令的遵循度其实更高;第二,用结构化的短句代替自然语言长段落,比如把“你需要在回答用户问题的时候始终保持礼貌并且不要使用任何不文明的词汇”压缩成“回答保持礼貌,禁用不文明词汇”;第三,把不常用的规则挪到检索环节,只在需要时动态注入,而不是全量塞进系统提示词。
历史对话管理是另一个重灾区。多轮对话场景下,如果每次都携带全部历史,Token消耗会随轮数平方级增长。合理的做法是滑动窗口加摘要:保留最近5到8轮原始对话,更早的内容用一次便宜模型的调用压缩成摘要。这样既保住了短期上下文的精确性,又把长期记忆控制在几百Token以内。
def build_context(history, keep_rounds=6, max_summary_tokens=300):
# 滑动窗口 + 历史摘要的上下文构建
if len(history) <= keep_rounds:
return history
old_rounds = history[:-keep_rounds]
recent_rounds = history[-keep_rounds:]
# 用低成本模型将早期对话压缩为摘要
summary = cheap_model_summarize(old_rounds, max_tokens=max_summary_tokens)
return [{"role": "system", "content": f"之前对话摘要:{summary}"}] + recent_rounds输出端同样可以控制。绝大多数场景对回答长度的需求是有限的,设置合理的max_tokens参数,并在提示词中明确要求回答简洁,可以有效避免模型输出冗长的“复读机式”内容。实测中,仅这一项就能减少15%到25%的输出Token。
三、模型分级调度:用对的模型干对的活
所有请求都走最强模型,是成本失控的典型原因。实际上业务请求里相当大比例是简单任务:打招呼、查常见问题、格式转换,这类请求用旗舰模型纯属杀鸡用牛刀。合理的架构是引入一个路由层,根据请求复杂度分发到不同档位的模型。
路由策略可以很简单:先用规则匹配高频固定问题,命中直接返回缓存的答案,连模型都不用调;规则未命中的,用一个轻量分类模型或几个关键词特征判断复杂度,简单问题走小参数量模型,只有真正需要深度推理的请求才路由到旗舰模型。实践数据表明,客服类业务中70%以上的请求可以由低成本的中小模型胜任,综合成本能直接腰斩。
def route_model(query, faq_index):
# 第一层:FAQ精确匹配,零成本返回
hit = faq_index.search(query)
if hit and hit.score > 0.95:
return ("cached", hit.answer)
# 第二层:复杂度判断,简单问题走小模型
complexity = classify_complexity(query)
if complexity == "simple":
return ("model", "small-model-v1")
return ("model", "flagship-model")此外,批处理接口也值得利用。当业务允许延迟时,把多个请求攒成批次提交,多数厂商的Batch API价格只有实时接口的一半。适合批处理的场景包括离线内容审核、夜间数据整理、批量摘要生成等。
四、缓存机制:让重复请求彻底免费
真实业务的请求重复度远超想象。电商客服里“怎么退货”一天可能被问几百次,如果每次都完整调用模型,纯粹是烧钱。缓存分两个层级:精确缓存和语义缓存。精确缓存直接对规范化后的输入做哈希,命中即返回,实现简单但命中率有限。语义缓存更进一步,把请求转成向量,在向量库里检索相似度超过阈值的历史问答,命中则直接复用答案,实测能把20%到40%的请求挡在模型调用之前。
import hashlib
def semantic_cache_lookup(query, vector_db, threshold=0.92):
# 先做语义检索,判断是否已有足够相似的问答对
results = vector_db.search(embed(query), top_k=1)
if results and results[0].similarity >= threshold:
return results[0].answer, True
return None, False语义缓存要注意两点:一是阈值设置要偏保守,宁可漏缓存也不要返回错误答案,建议从0.92起步调优;二是缓存条目要设置过期和淘汰策略,涉及价格、库存等时效性内容的问题不适合长缓存。另外,如果使用的是支持Prompt Caching的API,把稳定的系统提示词放在请求前部,可以自动享受缓存折扣,部分厂商对缓存命中的输入Token只收取十分之一的价格。
五、建立持续的成本监控闭环
一次性优化之后,如果没有监控兜底,成本会随着业务迭代悄悄涨回来。建议把Token成本作为一级指标接入监控体系,按业务场景、模型版本、接口类型打标签统计,设置单请求Token上限告警和日均成本告警。当某个场景的成本环比上涨超过30%时,自动触发分析任务,定位是新上线的提示词膨胀还是路由策略失效。
更进一步,可以在网关层做硬性限制:单请求输入超过阈值时自动触发上下文截断,输出超过限制时强制截断并提示用户。这些防御性设计能保证即使代码出现回归,成本也不会失控。综合运用提示词压缩、上下文摘要、模型分级、语义缓存和批处理这套组合拳,把每月百万级Token压到十万级别,在大多数业务场景中是可以稳定达成的目标,而且用户几乎感知不到回答质量的下降。成本优化不是一次性的运动,而是需要嵌入研发流程的长期习惯。