什么是 Token,以及如何精确统计:分词原理、计数方法与成本优化
做 AI 应用做到一定阶段,团队几乎都会撞上同一类问题:
- 调一次大模型到底花了多少 token?账单为什么比预期高?
- 同样一句话,为什么中文折算的 token 比英文多那么多?
- 上下文窗口说 128K,我到底能塞多少字?超了会怎样?
- 想在调用前预估成本,到底该用什么工具算 token?
- 不同模型(GPT、Claude、Llama)的 token 是一回事吗?数字能通用吗?
- "1 个 token ≈ 4 个字符" 这种说法,到底准不准?
这些问题之所以反复出现,不是因为 token 这个概念本身多复杂,而是因为很多文章只讲"1 个 token 大概是 3/4 个英文单词"这种粗略定义,不讲分词原理,不讲各家分词器的差异,也不讲工程上到底该怎么精确计数和省钱。
这篇文章不打算停留在"token 是文本片段"这种百科词条层面,而是按真实工程视角把 token 讲清楚:
- token 到底是什么,模型是怎么把文本切成 token 的
- 为什么 OpenAI、Anthropic、Meta 的 token 彼此不通用
- 如何用
tiktoken、厂商 API、开源工具精确统计 token - 中文和英文的 token 效率为什么差很多,这对成本意味着什么
- token 和计费、上下文窗口、缓存的关系
- 在真实 AI 应用里,应该如何管理 token 预算、避免踩坑
如果你是做 LLM 应用、RAG、智能客服或 AI 中台的后端工程师,这篇文章应该能帮你建立一套从"理解 token"到"精确统计"再到"成本优化"的完整判断框架。
先立边界:Token 不是字符,也不是词
这是最值得先讲清的一段。很多成本估算出错,不是因为算错了,而是从一开始就把 token 和"字"或"词"搞混了。
三种"文本单位"的边界
| 单位 | 定义 | 例子("hello world") | 谁在用 |
|---|---|---|---|
| 字符(Character) | 一个字母/汉字算一个 | h,e,l,l,o, ,w,o,r,l,d = 11 个 |
传统编程、len() |
| 词(Word) | 按空格/标点切分的单词 | hello, world = 2 个 |
传统 NLP、分词统计 |
| Token | 模型分词器切出的子词单元 | hello, world = 2 个(常见情况) |
大模型、计费、上下文窗口 |
一句话概括:
- 字符是给人看的长度单位。
- 词是给传统 NLP 用的语义单位。
- token 是给大模型用的、计费和上下文窗口都认的"硬通货"。
这三者数量接近,但绝不相等。最经典也最危险的错误,就是用 len(text)(字符数)去估算 token,然后据此做预算和报价。
为什么不能直接用字符数估算
以英文为例,经验规则是:
- 1 个 token ≈ 4 个字符 ≈ 0.75 个英文单词
但这是"平均",不是"保证"。常见词(the、ing)经常被压成 1 个 token,而生僻词、专有名词、URL、代码反而会被拆成好几个 token。中文的情况更极端,后面会单独展开。
边界先清楚,token 才不会越算越乱。
一张总览表先建立认知
| 概念 | 核心问题 | 单位特征 | 擅长什么 | 不擅长什么 |
|---|---|---|---|---|
| Token | 模型如何计量文本 | 子词(subword),非字符非词 | 计费、上下文窗口、限流 | 直接换算成"字数/词数" |
| 分词器(Tokenizer) | 文本 ↔ token id 的转换 | 每家模型一套词表与算法 | 离线、快速、可复现 | 跨模型通用 |
| BPE | 如何从语料学出词表 | 高频字节对合并 | 兼顾频率与覆盖 | 处理罕见词/多语言需扩词表 |
| 计数(Counting) | 调用前精确知道 token 数 | tiktoken / 厂商 API / 估算 | 成本预估、截断、配额 | 只能针对特定模型精确 |
| 上下文窗口 | 一次能塞多少 token | 输入 + 输出 ≤ 窗口 | 多轮对话、长文档 | 无限长记忆(仍需截断/检索) |
Token 是什么:从文本到模型输入的最小单位
Token,中文常译为"词元"或"标记",是大语言模型处理文本时的最小单位。
它既不是一个完整的词,也不是单个字符,而是介于两者之间的"子词片段"(subword)。模型眼里没有文字,只有一串 token id(整数)。一段文本进模型前,必须先被分词器切成 token,再查表转成 id。
典型代表(分词实现)包括:
- OpenAI 的
tiktoken(GPT 系列) - Anthropic Claude 的自有分词器
- HuggingFace
tokenizers/ GoogleSentencePiece(Llama、Mistral 等开源模型)
它的核心不只是"把文本切碎",而是:
- 按子词切分,让高频词省 token、低频词不丢信息
- 词表大小固定(几万到几十万),任何文本都能被编码
- 切分方式由训练语料统计学出来,不是人手定的规则
- 同一个词在不同模型里,token 数量可能完全不同
它解决的根问题是什么
在语言模型里,有些需求是硬性的:
- 词表必须有限,但又要能表示无限多的文本
- 常见词不能浪费 token(
the不该拆成三个字母) - 罕见词、错别字、生僻字也要能编码,不能"认不出"
- 多语言、代码、emoji 都要覆盖
- 切分必须可复现、可逆(能从 token 还原回文本)
这类问题的共同特征是:
- 要兼顾频率(高频短、低频长)
- 要兼顾覆盖(不能有"无法表示"的文本)
- 要确定性(同一段文本,同一家模型永远切成同样的 token)
token 和分词器就是为这类问题设计的。
BPE:决定 token 怎么切的核心算法
绝大多数现代分词器(包括 GPT、Claude、Llama)的底层都是 Byte Pair Encoding(BPE,字节对编码)。
BPE 的思路很朴素:
- 一开始,把文本拆成最小单位(字节或字符)。
- 统计语料里出现频率最高的相邻一对,把它们合并成一个新 token。
- 不断重复第 2 步,直到词表达到目标大小。
结果是:高频组合(ing、tion、http)被合并成单个 token,低频或罕见内容退回成更细的片段甚至单字节。这就是为什么"1 个 token ≈ 4 个字符"是平均值——常见片段远低于 4 字符/token,罕见片段远高于。
下面用 OpenAI 的 tiktoken 实际看看一段文本被切成了什么:
import tiktoken
# gpt-4o 使用 o200k_base 词表;gpt-4 / gpt-3.5 用 cl100k_base
enc = tiktoken.encoding_for_model("gpt-4o")
text = "Hello, world! 什么"
token_ids = enc.encode(text)
print("token 数:", len(token_ids))
print("token id 序列:", token_ids)
print("每个 token 还原回的文本:")
for tid in token_ids:
print(repr(enc.decode([tid])))
运行后你会看到类似这样的结果:
token 数: 7
token id 序列: [13225, 11, 5905, 0, 24402, 104421, 104421]
每个 token 还原回的文本:
'Hello'
','
'world'
'!'
'什'
'么'
'么' # (不同模型/版本切法略有差异,以实际运行为准)
这个例子的价值在于:
Hello、world、!这些常见片段是 1 个 token- 中文"什么"被拆成了多个 token——这就是后面要讲的"中文更贵"的根源
- token id 是整数,模型实际处理的就是这串 id
encode/decode是可逆的:enc.decode(enc.encode(text)) == text
为什么每个模型的 Token 不一样:分词器不统一
这是最容易踩的坑:不同模型的 token 彼此不通用。
同一段文本,在 GPT-4o、Claude、Llama 里切成多少个 token,数字可能差不少。原因不是谁"更先进",而是每家在训练模型时,用各自的语料、各自的目标词表大小,学出了各自的 BPE 词表。
三大主流分词器对比
| 分词器 | 代表模型 | 词表/编码 | 是否开源可离线 |
|---|---|---|---|
tiktoken(BPE) |
GPT-4o、GPT-4、GPT-3.5 | o200k_base、cl100k_base、p50k_base 等 |
是(Python/Rust 库) |
| Claude 自有分词器 | Claude 全系列 | 未公开词表 | 否(仅通过 API 计数) |
SentencePiece / tokenizers |
Llama、Mistral、Qwen 等 | 各模型自带 .model / tokenizer 文件 |
是(需加载对应模型词表) |
一句话概括:
- OpenAI 的 token 用
tiktoken离线就能精确算。 - Claude 的 token 只能用官方
count_tokens接口精确算。 - 开源模型的 token 要加载它自己的 tokenizer 文件才算得准。
同一段文本,三家切法不同
import tiktoken
text = "人工智能(AI)正在改变软件开发"
# 仅以 OpenAI 的两套词表举例,体会"同公司不同模型"都不一样
enc_new = tiktoken.encoding_for_model("gpt-4o") # o200k_base
enc_old = tiktoken.encoding_for_model("gpt-4") # cl100k_base
print("gpt-4o :", len(enc_new.encode(text)), "tokens")
print("gpt-4 :", len(enc_old.encode(text)), "tokens")
连 OpenAI 自家两代模型的词表都不一样(GPT-4o 的 o200k_base 对多语言做了优化,中文比 cl100k_base 更省),跨厂商差异只会更大。
经验上的一句直白话:脱离具体模型谈 token 数,等于脱离汇率谈价格。 计数必须对着你真正要调用的那个模型来算。
如何精确统计 Token:四种方法
这是工程上最实用的一节。"如何统计"不是一句话能答完的,要分场景选方法。
方法一:tiktoken(OpenAI 模型,离线、精确、最快)
如果你调的是 OpenAI 模型,tiktoken 是首选:纯本地计算,不需要发请求,毫秒级,结果和官方计费一致。
import tiktoken
enc = tiktoken.encoding_for_model("gpt-4o")
def count_tokens(text: str, model: str = "gpt-4o") -> int:
enc = tiktoken.encoding_for_model(model)
return len(enc.encode(text))
# 但真实计费不是"把所有文本拼一起数",而是按 messages 结构数
# 下面是 OpenAI 官方推荐的对话 token 估算(含每条消息的固定开销)
def count_chat_tokens(messages: list[dict], model: str = "gpt-4o") -> int:
enc = tiktoken.encoding_for_model(model)
# 不同模型每条消息的模板开销不同,这里用 gpt-4o 近似值
num = 0
for msg in messages:
num += 4 # 每条消息固定结构开销(role/分隔符等)
for key in ("role", "content", "name"):
if key in msg and msg[key]:
num += len(enc.encode(msg[key]))
num += 2 # priming
return num
messages = [
{"role": "system", "content": "你是一个简洁的助手。"},
{"role": "user", "content": "什么是 token?"},
]
print("对话 input_tokens ≈", count_chat_tokens(messages))
这段代码的价值在于:
encoding_for_model会自动选对词表,不要手动猜cl100k还是o200k- 真实计费按 messages 结构算,不是把字符串拼起来数——每条消息有固定的模板开销
- 这个估法和官方有少量出入(几 token),但做预算和截断足够稳
方法二:厂商 count_tokens 接口(Claude,精确)
调 Claude 不能用 tiktoken,因为它的词表没公开。Anthropic 提供了专门的计数接口,结果和计费一致:
curl https://api.anthropic.com/v1/messages/count_tokens \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{
"model": "claude-sonnet-4-5",
"messages": [
{"role": "user", "content": "什么是 token?如何统计?"}
]
}'
返回:
{
"input_tokens": 12
}
注意:
- 这个接口不计费,专门用来预估
- 它考虑了 messages 结构、system prompt、tools 定义的真实开销,比任何手算都准
- 代价是要发一次网络请求,不适合在超高频路径上逐条调
方法三:HuggingFace tokenizers / SentencePiece(开源模型)
调 Llama、Mistral、Qwen 等开源模型时,要加载它们各自的 tokenizer 文件:
from transformers import AutoTokenizer
# 以某开源模型为例,加载它自带的 tokenizer
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Meta-Llama-3-8B")
text = "什么是 token?如何统计?"
print("token 数:", len(tokenizer.encode(text)))
print("token id:", tokenizer.encode(text))
要点:
- 必须用目标模型对应的 tokenizer,用错模型就是错数
AutoTokenizer会自动从 Hub 拉对应文件,离线场景需提前下载- 这是本地部署开源模型时最权威的计数方式
方法四:估算规则(快速近似,不依赖模型)
很多时候你不需要精确,只需要一个量级(比如做配额、做产品报价)。这时用经验规则最快:
| 文本类型 | 经验规则 | 适用场景 |
|---|---|---|
| 英文 | 1 token ≈ 4 字符 ≈ 0.75 词 | 快速报价、粗略配额 |
| 中文 | 1 汉字 ≈ 0.6 ~ 1.5 token(差异大) | 仅做上限预估,别拿来做精确账单 |
| 代码 | 比自然语言更费 token(符号多) | 留足余量 |
| 结构化(JSON/工具定义) | 额外加模板开销 | 按 messages 结构估 |
# 一个保守的、不依赖任何库的中文上限估算
def estimate_tokens_naive(text: str) -> int:
# 中文按 1.5 倍向上取整做上限,英文按 4 字符
chinese = sum(1 for c in text if "一" <= c <= "鿿")
other = len(text) - chinese
return int(chinese * 1.5 + other / 4) + 1
这种估法的价值是零依赖、零延迟,但一定要记住它只是上限,不能用它给客户开账单。
四种方法对比
| 方法 | 精确度 | 是否离线 | 速度 | 适用 |
|---|---|---|---|---|
tiktoken |
精确(OpenAI) | 是 | 极快 | OpenAI 模型计费/截断 |
count_tokens API |
精确(Claude) | 否(需请求) | 中 | Claude 模型预估 |
tokenizers/SentencePiece |
精确(对应模型) | 是(需词表) | 快 | 开源模型本地部署 |
| 估算规则 | 近似(误差大) | 是 | 极快 | 配额、报价、量级判断 |
别让"估算"去干"计费"的活,也别让"调 API 计数"去卡每一条请求的性能。 场景选对方法,这是这一节真正想说的。
中文与英文的 Token 效率差异
这是国内团队做 LLM 应用最容易"花冤枉钱"的地方。
同样意思,中文就是更费 token
import tiktoken
enc = tiktoken.encoding_for_model("gpt-4o")
def count(text: str) -> int:
return len(enc.encode(text))
en = "The quick brown fox jumps over the lazy dog."
zh = "敏捷的棕色狐狸跳过了那只懒狗。"
print("英文:", count(en), "tokens /", len(en), "字符")
print("中文:", count(zh), "tokens /", len(zh), "字符")
运行后你会清楚地看到:
- 英文 9 个词,平均下来"不到 1 token/词"
- 中文十来个汉字,几乎每个汉字要 1 个甚至更多 token
| 维度 | 英文 | 中文 |
|---|---|---|
| 每 token 大约覆盖 | ~4 字符 / ~0.75 词 | |
| 原因 | 分词器在英文语料上训练,英文片段高频 | 汉字组合在词表里覆盖不如英文密集 |
| GPT-4o 后是否改善 | — | 有改善(o200k_base 优化了多语言),但仍不如英文 |
| 对成本的影响 | 基线 | 同样内容,中文输入/输出 token 更多 → 更贵 |
为什么这件事重要
这对工程和产品都有直接影响:
- 成本:同一段产品文案,中文版 token 更多,长期调用成本更高
- 上下文窗口:同样 128K 窗口,能塞的中文"篇幅"比英文小
- GEO / AI 可见度:大模型是按 token 消费你的网页内容的,中文页面在同等窗口下能被"读完"的有效内容更少,结构清晰、信息密度高的内容更占优势
- 截断策略:中文场景下,按字符数截断和按 token 截断的差距更大,必须按 token 截
一句话:中文做 LLM 应用,token 预算要按"更费"来估,留足余量。
Token 与计费:输入、输出、缓存
理解了 token,才能看懂账单。绝大多数大模型按 token 计费,而且输入和输出价格不同。
计费的三条基本规则
- 输入 token(input):你发给模型的所有内容(system + 历史 + 用户输入 + 工具定义)。
- 输出 token(output):模型生成的内容。输出通常比输入贵 3~5 倍。
- 总费用 = 输入 token × 输入单价 + 输出 token × 输出单价。
示意价格(仅为说明结构,实际以各厂商官方定价为准):
| 模型(示意) | 输入($/百万 token) | 输出($/百万 token) | 缓存命中输入 |
|---|---|---|---|
| 旗舰大模型 A | ~0.3(约 1 折) | ||
| 中端模型 B | ~0.1 | ||
| 小模型 C | 更低 |
这张表想强调的不是具体数字,而是三件事:
- 输出比输入贵很多——让模型少废话,是最直接的成本优化
- 缓存命中极便宜——重复的长 prompt 走缓存,能省一个数量级
- 不同档位模型价差巨大——能用小模型就别用旗舰
max_tokens:给输出戴上"紧箍咒"
调用时用 max_tokens 限制最大输出长度,是防止"成本失控"的第一道闸门:
from anthropic import Anthropic
client = Anthropic()
resp = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=512, # 输出最多 512 token,硬上限
messages=[{"role": "user", "content": "用一句话解释 token"}],
)
print(resp.usage) # usage 里有 input_tokens / output_tokens
max_tokens是输出上限,不影响输入计费usage.input_tokens和usage.output_tokens是事后对账的权威数字- 生产环境永远要设
max_tokens,否则一个"话痨"回复可能把单次成本顶上去
Prompt 缓存:省最多的一招
如果你的请求里有大段重复内容(长 system prompt、固定知识库片段、few-shot 示例),用 prompt caching 能让这部分以极低价格命中:
# Anthropic:标记可缓存的块
curl https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{
"model": "claude-sonnet-4-5",
"max_tokens": 256,
"system": [
{"type": "text", "text": "你是一个客服助手。...(超长固定人设和知识)...", "cache_control": {"type": "ephemeral"}}
],
"messages": [{"role": "user", "content": "怎么退款?"}]
}'
经验法则:
- 固定、重复、够长的内容才值得缓存(一般要求达到最小缓存 token 阈值)
- 缓存有写入成本(略高于普通输入),但只要被多次复用就稳赚
- 高频客服、固定 prompt 的批处理,是缓存收益最大的场景
Token 与上下文窗口
上下文窗口(context window) 是模型一次能"看"到的 token 总量上限。关键认知是:
- 窗口 = 输入 token + 输出 token,两者共享同一个额度
- 窗口大不等于"可以随便塞",塞得越满,延迟越高、成本越高、还可能"中间内容被忽略"
- 超出窗口,要么报错,要么被静默截断,取决于 SDK 和配置
超长历史怎么办:按 token 截断
多轮对话最常见的问题就是"聊着聊着历史超了"。正确做法是按 token 预算滚动裁剪,而不是按条数:
import tiktoken
enc = tiktoken.encoding_for_model("gpt-4o")
def token_of(text: str) -> int:
return len(enc.encode(text))
def trim_history(messages: list[dict], budget: int, keep_system: bool = True) -> list[dict]:
"""按 token 预算从最早的非 system 消息开始丢弃,直到总量 <= budget。"""
system = [m for m in messages if m["role"] == "system"] if keep_system else []
rest = [m for m in messages if m["role"] != "system"]
used = sum(token_of(m["content"]) for m in system + rest)
# 从最旧的历史开始丢(rest 里靠前的多轮对话)
while used > budget and rest:
dropped = rest.pop(0)
used -= token_of(dropped["content"])
return system + rest
messages = [
{"role": "system", "content": "你是助手。"},
{"role": "user", "content": "第1轮长文本..."},
{"role": "assistant", "content": "回复1..."},
{"role": "user", "content": "第2轮..."},
]
print(trim_history(messages, budget=200))
这个写法想表达三件事:
- 永远保留 system(人设/规则不能被裁掉)
- 按 token 裁,不按条数裁——中文按条数裁误差极大
- 丢最旧的,保留最近上下文,这是对话场景的标准策略
更成熟的系统会用摘要压缩或**向量检索(RAG)**替代简单截断,但前提是"简单截断已经扛不住"——别一上来就过度设计。
如何在真实 AI 应用里管理 Token
把前面的点组合起来,就是一个真实应用的 token 管理链路。
一张分工对照表
| 环节 | 目标 | 推荐方法 | 不该做什么 |
|---|---|---|---|
| 入参预估 | 调用前估输入 token | tiktoken / count_tokens |
用字符数估后直接报价 |
| 预算控制 | 防止超窗口、超成本 | 按 token 截断历史 | 按消息条数截断 |
| 输出控制 | 防止输出失控 | max_tokens |
不设上限 |
| 成本复用 | 重复 prompt 省钱 | prompt caching | 对一次性短 prompt 也缓存 |
| 事后对账 | 真实成本 | usage.input/output_tokens |
用预估数字当账单 |
| 模型选择 | 同质量降本 | 小模型优先 | 所有任务都上旗舰 |
什么时候精确计数,什么时候估算
- 精确计数:调费率高、面向客户报价、做硬性配额限制、截断临界点。
- 估算:内部粗略限流、产品早期量级判断、非计费路径。
判断标准很简单:这个数字会不会直接变成钱或直接决定用户体验?会,就精确;不会,就估算。
实战架构:一个聊天应用的 Token 预算链路
技术分工
- 入口服务:接收请求,拼装 messages
- 计数模块:用
tiktoken(OpenAI)或count_tokens(Claude)算输入 token - 预算控制器:超预算则截断历史或触发摘要
- 缓存层:固定 prompt 走 prompt caching
- 调用层:带
max_tokens调用模型 - 对账模块:从
usage取真实 token 写入计费表
架构示意
flowchart LR
A[用户输入] --> B[拼装 messages]
B --> C[计数 input_tokens]
C --> D{是否超预算?}
D -- 是 --> E[截断/摘要历史]
E --> B
D -- 否 --> F[固定 prompt 走缓存]
F --> G[调用模型 max_tokens]
G --> H[输出 output_tokens]
H --> I[usage 对账写计费表]
链路说明
- 用户发送一条消息,入口服务把 system、历史、当前输入拼成 messages。
- 计数模块按目标模型算出 input_tokens。
- 预算控制器判断是否超出上下文预算。
- 若超预算,截断或摘要最旧的历史,回到第 2 步重新计数。
- 固定的长 prompt 走 prompt 缓存,命中则按缓存价计费。
- 调用模型,带上
max_tokens限制输出。 - 模型返回,
usage.output_tokens即输出 token。 - 对账模块用
usage的真实数字写入计费表,与预估核对。
这条链路的关键在于:
- 计数在调用前,截断在调用前
- 输出有上限,成本有缓存
- 对账用真实 usage,不信任预估
职责清晰之后,token 相关的线上事故会大幅下降。
同一句请求,Token 是怎么算出来的
以"用户问了一句话,模型回答"这个动作为例,看 token 到底由哪几部分组成。
1. System prompt:固定人设和规则
你是一个简洁的技术助手,回答不超过 3 句话。
它通常是重复、固定的——最适合走缓存。
2. Tools / 函数定义:容易被忽略的开销
[
{"name": "search", "description": "搜索知识库", "input_schema": {"type": "object", "properties": {"q": {"type": "string"}}}}
]
工具定义算进 input_tokens,而且 JSON 结构比自然语言更费 token。工具越多,固定开销越大。
3. 历史对话:随轮次线性增长
user: 什么是 token?
assistant: token 是大模型处理文本的最小单位...
user: 那怎么统计?
多轮对话的 input 会越滚越大,这是要截断/检索的根本原因。
4. 当前用户输入:通常占比最小
中文和英文哪个更费 token?
5. 模型输出:按输出价计费
中文更费 token,因为分词器对汉字的覆盖不如英文密集...
看清这五部分之后,很多"为什么账单这么高"的疑问会自然消失——往往是 system + tools + 历史 占了大头,而不是用户那句输入。
常见误区集中澄清
误区一:1 个 token 永远等于 4 个字符
不是。这是英文的平均值,中文、代码、JSON 都不是这个比例,而且不同模型差异很大。
误区二:不同模型的 token 数可以通用
不可以。GPT、Claude、Llama 各有各的词表,同一段文本 token 数不同,计数必须对着目标模型来。
误区三:用 len(text) 字符数估算就够准
对英文粗略够用,对中文误差极大。凡是涉及计费和截断,都不能用字符数。
误区四:上下文窗口 128K,就是能放 128K 字符
不是。128K 是 token,且是输入 + 输出共享。中文实际能放的篇幅远小于 128K 字符。
误区五:输出 token 和输入 token 一个价
不是。输出通常贵 3~5 倍。让模型少废话,比压缩输入更省钱。
误区六:缓存只省一点点
错。缓存命中 input 可低到一折左右。对固定长 prompt 的高频场景,缓存是收益最大的一招。
误区七:token 优化做得越细越好
大多数时候正好相反。过度优化(逐条精确计数、激进压缩、到处缓存)会增加复杂度和延迟,收益却递减。先把 max_tokens、按 token 截断、固定 prompt 缓存这三件事做对,再谈精细优化。
反过度设计:Token 优化别走火入魔
这一段必须讲。很多团队不是 token 管错了,而是"优化过度"。
一个实用的反过度设计检查表
| 检查项 | 应该问自己的问题 |
|---|---|
| 计数是否必要 | 这个路径真的需要精确计数,还是估算就够? |
| 缓存是否值得 | 这段 prompt 够长、够固定、会被复用吗?达不到阈值就是负收益 |
| 截断是否过早 | 是真的超窗口了,还是只是"担心超"? |
| 模型是否选大 | 这个任务真的需要旗舰模型吗?小模型够不够? |
| 是否引入双写 | 自己维护一套 token 计数,会不会和官方 usage 对不上? |
| 优化收益是否大于复杂度 | 省的钱,够不够覆盖多写的代码和排查成本? |
几条经验法则
- 如果一个需求只需要量级判断,就先用估算规则,别急着全链路接
tiktoken。 - 如果一个 prompt 不够长、不复用,就先别开缓存,写入成本可能吃掉收益。
- 如果只是怕超窗口,就先做朴素的按 token 截断,别一上来上摘要+向量检索。
- 如果是高频调用,就先换小模型 + 开缓存,这比任何"提示词极限压缩"都有效。
token 优化最难的不是"知道有哪些招",而是知道什么时候不要用那些招。
选型速查表
优先用 tiktoken 的场景
- 调 OpenAI 模型
- 需要离线、毫秒级计数
- 做输入预算、历史截断
优先用厂商 count_tokens 接口的场景
- 调 Claude(词表未公开)
- 需要和官方计费一致的精确预估
- 计数频率不高,能接受一次网络请求
优先用 tokenizers / SentencePiece 的场景
- 本地部署开源模型(Llama、Mistral、Qwen 等)
- 需要离线且对应该模型词表
优先用估算规则的场景
- 内部限流、配额量级判断
- 产品早期报价、不直接计费
- 零依赖、零延迟的快速判断
总结
如果把整篇文章压缩成几句话,结论并不复杂:
- Token 是大模型处理文本的最小单位,按子词切分,不是字符也不是词。
- 分词器 每家一套,OpenAI、Claude、Llama 的 token 彼此不通用。
tiktoken是 OpenAI 模型离线精确计数的首选。count_tokens接口 是 Claude 精确计数的唯一权威方式。- 中文比英文更费 token,成本和窗口都要按"更费"估。
- 输出比输入贵,
max_tokens和 prompt caching 是最实用的两把刀。 - 上下文窗口是输入 + 输出共享,超了要按 token 截断,不是按条数。
真正成熟的 token 管理,不是把每个请求都精确到个位数,也不是把提示词压缩到极致。
真正成熟的做法是:
- 计数场景分得清(精确 vs 估算)
- 成本闸门守得住(
max_tokens+ 缓存) - 截断按 token、对账用
usage - 不为边际收益过度设计
做到这些,token 相关的成本和稳定性通常就不会失控。