每天打开 API 控制台,看着一个请求动辄几千 token,几个需求迭代下来账户余额就像被开了个小口子。群里也经常有人问:“今天又烧了一百多万 token,换算成钱到底是多少?”“同一个问题多聊了几句,费用直接翻倍,正常吗?”
其实 token 消耗并不是玄学,它有一套清晰的换算逻辑。只要理解了 token 的拆解方式、模型的计费结构,再加上简单的脚本统计,你就能把“烧了多少 token”精确换算成“花了多少钱”,还能在项目里提前设置水位,避免账单失控。
这篇文章就围绕“大模型 API 的 token 成本”展开,适合正在接入大模型 API、做应用开发、或者只是好奇费用构成的朋友。读完你会掌握:token 与字符的关系、常见模型的计费模式、如何用开源库精确统计 token、如何写一个费用估算器,以及生产环境里的省钱与监控手段。
1. 背景:为什么“烧 token”成了开发者的日常焦虑
以前做 NLP 项目,成本焦虑来自 GPU 租用时长;现在接入大语言模型 API,压力变成了 token 的消耗速度。只要你的应用里有一段 prompt,每一次用户提问,底层都会把文本拆成 token 放进模型,输出时再按 token 生成结果。这个过程本质上是被精确计费的。
“每天烧 token”不是抽象概念,而是实实在在的账单。特别是做以下事情时,消耗会非常明显:
- 聊天机器人:每轮都要携带历史上下文,用户聊得越久,单次请求的 token 越大。
- 文档总结:把长文档整段塞进 prompt,一次可能吃掉几万 token。
- 代码生成与重构:代码文本中空格、缩进、符号都会被拆分,token 密度和自然语言不同。
- 数据抽取与批量处理:一次任务循环十几次、上百次 API 调用,token 总数快速累积。
如果不懂计费规则,很容易出现“功能做好了,账单也爆了”的情况。反过来,只要掌握 token 的统计方法,成本是可以提前算出来的。
2. token 是什么?按 token 计费背后的逻辑
2.1 token 与字符的区别
大多数中文开发者第一次接触 token 时,容易把它理解为“字数”或“字符数”。其实 token 是模型进行文本处理的最小单位,可以理解为“词的碎片”。一个 token 可能是一个完整的英文单词,也可能是半个词、一个标点,甚至是一个空格。
以 OpenAI 官方文档中常用的例子为例:
英文字符串Hello world, how are you?大约会被拆分为 6 个 token。
中文文本则更特殊。由于中文字符的信息密度高,通常一个字可能对应 1 到 2 个 token,甚至更多。下面是一个粗略参考:
| 文本类型 | 1 个 token 大约对应 |
|---|---|
| 英文单词 | 0.7 到 1 个单词 |
| 中文汉字 | 0.5 到 1 个汉字 |
| 英文标点/空格 | 可能单独成 1 个 token |
| 多语言混合文本 | 不稳定,需要实测 |
所以在描述 token 数时,不要简单说“这篇文章有多少字”,要说“这段文本会被 tokenizer 拆成多少个 token”。tokenizer 是决定拆分结果的关键组件。
2.2 为什么按 token 计费
大语言模型底层是通过预测 token 的概率来生成内容的。模型推理时,计算量基本和输入 token 数 + 输出 token 数成正比。也就是说,你发送多少个 token、模型回复多少个 token,直接决定了 GPU 的计算开销。
因此 API 厂商按 token 计费是合理的:
- 输入 token 多,意味着模型需要“阅读”更多内容。
- 输出 token 多,意味着模型需要“思考并生成”更多内容。
这也是为什么上下文长度、输出长度限制(max tokens)都和 token 紧密相关。token 既是计算单位,也是计费单位。
2.3 不同模型的 tokenizer 差异
不同模型系列使用的 tokenizer 并不完全一致。比如 OpenAI 的 GPT-4 系列和 GPT-3.5 系列使用 tiktoken 库,不同模型对应不同的编码器;Claude 系列有自己的 tokenizer;国内各大模型厂商也各自实现分词逻辑。
这意味着同样一段中文,在不同模型下统计出的 token 数量可能有差异。在做成本预估时,最好优先使用模型官方提供的 tokenizer 或计数接口,不要跨模型套用经验值。
3. 大模型 API 计费模式与价格结构解析
各家 API 的计费结构大同小异,核心是“输入价格 + 输出价格分开计算”。以下是一个演示用价格表,仅用于理解结构,真实价格请以官方控制台为准。
| 计费项 | 示例价格 | 说明 |
|---|---|---|
| 输入 token | 0.005 元 / 1K tokens | prompt、历史记录、工具返回结果都算输入 |
| 输出 token | 0.015 元 / 1K tokens | 模型生成的回复内容 |
| 缓存命中输入 | 0.001 元 / 1K tokens | 未变更的上下文缓存命中时更便宜 |
| 特殊服务 | 按次或按资源计费 | 比如函数调用、图片输入、批量任务 |
从这个结构里可以得出几个关键结论:
- 输出 token 单价通常高于输入 token。避免模型废话、限制 max tokens,是省钱的重点。
- 输入 token 也不是免费的。系统提示词、历史消息、工具返回结果,都会循环计费。
- 同一模型在处理长上下文时可能存在“缓存计费”。如果的对话上下文中大部分内容没变,命中缓存的部分可能便宜一些。
3.1 输入与输出分开计费的影响
假设你设计了一个 prompt,固定系统提示词 1000 token,每次用户问题约 200 token,模型回复约 500 token。那么一次请求的计费构成是:
输入部分:1000 + 200 = 1200 token 输出部分:500 token如果用户连续对话 10 轮,每轮都把历史消息带上去,第二轮输入就会变成:
历史第1轮 (1200 + 500) + 第2轮用户问题 200 = 1900 token这还只是第二轮。到了第 10 轮,输入 token 会迅速膨胀到上万。这就是“越聊越贵”的根本原因。
3.2 上下文缓存的省钱逻辑
部分模型支持上下文缓存。也就是说,前面已经计算过的 prompt 如果原样出现,可以按缓存价计费。例如系统提示词很长,每次请求都带相同内容,这部分如果命中缓存,费用可以降低不少。
但缓存并不是自动对所有文本生效的,一般有前缀匹配、缓存有效期等限制。使用时需要查看对应平台的文档。
4. 如何准确估算 token 消耗
4.1 使用 tiktoken 精确统计 token
如果你想精确知道一段文本会消耗多少 token,可以使用 OpenAI 开源的tiktoken库。它是一个 Python 库,用于将文本编码为 token,官方模型 GPT 系列基本都支持。
安装方式:
pip install tiktoken下面是一个最基础的统计示例:
# 文件路径:token_count_demo.py import tiktoken # 指定模型对应的编码器 # 新模型一般对应 "cl100k_base" encoding = tiktoken.get_encoding("cl100k_base") text = "今天 token 消耗是多少?Hello, how much will this cost?" tokens = encoding.encode(text) print("文本内容:", text) print("token 数量:", len(tokens)) print("token 列表:", tokens)运行结果大致为:
文本内容: 今天 token 消耗是多少?Hello, how much will this cost? token 数量: 26 token 列表: [32844, 30210, ...]如果用的是按模型名获取编码器的方式,可以这样写:
import tiktoken encoding = tiktoken.encoding_for_model("gpt-4") tokens = encoding.encode("你好,请帮我总结这段文本。") print(len(tokens))encoding_for_model会自动根据模型名称选择合适的编码器。模型名称需要和 tiktoken 库支持列表一致,实际使用时要确认库版本和模型是否匹配。
4.2 常用文本的 token 数量参考
为了帮助你建立直觉,下面是一些常见场景的 token 估算,数值会因模型和 tokenizer 有少量浮动:
| 场景 | 文本长度参考 | token 范围 |
|---|---|---|
| 一句话 | 20 个汉字左右 | 15 - 30 |
| 一个标准网页正文 | 500 汉字 | 300 - 600 |
| 一份会议纪要 | 3000 汉字 | 2000 - 4000 |
| 一份技术文档 | 1 万汉字 | 7000 - 15000 |
| 一个完整代码文件 | 200 行 | 1000 - 3000 |
可以看出,中文文本的 token 数并不等于汉字数,有时 1 个汉字约等于 1 个 token,但遇到英文、数字、代码符号混合时,波动范围很大。生产环境建议直接调用 tokenizer 统计,不要凭感觉预估。
4.3 估算多轮对话的总成本
如果要做成本预估,不能只看单次 prompt 的 token 数,还要把“请求次数”和“对话轮次”算进去。
多轮对话成本估算公式可以简化成:
单轮成本 = 输入token数 * 输入单价 + 输出token数 * 输出单价 总成本 = 所有轮次成本之和其中第 N 轮输入 token 数并不是固定的,而是等于“历史消息 token + 新增用户消息 token”。下面会给出自动计算的示例。
5. 完整实战:写一个 token 费用估算器
这一节我们实现一个小工具,读取对话消息列表,统计每一轮的 token 消耗,并按照配置的单价换算成费用。
5.1 项目结构
token-cost-estimator/ ├── estimator.py # 核心估算逻辑 ├── token_counter.py # token 统计封装 ├── example_usage.py # 示例调用 └── requirements.txt # 依赖列表5.2 安装依赖
创建requirements.txt:
tiktoken>=0.5.0安装:
pip install -r requirements.txt5.3 实现 token 统计封装
先建立一个统一的 token 统计模块,方便切换不同模型的编码器。
# 文件路径:token_counter.py import tiktoken class TokenCounter: def __init__(self, model="gpt-4"): # 根据模型名获取编码器,如果模型不支持,可以回退到 cl100k_base try: self.encoding = tiktoken.encoding_for_model(model) except KeyError: self.encoding = tiktoken.get_encoding("cl100k_base") def count(self, text: str) -> int: if not text: return 0 return len(self.encoding.encode(text)) def count_messages(self, messages: list) -> int: """ 计算一组对话消息的 token 总数。 messages 格式:[{"role": "user", "content": "你好"}, ...] """ total = 0 for msg in messages: # 每条消息包含角色和内容两部分 total += self.count(msg.get("role", "")) total += self.count(msg.get("content", "")) return total这里把每条消息的role也统计进去,是因为 API 实际计费时,角色标识同样会参与 token 拆分。虽然每个角色词只占几个 token,但在多轮对话中,所有历史角色叠加后也不容忽视。
5.4 实现费用估算逻辑
接下来实现核心估算类。它负责维护“历史消息”,并在每次新增用户消息时计算本轮费用。
# 文件路径:estimator.py from token_counter import TokenCounter class CostEstimator: def __init__(self, input_price=0.005, output_price=0.015, model="gpt-4"): """ input_price: 每 1K 输入 token 的价格(演示用) output_price: 每 1K 输出 token 的价格(演示用) """ self.counter = TokenCounter(model) self.history = [] self.input_price = input_price self.output_price = output_price def add_user_message(self, content: str) -> dict: self.history.append({"role": "user", "content": content}) input_tokens = self.counter.count_messages(self.history) # 模拟模型输出,实际项目中这里应该替换成真实 API 返回结果 reply_content = f"收到,当前问题已经理解。你的问题是:{content[:20]}" self.history.append({"role": "assistant", "content": reply_content}) output_tokens = self.counter.count(reply_content) input_cost = input_tokens / 1000 * self.input_price output_cost = output_tokens / 1000 * self.output_price total_cost = input_cost + output_cost return { "input_tokens": input_tokens, "output_tokens": output_tokens, "total_tokens": input_tokens + output_tokens, "input_cost": round(input_cost, 6), "output_cost": round(output_cost, 6), "total_cost": round(total_cost, 6), } def reset(self): self.history = []为了让示例完整,代码里用了一段模拟回复。在实际项目中,你只需要把add_user_message里的模拟回复替换成真实 API 调用,再把返回的完成文本传回即可。
5.5 运行示例与结果说明
创建一个测试入口:
# 文件路径:example_usage.py from estimator import CostEstimator # 以演示价格为例:输入 0.005 元/1K,输出 0.015 元/1K estimator = CostEstimator(input_price=0.005, output_price=0.015) questions = [ "帮我解释一下什么是大模型 token?", "那 API 是怎么按 token 收费的?", "如果我想降低费用,有什么建议?", ] for q in questions: result = estimator.add_user_message(q) print(f"用户问题:{q}") print(f" -> 输入 token:{result['input_tokens']}") print(f" -> 输出 token:{result['output_tokens']}") print(f" -> 本轮费用:{result['total_cost']} 元") print() print(f"总历史消息数:{len(estimator.history)} 条")运行:
python example_usage.py你会在输出中看到,随着问题数量增加,每一轮的输入 token 都在累积。比如第一轮的输入 token 只有几百,第三次可能已经超过一千。这说明多轮对话的历史上下文是成本增长的主要来源。
这个脚本本身不产生真实 API 费用,所以可以放心运行。当你接入真正 API 后,可以进一步把返回的usage.prompt_tokens和usage.completion_tokens取出来,替换掉本地的统计值,准确度会更高。
6. 成本监控与告警方案
光靠本地估算还不够,生产环境里必须有线上监控,否则哪天缓存命中率下降、上下文长度变大,账单就会悄悄上涨。
6.1 记录每次调用的 token 用量
不论使用哪个平台的 SDK,响应结果里一般都会包含用量信息。以常见 OpenAI 风格接口为例,响应中可能有usage字段:
{ "usage": { "prompt_tokens": 1200, "completion_tokens": 500, "total_tokens": 1700 } }封装一个简单的日志记录函数:
# 文件路径:usage_logger.py import sqlite3 import datetime def init_db(db_path="usage.db"): conn = sqlite3.connect(db_path) conn.execute( """ CREATE TABLE IF NOT EXISTS api_usage ( id INTEGER PRIMARY KEY AUTOINCREMENT, request_time TEXT, model TEXT, prompt_tokens INTEGER, completion_tokens INTEGER, total_tokens INTEGER, estimated_cost REAL ) """ ) conn.commit() conn.close() def save_usage(model, prompt_tokens, completion_tokens, estimated_cost): conn = sqlite3.connect("usage.db") conn.execute( "INSERT INTO api_usage (request_time, model, prompt_tokens, completion_tokens, total_tokens, estimated_cost) VALUES (?, ?, ?, ?, ?, ?)", (datetime.datetime.now().isoformat(), model, prompt_tokens, completion_tokens, prompt_tokens + completion_tokens, estimated_cost), ) conn.commit() conn.close()这样做的好处是,后续可以按天、按模型统计消耗趋势。
6.2 设置每日消耗告警
你可以写一个定时任务,统计当天累计 token 和费用,超过阈值就发送告警。下面是简洁版:
# 文件路径:daily_report.py import sqlite3 import datetime def daily_cost_report(): today = datetime.date.today().isoformat() conn = sqlite3.connect("usage.db") row = conn.execute( """ SELECT COUNT(*), SUM(total_tokens), SUM(estimated_cost) FROM api_usage WHERE request_time LIKE ? """, (today + "%",), ).fetchone() conn.close() count, total_tokens, total_cost = row print(f"日期:{today}") print(f"调用次数:{count}") print(f"总 token:{total_tokens or 0}") print(f"估算费用:{total_cost or 0} 元") limit = 5.0 if (total_cost or 0) > limit: print("告警:今日费用超过设定阈值!") if __name__ == "__main__": daily_cost_report()注意:告警只是辅助手段,更重要的是在业务层限制单次请求的max_tokens,以及限制多轮对话的最大轮数。否则即使有告警,垃圾请求也可能在告警触发前就已经打完了。
6.3 可观测性数据落库
除了 token 用量,还建议记录:
- 请求来源业务线。
- 用户 ID 或会话 ID。
- 系统提示词的版本号。
- prompt 是否命中缓存。
- 请求耗时。
这些数据能帮助你快速定位是哪个功能、哪个用户、哪个 prompt 在消耗成本。比如某天费用突然上涨,先看模型版本是不是变了,再看是不是某个会话的历史消息没有清理。
7. 常见问题与排查思路
我在实际排查成本问题时,发现下面几个问题出现频率最高。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 明明只问了一句,账单却不低 | 请求里带了大量 system prompt 和工具结果 | 精简系统提示词,删除无用历史工具输出 |
| 对话多轮后费用快速增长 | 历史消息全部追加到上下文 | 做滑动窗口,只保留最近 N 轮 |
| 同一段 prompt 重复计费很多次 | 没有开启上下文缓存或缓存未命中 | 调整 prompt 结构,前缀完全一致,确认缓存有效期 |
| 实际费用比本地预估多 | 本地使用的 tokenizer 与模型不一致 | 使用模型官方 tokenizer,或以 API 返回的 usage 为准 |
| 输出 token 超预期 | max_tokens 设置过大 | 设定合理的 max_tokens,必要时做回复长度约束 |
| 并行请求过多,消耗像坐火箭 | 没有做批量控制和并发限流 | 增加信号量限制并发,必要时排队处理 |
下面展开两个典型问题的排查方法。
7.1 历史消息无限增长导致费用翻倍
现象:用户会话超过 10 轮后,请求的 prompt_tokens 已经超过 5000,费用明显上升。
排查:查看请求体里的 messages 数组长度和内容。如果每轮都原样带上全部历史,就会线性增长。
解决:对历史消息做裁剪。例如只保留最近 5 轮,或者把超过 3000 字符的历史消息先做一轮摘要再放回上下文。
def trim_history(messages, max_tokens=2000): from token_counter import TokenCounter counter = TokenCounter() kept = [] for msg in reversed(messages): msg_tokens = counter.count_messages([msg]) if sum(counter.count_messages([m]) for m in kept) + msg_tokens > max_tokens: break kept.insert(0, msg) return kept7.2 系统提示词过长导致单次请求成本高
现象:每次请求都固定消耗 2000 token,几乎不会带来额外的回答收益。
排查:把系统提示词单独拿出来统计 token,对比用户问题 token。
解决:压缩系统提示词,把必要说明用简洁的短句表达;如果提示词必须很长,尽量保证内容稳定,以触发缓存计费。
8. 最佳实践:降低 token 消耗的工程手段
成本优化并不是把模型换成最便宜的就完事了。以下几个手段结合使用,效果更明显。
8.1 精简 prompt,去掉“寒暄”和重复说明
模型不会因为你写“你好呀,请帮我写一个项目方案,谢谢你啦”就更认真,这些字也会被计入 token。系统提示词里只保留必要规则,去掉语气词和重复强调。
8.2 控制上下文长度,优先使用滑动窗口
对聊天类应用,没必要把三个月前的对话都塞给模型。推荐策略:
- 固定最近 N 轮完整消息。
- 更早的消息用摘要替换。
- 超长文档先检索相关片段,而不是全文塞入。
8.3 合理设置 max_tokens
max_tokens控制的是输出 token 上限,而不是“一定会输出这么多”。但设置过大会在异常生成时产生多余成本。常见做法是设置为期望回复长度的 1.2 到 1.5 倍。
8.4 使用上下文缓存
如果系统提示词很长且很少变化,先确认 API 平台是否支持缓存。使用缓存时,prompt 的前缀必须保持稳定。不要在每个请求前面拼接动态时间戳、随机内容,这会破坏缓存命中。
8.5 批处理与并发控制
批量任务不要一次性全部并发请求。一方面容易触发速率限制,另一方面一旦中间有问题,无法及时熔断。推荐用生产者消费者模式,控制同时进行的请求数量。
8.6 对模型输出做结构化约束
让模型输出 JSON 时,使用函数调用或结构化输出格式,比让模型自由发挥更容易控制长度。这一点对降低输出 token 成本很有效。
8.7 按场景选择模型
不是所有任务都需要最强模型。简单分类任务、关键词抽取,使用轻量模型足以完成;复杂推理、长文档深度分析,才使用更强的模型。把请求按难度分发到不同模型,整体成本会下降很多。
8.8 记录成本和质量的对应关系
成本优化不能只盯着数字。推荐建立一个评估集,每隔一段时间对比模型输出质量。如果换了一个便宜模型后,业务指标明显下滑,那么省钱是得不偿失的。
9. 总结与后续建议
回到开头的问题:“每天烧 token,我需要花多少钱?”其实它不再是模糊的疑问,而是可以被回答的工程问题。
这个回答由三部分组成:
- 把业务文本交给官方 tokenizer 统计 token。
- 根据 API 的输入/输出单价算出每次调用的费用。
- 记录线上用量,观察每天、每周的趋势,提前设置告警。
在此基础上,通过精简 prompt、限制历史上下文、合理使用缓存、按场景分流模型,可以在不牺牲效果的前提下把费用降下来。
我建议你下周就做一次“成本体检”:打开你的 API 日志或数据库,按模型、按功能统计 token 排名前 10 的请求,看看是哪些场景在消耗大头。很多项目的优化点不是模型选错了,而是某个循环里重复携带了整段长文档。
下一步可以继续研究:多轮对话的摘要压缩算法、RAG 场景下的检索片段截取策略、以及大模型 API 网关层面的统一限流。成本控制是一项长期工程,养成每周查看用量明细的习惯,比等账单出来再吃惊要划算得多。