理解切分的细节,省钱不是玄学
为什么要深入 Token
主线第 01 篇说过 Token 的粗略概念——英文一个 token 约等于 0.75 词、中文 1~2 字。这个估算用来"心里有数"够用,但真要做成本控制、上下文管理、缓存命中率优化,必须懂 Token 切分的细节:为什么"hello" 是 1 个 token 而 "你好" 是 2 个?为什么换行符也要算 token?为什么相同信息英文比中文便宜 3 倍?
这一篇用 Python 实操把这些都看清楚。
Token 是什么
LLM 不是按字符或词处理文本,而是按"token"——一个被 tokenizer 切分出来的最小文本单位。这个切分由 BPE(Byte-Pair Encoding) 算法完成,BPE 的思想很朴素:
- 把文本拆到字节级(每个字节一个单位)
- 统计相邻字节对的出现频率,把最频繁的合并成一个新单位
- 重复,直到达到目标词汇表大小(典型是 5 万~20 万)
结果就是一个"高频字符序列被合并成大块、低频被拆细"的词汇表。常见词如 "hello"、"the" 是单个 token;罕见词如 "tokenization" 可能是 token + ization 两个 token;任意 unicode 字符的 fallback 总能拆到字节级处理。
tiktoken:精确数 token
OpenAI 的 tiktoken 是最常用的 Python tokenizer,准确度匹配 OpenAI 系列模型。其它兼容 OpenAI 协议的服务(DeepSeek、Qwen)的实际 tokenizer 略有差异,但用 tiktoken 估算误差通常在 5% 以内,足够做成本预估。
pip install tiktokenimport tiktoken
# 不同模型用不同 encoding。GPT-4o 用 o200k_base,GPT-3.5/4 用 cl100k_base
enc = tiktoken.encoding_for_model("gpt-4o")
text = "Hello world"
tokens = enc.encode(text)
print(tokens) # [13225, 2375]
print(len(tokens)) # 2
print(enc.decode(tokens)) # "Hello world"
# 看每个 token 对应的文本
for tid in tokens:
print(f"{tid}: {enc.decode([tid])!r}")
输出会让你直观看到切分边界。试几段文本你能立刻发现规律:
samples = [
"hello", # ['hello'] → 1 token
"Hello", # ['Hello'] → 1 token (大小写敏感)
" hello", # [' hello'] → 1 token (前导空格被吸收)
"hellothere", # 可能拆成 ['hello','there']
"你好", # ['你', '好'] → 2 token (中文常见字也被拆)
"你好世界", # ['你好', '世界'] → 2 token
"鬱", # 罕见字会拆到多 byte → 可能 3 token
"🤖", # emoji 通常 2~3 token
" indented", # 4 空格可能算 1~2 个 token
"https://example.com/a/b", # URL 切得很碎
]
for s in samples:
n = len(enc.encode(s))
print(f"{n:>3} {s!r}")
跑一下你会得到非常具体的认知:英文常用词大多是 1 个 token,标点空格也常常被吸收进相邻 token,URL 拆得碎,中文常用字一字一 token,罕见字和 emoji 都偏贵。
为什么中文更贵
主流 LLM 的 tokenizer 训练数据里英文占比远高于中文,BPE 自然把英文优化得更好——常见英文短语合成大 token,罕见的非英语 unicode 留在小 token。直接结果:表达同样意思,中文消耗的 token 数明显多于英文。
en = "The quick brown fox jumps over the lazy dog."
zh = "敏捷的棕色狐狸跳过了那只懒狗。"
print(f"英文:{len(enc.encode(en)):>3} tokens / {len(en):>3} chars")
print(f"中文:{len(enc.encode(zh)):>3} tokens / {len(zh):>3} chars")
典型输出:
英文: 10 tokens / 44 chars
中文: 24 tokens / 16 chars
英文 4 字符约 1 token,中文 0.7 字符就 1 token。同一篇文章的中文版本比英文版本贵 2~3 倍。
不同模型对中文的优化程度不同:
- OpenAI GPT-4o(o200k_base)已经显著优化中文,比之前 GPT-3.5/4 的 cl100k_base 中文 token 减少约 40%
- DeepSeek、Qwen 这类国产模型自然在中文 tokenizer 上做了重点优化,同样文本的 token 数比 OpenAI 还少
- 这也是为什么"做中文应用首选国产模型"既有价格便宜也有 token 效率的双重原因
完整对话的 token 怎么算
实际请求里发出去的不只是 messages 的 content。OpenAI 协议要求每条消息都有协议级开销(role 标签、分隔符等),算法大致是:
def count_message_tokens(messages: list[dict], model: str = "gpt-4o") -> int:
enc = tiktoken.encoding_for_model(model)
# 每条 message 有约 3~4 个协议 token 开销
overhead_per_message = 4
# assistant 还有起始标记
final_overhead = 3
total = final_overhead
for m in messages:
total += overhead_per_message
total += len(enc.encode(m["role"]))
total += len(enc.encode(m["content"]))
return total
实际响应里的 usage.prompt_tokens 是 OpenAI 服务端的精确计数,建议用响应的真实数字而不是自己算。但发送前的成本预估很有用——尤其在长上下文场景,先估一下会不会超出预算或上下文窗口再决定要不要发。
tools 和 function 的 token 成本
这个最容易被忽略:你定义的 tools 数组也会被序列化进 Prompt,每次请求都会被算 token。一个有 10 个工具的 Agent,光 tools 定义可能就占 2000+ token。
预估:
import json
tools_json = json.dumps(tools, ensure_ascii=False)
print(f"tools 占用:约 {len(enc.encode(tools_json))} tokens")
实际占用通常比这个略高(OpenAI 内部会加一些格式化)。优化思路:
- 工具定义里 description 简洁但准确,不要堆废话
- 不必要的工具不传——用工具路由按场景动态暴露
- 复杂参数 schema 用嵌套对象而不是平铺扁平字段
省 token 的几个实操技巧
1. 用模型擅长的语言
如果业务允许,把 system prompt 用英文写、用户输入翻译成英文再处理,处理结果再翻回中文。听起来折腾,但对长 system prompt 的场景能省 50%+ 的 token。当然这会增加一些处理逻辑复杂度,权衡。
2. Prompt 短化
去掉 Prompt 里的"请、麻烦、谢谢"这些礼貌词(模型完全不在意)。把"你应该首先做 X,然后做 Y"压成"先 X 后 Y"。中文 Prompt 这种压缩能省 20~30%。
3. 历史摘要
多轮对话超过一定长度时,让模型给"前 N 轮对话"做一个摘要,用摘要替换原文。配合 Langfuse 这类工具自动监控对话长度触发。
4. 检索精简
RAG 场景里,检索回来的文档片段比真正相关的部分多得多。检索到 5 块各 500 字,相关的可能只有 100 字。可以让小模型先做一次"提取相关句子"的预处理。
5. Prompt Caching
OpenAI / Anthropic / DeepSeek 都已经支持 Prompt Caching——相同前缀的 Prompt 享受 10~50% 的折扣。把 system prompt + 不变的 few-shot 示例放在 messages 列表最前,缓存命中率高的多。
6. 批量 Embedding
一次请求传一批文本进行 embedding,每条文本本身的 token 计费不变,但 HTTP 往返开销摊薄。处理大量文档时一定要批量。
不同模型的 tokenizer 对比
import tiktoken
text = "今天的天气很好,我想出去散步。Hello, how are you doing today?"
for model_name, enc_name in [
("gpt-3.5/gpt-4", "cl100k_base"),
("gpt-4o", "o200k_base"),
]:
enc = tiktoken.get_encoding(enc_name)
print(f"{model_name:>20}: {len(enc.encode(text))} tokens")
DeepSeek、Qwen 用的是各自训练的 tokenizer,可以通过 HuggingFace 的 transformers 库获取:
from transformers import AutoTokenizer
tok = AutoTokenizer.from_pretrained("deepseek-ai/DeepSeek-V3")
print(len(tok.encode(text)))
实际跑下来,国产模型对纯中文文本通常比 OpenAI 节省 30~50% token,这也是为什么国产模型 API 价格便宜的一部分原因——同样钱能买更多有效信息。
相关阅读
- tiktoken GitHub
- OpenAI Tokenizer 在线演示
- Hugging Face Tokenizers 文档
- Byte Pair Encoding 维基百科
- Tokenization 详解 (Hugging Face)
版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。
(采用 CC BY-NC-SA 4.0 许可协议进行授权)
本文标题:番外 3:Token 的秘密,BPE、中文为什么更贵
本文链接:https://www.sshipanoo.com/blog/ai/ai-for-python/番外03-Token秘密/
本文最后一次更新为 天前,文章中的某些内容可能已过时!