理解切分的细节,省钱不是玄学

为什么要深入 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 的思想很朴素:

  1. 把文本拆到字节级(每个字节一个单位)
  2. 统计相邻字节对的出现频率,把最频繁的合并成一个新单位
  3. 重复,直到达到目标词汇表大小(典型是 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 tiktoken
import 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%
  • DeepSeekQwen 这类国产模型自然在中文 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 价格便宜的一部分原因——同样钱能买更多有效信息。

相关阅读

版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。

(采用 CC BY-NC-SA 4.0 许可协议进行授权)

本文标题:番外 3:Token 的秘密,BPE、中文为什么更贵

本文链接:https://www.sshipanoo.com/blog/ai/ai-for-python/番外03-Token秘密/

本文最后一次更新为 天前,文章中的某些内容可能已过时!