OpenAI API 成本控制实战:缓存 + 压缩 + 路由,让 token 消耗减半

这篇文章写给用 OpenAI API 做项目,但每月账单越来越贵想省钱的开发者。五种亲测有效的方法,让你 token 消耗直接减半,不用换服务商也能省出钱。

痛点场景

刚开始用 OpenAI API 做项目的时候,感觉也不贵,调用一次才几分钱。用着用着用户多了,月底一看账单------几百上千块就没了。

其实很多 token 都是浪费掉的:重复请求、上下文太长、大模型杀鸡用牛刀... 我整理了几种我实际在用的成本控制方法,用上之后我的 token 消耗直接减半,账单好看多了。

这篇把代码和方法都放出来,你直接抄作业就行。

适用场景:

  • 用 OpenAI API(或兼容接口)做产品,每月成本超出预算
  • 想优化成本,但又不想换模型降质量
  • 不知道从哪儿下手优化,只知道钱花得快

方法一:语义缓存重复请求

问题

很多用户会问重复或者相似的问题,每次都重新调用 API,完全是浪费钱。比如做知识库问答,十个人问同一个问题,你要调用十次,完全没必要。

解决方法

把用户问题做 Embedding,存在缓存里,下次有相似问题直接返回 cached 结果,不用再调用大模型。

代码实现:

import numpy as np

import redis

from openai import OpenAI

client = OpenAI()

用 Redis 存缓存

r = redis.Redis(host='localhost', port=6379, db=0)

相似度阈值,越高越严格,0.8 差不多

SIMILARITY_THRESHOLD = 0.8

def get_embedding(text):

resp = client.embeddings.create(

model="text-embedding-3-small",

input=text

)

return np.array(resp.data[0].embedding)

def cosine_similarity(a, b):

return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

def get_answer_with_cache(question):

计算问题的 embedding

q_emb = get_embedding(question)

遍历缓存找相似问题

for key in r.keys():

cached = r.hgetall(key)

cached_emb = np.frombuffer(cached[b'embedding'], dtype=np.float32)

similarity = cosine_similarity(q_emb, cached_emb)

if similarity >= SIMILARITY_THRESHOLD:

找到相似问题,直接返回缓存结果

return cached[b'answer'].decode('utf-8'), True

没找到,调用 API 回答

response = client.chat.completions.create(

model="gpt-3.5-turbo",

messages=[{"role": "user", "content": question}]

)

answer = response.choices[0].message.content

存入缓存

cache_key = f"cache:{hash(question)}"

r.hset(cache_key, mapping={

'question': question,

'answer': answer,

'embedding': q_emb.tobytes()

})

设置过期时间,比如 7 天

r.expire(cache_key, 60 * 60 * 24 * 7)

return answer, False

能省多少

  • 如果你的应用有很多重复问题(比如客服问答),能省 30% - 50%
  • Embedding 本身很便宜,text-embedding-3-small 一百万个 token 才 $0.02,缓存带来的开销可以忽略

避坑

  • 阈值根据你的场景调:问题越固定,阈值可以设高点;问题多样性大,设低点
  • 给缓存加过期时间,过时了重新生成,保证答案不过期
  • 如果缓存量大,可以用飞书多维表格存,不用自己搭 Redis

方法二:上下文压缩,只给大模型真正需要的

问题

RAG 问答或者对话场景,你会把历史对话和检索结果都塞给大模型,上下文动不动就几千 token,很多内容其实和当前问题没关系,白白浪费钱。

解决方法

在把上下文给大模型之前,先压缩一遍------让小模型把长上下文压缩成和当前问题相关的核心内容,减少 token 消耗。

代码实现:

def compress_context(question, context_list):

"""用 gpt-3.5-turbo 压缩上下文,比直接给 gpt-4 省钱多了

prompt = f"""下面是和问题「{question}」相关的上下文,请提取出和问题相关的内容,去掉无关信息,压缩到最精简:

{context_list}

