Token 费用越来越贵,开发者怎么扛住这笔账

2026-05-29 28 预计阅读时间:1 分钟
来源:ruanyifeng.com AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:13 分钟

大模型的 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 越复杂,费用曲线就越陡。现在不建计量和优化基础设施,等到账单爆炸那天再补,成本远比提前做要高。


相关推荐