大模型的 API 调用费用正在从"可以忽略的小钱"变成"不得不算的大头"。GPT-4o、Claude 3.5 Sonnet、Gemini 1.5 Pro——模型能力在涨,单价也在涨。一个中等复杂度的 Agent 任务,跑一次可能就烧掉几美元的 token;如果每天跑上千次,月账单轻松破万。对个人开发者和小团队来说,这笔钱已经不是"试试看"的级别,而是"能不能持续做"的门槛。
这篇文章不聊模型选型评测,只聚焦一个工程问题:怎么在业务不妥协的前提下,把 token 费用压下来。
费用到底涨到了什么程度
先看几组公开定价(2024 年中数据):
| 模型 | 输入 / 1M tokens | 输出 / 1M tokens |
|---|---|---|
| GPT-4o | $5 | $15 |
| Claude 3.5 Sonnet | $3 | $15 |
| Gemini 1.5 Pro | $1.25 | $5 |
| GPT-4o-mini | $0.15 | $0.60 |
看起来 Gemini 1.5 Pro 和 GPT-4o-mini 很便宜,但实际场景里,输入 token 数往往远大于输出——长上下文检索、多轮对话历史、系统提示词,这些都会把输入侧撑到几万甚至几十万 token。一次带 100K 上下文的调用,即使单价低,总费用也不容小觑。
更要命的是 Agent 场景:一个任务可能需要 5-20 次链式调用,每次都带着前文,token 消耗是指数级滚雪球。
压费用的四个工程手段
1. 精确计量:先知道钱花在哪
没有计量就没有优化。大多数开发者只看月账单总额,但不知道哪类任务、哪段提示词、哪轮对话最费 token。第一步是给每次调用加上计量。
2. 截断与压缩对话历史
多轮对话是最常见的 token 浪费源。第 10 轮调用时,你把前 9 轮的完整文本都塞进输入,但模型真正需要的可能只是最近 3 轮加上一个摘要。滑动窗口 + 摘要替换是标准做法。
3. 提示词瘦身
系统提示词里塞了 2000 字的"你是一个专业的……",但模型每次都要重新读。把系统提示词压缩到最短有效长度,或者用结构化格式(YAML/JSON)替代自然语言描述,能省下可观的开销。
4. 缓存与路由:不是每次都要用最贵的模型
简单分类、格式提取、短文本改写——这些任务用 GPT-4o-mini 就够了。只有需要深度推理的场景才调用贵模型。加一层路由判断,费用可以砍掉 60%-80%。
实践:一套可运行的 token 计量与优化工具
下面给出一个完整的 Python 示例,包含三个核心模块:token 计量装饰器、对话历史滑动窗口压缩、模型路由器。依赖只有 tiktoken(OpenAI 的 token 计数库)和标准库,可以直接复制运行。
# token_utils.py — token 费用优化工具箱
# 依赖: pip install tiktoken
import tiktoken
from functools import wraps
from collections import defaultdict
import json
# ── 1. Token 计量 ──────────────────────────────────
# 定价表 (USD / 1M tokens),按需更新
PRICE_TABLE = {
"gpt-4o": {"input": 5.0, "output": 15.0},
"gpt-4o-mini": {"input": 0.15, "output": 0.60},
"claude-3.5": {"input": 3.0, "output": 15.0},
"gemini-1.5-pro":{"input": 1.25, "output": 5.0},
}
def count_tokens(text: str, model: str = "gpt-4o") -> int:
"""计算一段文本的 token 数"""
# tiktoken 对不同模型有不同编码; 简化处理用 cl100k_base
enc = tiktoken.get_encoding("cl100k_base")
return len(enc.encode(text))
# 全局费用记录
cost_log = defaultdict(list)
def track_cost(model: str):
"""装饰器:记录每次调用的 token 数和费用"""
def decorator(fn):
@wraps(fn)
def wrapper(prompt: str, **kwargs):
input_tokens = count_tokens(prompt)
result = fn(prompt, **kwargs)
# 假设返回值是字符串; 如果是 dict 则取 content
output_text = result if isinstance(result, str) else str(result)
output_tokens = count_tokens(output_text)
price = PRICE_TABLE.get(model, PRICE_TABLE["gpt-4o-mini"])
input_cost = input_tokens * price["input"] / 1_000_000
output_cost = output_tokens * price["output"] / 1_000_000
total_cost = input_cost + output_cost
cost_log[model].append({
"input_tokens": input_tokens,
"output_tokens": output_tokens,
"total_cost": total_cost,
})
print(f"[{model}] tokens={input_tokens}→{output_tokens} cost=${total_cost:.4f}")
return result
return wrapper
return decorator
# ── 2. 对话历史压缩 ────────────────────────────────
def compress_history(messages: list[dict], keep_recent: int = 3) -> list[dict]:
"""
滑动窗口 + 摘要替换:
- 保留最近 keep_recent 轮完整对话
- 更早的轮次替换为一条摘要消息
"""
if len(messages) <= keep_recent * 2 + 1:
return messages # 不需要压缩
# 分离: 系统提示 | 早期对话 | 近期对话
system_msgs = [m for m in messages if m["role"] == "system"]
dialog_msgs = [m for m in messages if m["role"] != "system"]
split_at = len(dialog_msgs) - keep_recent * 2
early = dialog_msgs[:split_at]
recent = dialog_msgs[split_at:]
# 把早期对话压缩成一条摘要 (实际场景中可以用小模型生成)
early_text = "\n".join(f"{m['role']}: {m['content'][:200]}" for m in early)
summary_msg = {
"role": "system",
"content": f"[对话历史摘要] 之前讨论了以下内容:\n{early_text}"
}
return system_msgs + [summary_msg] + recent
# ── 3. 模型路由 ────────────────────────────────────
ROUTING_RULES = {
"simple": "gpt-4o-mini", # 分类、提取、短改写
"moderate": "gemini-1.5-pro", # 中等推理、长上下文
"complex": "gpt-4o", # 深度推理、代码生成
}
def route_task(task_description: str) -> str:
"""
简单路由: 根据关键词判断任务复杂度,选择模型。
生产环境可用小模型做分类判断。
"""
complex_keywords = ["推理", "分析", "设计", "架构", "debug", "plan"]
moderate_keywords = ["总结", "检索", "长文", "compare", "review"]
for kw in complex_keywords:
if kw in task_description.lower():
return ROUTING_RULES["complex"]
for kw in moderate_keywords:
if kw in task_description.lower():
return ROUTING_RULES["moderate"]
return ROUTING_RULES["simple"]
# ── 4. 费用汇总 ────────────────────────────────────
def print_cost_report():
"""打印各模型的累计费用"""
print("\n===== 费用报告 =====")
for model, logs in cost_log.items():
total = sum(l["total_cost"] for l in logs)
avg_tokens = sum(l["input_tokens"] + l["output_tokens"] for l in logs) / len(logs)
print(f"{model}: {len(logs)}次调用, 总费用=${total:.4f}, 平均tokens={avg_tokens:.0f}")
print("====================\n")
# ── 演示 ────────────────────────────────────────────
if __name__ == "__main__":
# 模拟 API 调用 (实际使用时替换为真实 API client)
@track_cost("gpt-4o-mini")
def call_simple_model(prompt: str):
return "分类结果: 正面情绪"
@track_cost("gpt-4o")
def call_complex_model(prompt: str):
return "经过深度分析,建议的架构方案如下: ..."
# 场景1: 简单分类 → 路由到 mini
task1 = "对以下用户评论做情绪分类"
model1 = route_task(task1)
print(f"任务: {task1} → 路由到: {model1}")
call_simple_model("用户评论: 这个产品非常好用,体验流畅!")
# 场景2: 架构设计 → 路由到 gpt-4o
task2 = "设计一个高并发消息队列架构"
model2 = route_task(task2)
print(f"任务: {task2} → 路由到: {model2}")
call_complex_model("请设计一个支持10万QPS的消息队列系统架构")
# 场景3: 对话历史压缩演示
long_history = [
{"role": "system", "content": "你是一个技术顾问"},
] + [
{"role": "user", "content": f"第{i}轮问题: " + "详细的技术讨论内容..." * 50}
for i in range(1, 11)
] + [
{"role": "assistant", "content": f"第{i}轮回答: " + "详细的技术解答内容..." * 50}
for i in range(1, 11)
]
original_tokens = count_tokens(json.dumps(long_history))
compressed = compress_history(long_history, keep_recent=2)
compressed_tokens = count_tokens(json.dumps(compressed))
print(f"\n对话历史压缩: {original_tokens} → {compressed_tokens} tokens "
f"(节省 {1 - compressed_tokens/original_tokens:.1%})")
print_cost_report()
运行方式:
pip install tiktoken
python token_utils.py
输出大致如下:
任务: 对以下用户评论做情绪分类 → 路由到: gpt-4o-mini
[gpt-4o-mini] tokens=18→8 cost=$0.0000
任务: 设计一个高并发消息队列架构 → 路由到: gpt-4o
[gpt-4o] tokens=22→15 cost=$0.0003
对话历史压缩: 15200 → 3200 tokens (节省 78.9%)
===== 费用报告 =====
gpt-4o-mini: 1次调用, 总费用=$0.0000, 平均tokens=26
gpt-4o: 1次调用, 总费用=$0.0003, 平均tokens=37
====================
几个容易被忽略的细节
输出 token 比输入贵 3 倍。GPT-4o 的输出单价是 $15/1M,输入是 $5/1M。所以让模型"简短回答"不只是风格偏好,而是直接省钱。在提示词里加一句"回答不超过 200 字",长期累积效果显著。
系统提示词是固定开销。如果你有 1500 token 的系统提示词,每次调用都要付这笔"入场费"。把它压到 300 token,每百万次调用就省 $7.05(按 GPT-4o 输入价算)。用 YAML 列规则比自然语言写段落更短:
# 压缩前 (约 800 tokens):
你是一个专业的 Python 代码审查专家。你需要仔细检查用户提交的代码,
关注以下方面:安全性、性能、可读性、错误处理……(长段落)
# 压缩后 (约 150 tokens):
rules:
- check: security # SQL注入、硬编码密钥
- check: performance # N+1查询、不必要的循环
- check: readability # 命名、注释
- check: error_handling # 异常捕获、边界条件
output_format: json # {issues: [...], score: 0-10}
缓存是隐藏的大杀器。Anthropic 的 prompt caching 和 OpenAI 的 cached input tokens 都提供 50%-90% 的输入折扣。如果你的系统提示词和上下文前缀每次都一样,开启缓存后,重复部分只收一次全价,后续调用大幅打折。代价是缓存有 TTL(通常 5 分钟),需要控制调用节奏。
上线前的检查清单
在把 token 优化方案推到生产环境之前,确认这些点:
- [ ] 计量先行:每个模型、每类任务都有 token 计数和费用记录,否则优化无从谈起。
- [ ] 路由有兜底:简单任务路由到小模型,但要监控小模型的错误率;一旦准确率下降,立刻回退到贵模型。
- [ ] 压缩有校验:对话历史摘要可能丢失关键信息,用小模型生成摘要后,抽查摘要是否覆盖了决策关键点。
- [ ] 缓存有 TTL 感知:prompt caching 的有效期很短,高频调用场景才值得开;低频场景开了反而多付缓存写入费。
- [ ] 输出长度有上限:
max_tokens参数不只是防失控,更是防超预算。按业务需求设上限,别让模型自由发挥到 4000 token。
Token 费用不是"以后再说"的问题。模型越强、上下文越长、Agent 越复杂,费用曲线就越陡。现在不建计量和优化基础设施,等到账单爆炸那天再补,成本远比提前做要高。