压缩后的内容:"""

response = client.chat.completions.create(

model="gpt-3.5-turbo",

messages=[{"role": "user", "content": prompt}],

temperature=0

)

compressed = response.choices[0].message.content

return compressed

能省多少

  • 原来 4000 token 的上下文,压缩完可能只剩 1000 token,省 75%
  • 用便宜的 gpt-3.5-turbo 做压缩,花很少的钱帮贵的模型省 token,总体还是赚的

什么时候用

  • 对话多轮,历史很长
  • RAG 检索回来好几段,总长度太大
  • 你用的模型按上下文长度收费,越长越贵

方法三:模型路由,该用什么模型用什么

问题

不管什么请求都扔给 gpt-4,成本当然高。其实很多简单任务,gpt-3.5-turbo 就能搞定,效果差不了多少,但便宜 10 倍。

解决方法

加一层路由:

  • 简单任务(问答、分类、摘要)→ gpt-3.5-turbo
  • 复杂推理、代码生成 → gpt-4
  • Embedding → 用最新的 text-embedding-3-small,比旧版本更便宜效果更好

路由代码示例:

def route_request(question, task_type):

"""简单的路由逻辑"""

定义哪些任务用便宜模型

simple_tasks = ['faq', 'classification', 'summarization', 'chat']

complex_tasks = ['reasoning', 'code', 'creative']

if task_type in simple_tasks:

return "gpt-3.5-turbo"

elif task_type in complex_tasks:

return "gpt-4o"

else:

可以让模型自己判断复杂度

return judge_complexity(question)

OpenAI 最新模型价格对比:

可以看到,gpt-3.5-turbo 比 gpt-4o 便宜 10 倍,能分流就分流。

能省多少

  • 如果你的请求有 60% 可以分给 gpt-3.5-turbo,总体成本能降一半左右

方法四:提示词压缩,去掉冗余

问题

很多人写提示词喜欢复制粘贴模板,每次都带一堆重复的系统提示,日积月累 token 就上去了。

比如:

你是一个乐于助人的AI助手,你总是回答准确...

...这里重复了几百个字...

现在请回答用户问题:xxx

其实很多话大模型不需要看,每次都带就是浪费 token。

优化方法

    1. 系统提示只说必要的:去掉所有空泛的套话,直接说任务要求
    1. 复用上下文:对话中已经说过的规则,不用每次都重复
    1. 用简写不影响理解:比如"用户"不用每次都写成"亲爱的用户"

不好的例子:

你现在是一个专业的AI助手,你拥有丰富的大模型落地知识,你总是能够给用户提供非常有帮助的回答,你的回答总是准确可靠,用户非常喜欢你的回答。现在用户问了一个问题,请你认真回答。

压缩后:

你是大模型落地领域专家,请回答用户问题,准确可靠。

省了几十 token,效果一点不差。

能省多少

  • 每次请求能省几十到上百 token,积少成多,一个月下来也能省不少

方法五:流式返回 + 提前截断

问题

很多时候大模型输出一半你就知道它要说什么了,还让它继续输出完,完全是浪费 tokens。

比如用户问一个有确定答案的问题,大模型第一句就说对了,后面都是扩展解释,用户其实已经得到答案了。

解决方法

做提前截断:当输出满足终止条件的时候,直接停止生成。

代码示例:

def stream_with_stop(question, stop_conditions):

"""流式生成,满足停止条件就提前终止"""

response = client.chat.completions.create(

model="gpt-3.5-turbo",

messages=[{"role": "user", "content": question}],

stream=True

)

output = ""

for chunk in response:

if chunk.choices[0].delta.content is None:

break

output += chunk.choices[0].delta.content

检查是否满足停止条件

for cond in stop_conditions:

if cond in output:

提前停止,不生成了

return output

return output

常见停止条件:

  • 用户只要求列表,拿到指定条数就停
  • 问题有确定答案,答案出来就停
  • 已经生成了指定最大长度,停

能省多少

  • 看场景,问答类能省 10% - 20% 输出 token

方法总结和效果估算

我把这五种方法都用上之后,在我的知识库项目里,token 消耗直接减半:

我的推荐组合(从易到难,先做简单的):

    1. ✅ 先做提示词压缩和模型路由,零成本就能省钱
    1. ✅ 加上语义缓存,效果立竿见影
    1. ✅ 如果你的上下文总是很长,再加上下文压缩
    1. ✅ 如果你的场景适合,最后加提前截断

避坑指南

坑 1:缓存太严格,错把不同问题当相同

解决:阈值从 0.8 开始试,根据你的实际情况调整,不行再调。

坑 2:压缩太狠,把关键信息压没了

解决:压缩完可以抽样检查一下,确保关键信息还在。一般来说,用 gpt-3.5-turbo 压缩不会丢关键信息。

坑 3:路由错了,简单任务给了复杂模型,复杂任务给了简单模型

解决:刚开始可以让人工分一分,线上跑起来了再用模型自动分类。

坑 4:为了省钱牺牲太多用户体验

记住:用户体验第一,成本优化第二。不要为了省几块钱,让回答质量下降太多,就得不偿失了。

相关推荐
数字芯片实验室3 小时前
提示词缓存:一个新瓶装旧酒的agent降本手段
缓存
披着羊皮不是狼17 小时前
(7)为 RAG 系统接入 Redis Stack 实现向量持久化
数据库·redis·缓存
難釋懷18 小时前
数据同步策略
缓存
程序员潘子20 小时前
【保姆级教程】B 站缓存 m4s 文件转 MP4,无损合成一行命令搞定
缓存·ffmpeg·ffmpeg\
Micro麦可乐20 小时前
Redis只会用来做缓存?解锁Redis非缓存的九个应用场景,90%程序员不知道的隐藏技能
数据库·redis·缓存·消息队列·分布式锁·延迟队列·布隆过滤器
键盘鼓手苏苏20 小时前
Flutter 三方库 persistent_cache_simple 的鸿蒙化适配指南 - 实现具备磁盘溢出淘汰与极简 API 的本地持久化缓存、支持端侧资源异步落地与状态秒开实战
flutter·缓存·harmonyos
21号 120 小时前
10.Redis 缓存
数据库·redis·缓存
从零开始的-CodeNinja之路20 小时前
【Redis】Redis 缓存应用、淘汰机制—(四)
java·redis·缓存
星辰徐哥20 小时前
CDN工作原理:节点缓存、智能调度,减少跨网传输延迟
服务器·缓存·php