近期社区讨论里,“Opus 5 狂烧 6.9 亿 token 做游戏,GPT-5.6 用 5 美元复刻了”这类对比标题吸引了不少注意力。且不说标题中的模型版本具体是哪一代、价格是否准确,它背后反映的是一个所有大模型应用开发者都会遇到的实际问题:同样一个目标,不同实现路径下的 token 消耗为什么能差出好几个数量级。token 是大模型计费、上下文容量和推理成本的最小单位。无论是做代码生成、搭建 RAG 问答系统,还是做一个游戏 demo,token 消耗都直接决定你的调用成本、接口响应时间和线上可承载的请求量。这篇文章不讨论“谁更厉害”,而是把一条完整的工程链路讲清楚:token 是怎么被消耗掉的,为什么有些场景会烧到上亿级别,有些场景却能控制到极低水平,以及在自己的项目里如何做预算、记录、优化和排查。
1. 先看懂 token 消耗差距是怎么拉开的
1.1 token 是模型处理文本的最小单元
在大型语言模型里,文本并不是按“字符”或“单词”直接输入给模型的。模型会先把文本切分成一个个 token,再转成向量参与计算。不同模型的 tokenizer 不同,切分结果也不同。英文常见的切分是一个单词拆成一到两个 token,中文则可能是几个字合成一个 token,代码中的空格、换行、括号也会产生 token 消耗。
一个容易误解的点是:不能按字符数估算 token 数。下面这段代码可以直观展示切分差异:
import tiktoken enc = tiktoken.get_encoding("cl100k_base") text = "def hello(): return '你好,世界'" print(len(enc.encode(text)))这里使用的cl100k_base是 OpenAI 文本模型常用的一种 BPE 词表。不同模型有各自的 tokenizer,同一个文本在不同模型的计费口径下,token 数并不一致。所以项目里做用量统计时,必须以实际调用的模型返回的usage字段为准,而不是本地估算后猜一个数。
1.2 做游戏这类长任务,token 消耗来自哪几层
标题里说的“做游戏”,本质上是一次多轮、长上下文的生成任务。这类任务的 token 消耗不是只来自“最终生成的那段代码”,而是来自四个叠加的层级:
第一层是初始需求描述。把“做一个俄罗斯方块”这种需求扩写成完整的产品说明、规则要求、技术栈约束,通常就是上千 token。第二层是每轮模型生成的输出。生成一个完整小游戏的源码,输出量可能在 5000 token 到 30000 token 之间,取决于代码长度和注释量。第三层是运行报错后的回填。工程实践中,开发者会把报错日志、堆栈、截图描述塞回模型,请求里的输入会迅速膨胀。第四层是长对话自动累积。很多项目不是一次生成成功,而是几十轮迭代。每一轮都要重新携带之前的系统提示词、历史消息和上下文,总消耗会随轮次增长。
用一个表格可以更直观表达这些消耗来源:
| 消耗来源 | 典型内容 | token 量级(示例) |
|---|---|---|
| 初始需求与规范 | 中文需求、技术约束、目录结构说明 | 1500 - 3000 |
| 单次代码输出 | 完整源码与注释 | 5000 - 30000 |
| 错误日志回填 | 多行堆栈、日志片段 | 2000 - 10000 |
| 历史对话累积 | 30 轮以上迭代上下文 | 数万至数十万 |
| 全量项目上下文 | 代码库、规范文档反复注入 | 十万至百万级 |
当迭代轮数达到几十轮、上下文里又始终携带整份代码和日志时,单次请求就可能消耗几万甚至十几万 token。再乘上几十轮迭代,总消耗达到千万级甚至亿级并不奇怪。
1.3 为什么有人狂烧 6.9 亿,有人 5 美元复刻
同样是“AI 做游戏”,两种不同思路会产生截然不同的成本曲线。
第一种思路是直接把所有内容堆进上下文,让模型一口气完成。开发过程中,开发者每遇到一次报错,就把完整日志和历史对话继续追加进去。这种方式实现最直接,但每一轮都会重复计算大量历史 token,而且输出内容往往包含大量解释性文字、示例代码和无用注释。时间一长,消耗会呈现指数级增长。
第二种思路是模块化拆解。先把游戏拆成入口、游戏循环、碰撞检测、界面渲染等独立模块,每一轮只给模型当前模块的最小上下文。同时压缩系统提示词、限制输出格式、对历史对话做摘要,并对完全相同的请求启用结果缓存。这样每一轮消耗可能只有几千 token,十几轮迭代下来消耗依然可控。
两种路径可以这样对比:
| 对比维度 | 无约束长上下文路线 | 有预算控制路线 |
|---|---|---|
| 每轮上下文大小 | 数万至数十万 token | 数千 token |
| 输出控制 | 自由生成,附带大量解释 | 结构化输出,只返回代码 |
| 历史处理 | 全量保留 | 摘要 + 最近轮次 |
| 缓存使用 | 基本不用 | 重复请求直接命中缓存 |
| 总消耗量级 | 容易达到百万级以上 | 往往能控制在十万级以下 |
所以,所谓“狂烧”和“低成本复刻”,本质不是模型能力差异,而是工程策略差异。
2. 把 6.9 亿 token 和 5 美元换算成可计算的成本
2.1 输入、输出、缓存:计费单价完全不同
模型 API 的计费并不是“一个 token 一个价”。输入 token 和输出 token 的单价通常不同,输出单价往往明显高于输入单价。部分服务商对缓存命中的输入 token 提供更低的折扣价,对长上下文中的反复请求也可能有特殊计费策略。
因此,在比较“6.9 亿 token”和“5 美元”时,不能直接说“6.9 亿 token 一定值多少钱”。必须知道这些 token 是输入还是输出,是否命中缓存,以及使用的是什么模型。至少要把输入和输出拆分估算,才能得出一个相对可信的数量级。
2.2 用一张估算表理解数量级
下面用一个示例单价来说明数量级。这个价格不代表任何模型的真实报价,仅用于帮助理解。
| 估算项目 | 数值 |
|---|---|
| 示例输入单价 | 3 美元 / 百万 token |
| 示例输出单价 | 15 美元 / 百万 token |
| 6.9 亿 token 按输入输出 4:1 估算 | 输入 5.52 亿,输出 1.38 亿 |
| 对应估算成本 | 约 3700 美元量级 |
| 5 美元按输入单价折算 | 约 166 万输入 token |
| 5 美元按输出单价折算 | 约 33 万输出 token |
从这张表可以看出,6.9 亿 token 和“5 美元”之间并不是同一个数量级的对比。能在很小预算内完成相近结果,说明实现路径必然在“每轮输入 token 数”“输出 token 数”“调用轮次”上都做了大幅压缩。
需要强调,真实项目的成本计算不能靠这种含糊估算。落地时要根据模型官方计费页、实际调用中的usage返回值和当前区域的定价进行逐项计算。
2.3 影响成本的核心变量
第一个变量是模型单价。不同模型之间的输入输出价格差距可能达到数倍甚至数十倍。使用同一个任务,选择不同模型,成本差异非常大。
第二个变量是每轮请求的上下文长度。系统提示词越长、历史消息越多、每次注入的代码库越大,单次请求的输入 token 就越高。把上下文从 8 万 token 压缩到 1 万 token,成本并不是简单减少到八分之一,因为在长文本场景中,服务商可能还会引入额外的上下文计费规则。
第三个变量是调用轮次。很多任务不是一次调用就能完成,如果反复把错误日志回传、反复让模型重写整个文件,轮次会迅速增加。减少无效调用轮次,是控制成本最直接的手段。
第四个变量是输出长度。输出 token 单价更高,所以模型生成的“废话”越多,成本越高。通过结构化输出、限制生成范围、控制max_tokens,能减少不必要的输出。
第五个变量是缓存命中率。如果大量请求的 prompt 前缀完全相同,缓存命中后成本会显著降低。这也是“低成本复刻”类方案里最常见的手段之一。
3. 统一模型调用入口,先给项目装上 token 计量表
3.1 为什么要统一入口
很多项目在接入大模型 API 时,会在各个业务模块里直接调用 SDK,导致用量散落各处。服务上线后想排查“哪条业务路径烧钱最多”,完全无从下手。正确做法是给所有模型请求加一个统一网关层,让每个请求都必须经过同一个入口。
统一入口能带来几个直接收益:所有调用统一获得usage数据;可以直接按项目名、模型名、日期记录用量;可以统一插入限流、缓存、重试和熔断逻辑;在模型切换或计价调整时,只改一个地方。
3.2 最小版 LLM 网关
下面是一个最小结构的 Python 网关示例。它不绑定任何具体 SDK,只展示核心逻辑。
class LLMGateway: def __init__(self, storage): self.storage = storage def complete(self, *, model, messages, max_tokens=1024, project="default"): response = self._call_model_api( model=model, messages=messages, max_tokens=max_tokens, ) usage = response["usage"] self.storage.record( project=project, model=model, input_tokens=usage["input_tokens"], output_tokens=usage["output_tokens"], ) return response["content"] def _call_model_api(self, *, model, messages, max_tokens): # 实际项目在这里接入你自己的模型 SDK, # 并保证返回结构里包含 usage 字段。 raise NotImplementedError这个网关的核心约束是:所有模型调用都必须返回usage,并且所有调用的用量都必须落库。缺少任何一项,后续成本统计都会变成盲人摸象。实际项目中,usage的字段名可能叫prompt_tokens、completion_tokens、input_tokens、output_tokens,也可能有单独的reasoning_tokens字段,解析时要以具体 SDK 的返回结构为准。
3.3 用量落到 SQLite,方便后续聚合
对于中小项目,用量记录可以先落到 SQLite。表结构不需要过于复杂,能支撑按日期、按项目、按模型的聚合统计即可。
CREATE TABLE IF NOT EXISTS token_usage ( id INTEGER PRIMARY KEY AUTOINCREMENT, project TEXT NOT NULL, model TEXT NOT NULL, input_tokens INTEGER NOT NULL, output_tokens INTEGER NOT NULL, created_at TEXT NOT NULL );对应 Python 写入逻辑:
import sqlite3 from datetime import datetime class SqliteTokenStorage: def __init__(self, db_path): self.conn = sqlite3.connect(db_path) self.conn.execute(""" CREATE TABLE IF NOT EXISTS token_usage ( id INTEGER PRIMARY KEY AUTOINCREMENT, project TEXT NOT NULL, model TEXT NOT NULL, input_tokens INTEGER NOT NULL, output_tokens INTEGER NOT NULL, created_at TEXT NOT NULL ) """) self.conn.commit() def record(self, *, project, model, input_tokens, output_tokens): self.conn.execute( "INSERT INTO token_usage (project, model, input_tokens, output_tokens, created_at) VALUES (?, ?, ?, ?, ?)", (project, model, input_tokens, output_tokens, datetime.now().isoformat()), ) self.conn.commit()生产环境不建议直接在每台应用服务器上各建一个 SQLite 文件。多实例部署时,需要把用量记录写入集中式数据库或日志采集平台,避免出现重复统计或统计遗漏。
4. 降低单次调用 token 消耗的四种手段
4.1 精简 system prompt,去掉样板话
系统提示词是每轮请求都会携带的固定输入。如果系统提示词很长,它会成为整个项目最稳定的 token 消耗来源。
下面是一个对比示例。原始版本:
你是一个资深游戏开发专家。你擅长使用 Python 和 Pygame 开发各种小游戏。在编写代码之前,你要先仔细分析需求,然后列出游戏规则,再编写完整代码。代码中需要包含尽量详细的中文注释。你还需要解释每段代码的作用,并提供运行说明。压缩后版本:
你是一位 Python 游戏开发助手。规则: 1. 只输出代码,不解释。 2. 代码必须有中文注释。 3. 使用 Pygame 实现。压缩后的提示词保留了任务目标、输出格式和行为约束,去掉了大量人设描述和冗余铺垫。对于固定规则,写成编号列表比写成长段文字更节省 token,也更容易被模型遵循。
4.2 用结构化输出约束模型不要“话痨”
很多 token 的浪费来自模型输出的解释性文字。如果业务只关心最终代码或结果,可以让模型直接输出结构化内容,并约定字段含义。
示例:
{ "code": "完整的 pygame 代码字符串", "assets": ["每个资源文件的生成方式"], "steps": ["运行步骤"] }在提示词里告诉模型“只输出 JSON,不要输出其他内容”。这样模型会收敛到指定字段,就不会反复输出“好的,下面我来实现”这类无效内容。需要注意的是,JSON 里的字段名本身也会占用 token,所以字段名不宜过长,字段数量也应该控制在业务必需范围内。
4.3 历史上下文裁剪与摘要压缩
长对话里最消耗 token 的是历史。随着轮次增加,历史消息会反复携带旧代码、旧日志、旧讨论。常见优化方式是“保留最近 N 轮 + 对早期内容做摘要”。
def compress_history(history, max_round=10, summarize_fn=None): if len(history) <= max_round: return history early = history[:-max_round] recent = history[-max_round:] if summarize_fn is None: summary = "早期上下文已省略" else: summary = summarize_fn(early) return [{"role": "system", "content": f"早期对话摘要:{summary}"}] + recent摘要本身可能也是一次模型调用,也会产生 token。只有当摘要占用的 token 远低于直接携带早期历史时,这种替换才划算。更简单的做法是直接丢弃早期原始文本,只保留最近几轮的上下文。对于游戏开发这类任务,早期需求已经转换为代码,后续轮次并不需要反复携带完整需求原文。
4.4 相同请求结果直接走缓存
最省 token 的方式,是避免产生第二次相同调用。对于输入完全相同的请求,可以按模型和消息内容生成哈希键,直接在缓存中返回结果。
import hashlib class PromptCache: def __init__(self): self.data = {} def get(self, model, messages): key = self._key(model, messages) return self.data.get(key) def set(self, model, messages, result): key = self._key(model, messages) self.data[key] = result @staticmethod def _key(model, messages): raw = model + "|" + repr(messages) return hashlib.sha256(raw.encode("utf-8")).hexdigest()这个方案只适合“完全相同”或“高度可复用”的 prompt,不适合开放式生成。生产环境可以使用 Redis 这类外部缓存,并设置过期时间。缓存命中后,模型调用没有发生,对应的 token 消耗也为零,这是压缩成本效果最显著的手段之一。
5. 用量统计、预算告警与验证
5.1 按天、按项目、按模型聚合
有了统一网关和用量存储,就可以用 SQL 做聚合分析。下面这条查询按天、项目、模型汇总输入输出 token:
SELECT date(created_at) AS day, project, model, sum(input_tokens) AS input_tokens, sum(output_tokens) AS output_tokens, sum(input_tokens) + sum(output_tokens) AS total_tokens FROM token_usage GROUP BY day, project, model ORDER BY day DESC;这一步能快速回答“今天哪个项目消耗最多”“是输入还是输出占大头”这类问题。建议把这条 SQL 固化成定时任务,每日生成一份用量报告。
5.2 用脚本设置软线和硬线
成本控制不能只靠事后分析,还需要在达到阈值时触发告警。可以在统计脚本里设置两个阈值:软阈值用于提前提醒,硬阈值用于彻底拦截。
daily_limit = 500_000 # 示例:每日硬预算 50 万 token warn_ratio = 0.8 # 假设 total 是当天统计出的实际消耗 if total >= daily_limit: print("ALERT: daily budget exceeded, block new requests") elif total >= daily_limit * warn_ratio: print("WARN: daily budget reaches 80%, prepare to throttle")真实系统中,软线附近可以增加限流概率,硬线触发后可以临时拒绝非核心请求。需要注意的是,模型请求通常是异步的,统计结果可能滞后,所以硬线阈值要留有余量,不能卡在精确的预算上限上执行拦截。
5.3 调优前后做对比验证
做任何 token 优化,都要先记录基线,再对比优化前后的差异。下表是一个演示示例,实际数据以自己项目的日志为准:
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 单局游戏开发平均调用轮次 | 35 轮 | 10 轮 |
| 每轮平均输入 token | 80000 | 12000 |
| 每轮平均输出 token | 4000 | 1800 |
| 单任务日均总消耗 | 约 294 万 | 约 13.8 万 |
从表中可以看出,优化后的总消耗可能只有优化前的几十分之一。如果任务中还有大量重复请求命中缓存,消耗还能进一步下降。验证时要注意,不要只看“总 token 数下降”,还要确认任务完成度和输出质量没有明显下降。
6. token 相关常见坑和排查路径
6.1 token 费用突增怎么排查
当某个项目的 token 消耗突然飙升时,按这个顺序排查:
- 先按项目、模型、日期聚合,定位是哪个入口消耗增长。
- 检查单次请求平均输入和输出 token 是否变大,如果变大,重点看系统提示词和历史消息是否被改长。
- 检查调用轮次是否增多,重点看是否出现了大量失败重试或错误日志回填。
- 检查有没有代码在循环里重复调用模型,比如把模型调用写进了高频循环。
- 检查日志记录的
usage是否出现异常大的单次消耗,例如有人把整份代码库塞进了 prompt。
大多数费用突增都不是模型价格变了,而是请求形态变了。
6.2 鉴权里的 token 报错不是计费 token
“token”这个词在计算机领域有多个含义。大模型计费中的 token 是文本切分单元,而登录鉴权里常见的access token、refresh token、id token是完全不同的概念。
很多开发者在集成模型 API 时遇到过类似报错:
sign-in failed: login server error: token exchange failed: token endpoint returned status 403 forbidden这类报错与模型费用无关,而是 OAuth 登录流程中的 token 交换失败。排查路径如下:检查 access token 或 refresh token 是否已过期;检查客户端 id、secret、scope 是否配置正确;检查服务器系统时间是否偏差过大,时间偏差会导致 JWT 校验失败;确认账号在目标服务商的可用范围内。如果公司出口网络对 OAuth 域名存在访问限制,也可能导致握手失败,需要从网络连通性角度排查。注意不要混淆“token 用量统计”和“登录 token 交换”两件事。
6.3 JWT 续签场景的注意点
如果项目里使用 JWT 作为访问凭证,常见坑是只在请求失败时才被动刷新。更好的做法是根据 JWT 的exp声明判断剩余有效期,并在过期前提前刷新。
import time def need_refresh(exp_epoch, advance_seconds=300): return time.time() > (exp_epoch - advance_seconds)提前刷新可以避免并发请求同时发现 token 过期,然后同时触发刷新导致请求雪崩。生产环境还需要对刷新操作加锁,保证同一时刻只有一个线程去刷新。
6.4 不要用“字符串长度”估算 token
用中文、英文、代码混合文本时,按字符数估算 token 误差很大。正确做法是:开发环境中使用模型对应的 tokenizer 本地估算;线上环境中直接读取模型返回的usage字段,以官方计费口径为准。任何“我一个字符大概等于多少 token”的换算都只适用于粗略理解,不能用于成本核算。
7. 生产环境最佳实践与可复用清单
7.1 学习环境与生产环境的差异
学习环境里可以把所有请求都打到模型 API,方便快速调试。生产环境必须多考虑几层:
| 关注点 | 学习环境 | 生产环境 |
|---|---|---|
| 用量记录 | 可以不记录 | 必须统一入库,支持按项目和模型聚合 |
| 缓存 | 可加可不加 | 高频重复请求应该做结果缓存 |
| 预算 | 手动看几次即可 | 必须有软告警和硬拦截 |
| 鉴权 | 使用临时 token 即可 | 使用独立密钥,定期轮换 |
| 失败重试 | 手动重试 | 增加退避重试、熔断、幂等 |
| 日志 | 打印到控制台 | 输出结构化日志,关联请求 id |
| 模型版本 | 随意 | 固定版本,升级前先灰度 |
7.2 可复用清单:上线前逐一打勾
以下清单适用于以大模型 API 为核心成本的项目:
- 所有模型调用是否通过统一网关入口?
- 每次调用是否记录了项目、模型、输入 token、输出 token、时间戳?
- 系统提示词是否做过精简,去掉了不必要的长文本?
- 输出是否有限制条件或结构化约束?
- 历史消息是否有裁剪或摘要策略?
- 相同或相似请求是否有缓存?
- 是否设置了每日软告警阈值和硬拦截阈值?
- 是否按天生成用量报告,并检查异常突增?
- 调用密钥和登录 token 是否分开管理,是否有过期刷新机制?
- 是否保留了每次请求的日志,方便在费用异常时回溯?
如果这十项都做到了,基本不会出现“上个月花了多少钱完全不知道”的失控局面。
7.3 下一步扩展方向
在预算控制跑通之后,可以做更精细的治理。第一个方向是模型路由:简单任务走便宜模型,复杂任务走高能力模型,降低平均成本。第二个方向是 Prompt 压缩:对长代码库、长日志做自动摘要,把必须注入的内容压缩到最低。第三个方向是缓存深化:使用服务商提供的上下文缓存,减少长 prompt 重复计算。第四个方向是把用量统计接入监控平台,结合成本面板做每日、每周、每项目的多维分析。
对于刚接触大模型开发的读者,建议先不要追求复杂的成本治理系统,而是先把统一的调用入口和用量记录搭起来。有了数据,优化才有方向;没有数据,任何“省 token”的讨论都只是猜测。