【学习笔记】Tokenizer 那些事:BPE、SentencePiece 与中文分词的爱恨情仇(4/35)

发布时间:2026/6/28 17:49:58

【学习笔记】Tokenizer 那些事:BPE、SentencePiece 与中文分词的爱恨情仇(4/35) 一、写在前面在第3篇我们算清了参数账。这一篇我们要算另一笔同样重要、却经常被工程师忽视的账Token 账。如果你做过大模型相关的工作下面这些经历应该不陌生同样一段中文文本喂给 GPT-3.5 和 Qwen 算出来的 token 数能差两倍——意味着 API 费用差两倍模型说支持 32K 上下文实际能塞下的中文小说不到 1.2 万字让模型输出 JSON结果它把name切成了、name、三个 token输出莫名其妙微调时新加的特殊 token 一直被分成奇怪的子词怎么训都学不会写代码场景__init__被切成__、init、__看着就难受这些问题的根源全部指向同一个组件——Tokenizer。Tokenizer 是 LLM 系统中最被低估、最容易踩坑、最少被讲透的部分。它处在模型的门口所有文本都要先过它一道。它决定了模型如何看见文本粒度上下文窗口的实际容量中文/英文/代码差异巨大API 计费的真实金额模型对特定语言/领域的友好程度微调时新词汇能否学好读完这一篇你将能回答BPE、WordPiece、SentencePiece、BBPE它们到底有什么区别为什么 GPT-3.5 处理中文那么贵而 GPT-4o 一下子便宜了为什么 Qwen 处理中文比 Llama 快怎么选 tokenizer 友好的模型自部署模型时怎么实测一个 tokenizer 的中文效率微调时加入领域词汇要怎么做才不踩坑我们开始。二、为什么 Tokenizer 不是「小事」2.1 Tokenizer 在 LLM 系统中的位置回忆第 2 篇我们画的 Transformer 流程第一步永远是原始文本 → [Tokenizer] → Token IDs → [Embedding] → 进入 Transformer模型本质上不认识文字它只认识整数 ID。Tokenizer 的任务就是把人类文字翻译成模型可消化的 ID 序列。这个翻译过程有几个工程上极其重要的特性离散且固定模型词表一旦确定所有文本都必须用这个词表里的 token 表示粒度决定一切切得粗如整词词表会爆炸切得细如字符序列会过长不可逆性弱好的 tokenizer 是双射编码后能解码回原文但实际中常出问题影响范围超出文本本身决定了 attention 序列长度、显存占用、API 计费2.2 一个真实的对比实验我们拿一段标准中文约 100 字做对比大模型时代工程师面对的最大挑战不再是写好一段代码而是如何在算力、显存、延迟、成本之间找到平衡点。从 Transformer 架构到推理优化从 KV Cache 到 PagedAttention每一个技术细节都决定着最终系统的性能上限。用主流 tokenizer 实测约略数字不同版本会有微调Tokenizer模型Token 数字符/Tokencl100k_baseGPT-3.5 / GPT-4~1650.65o200k_baseGPT-4o / GPT-5~1100.97Llama-3(BBPE)Llama-3-8B~1500.71Qwen-2/3Qwen 系列~921.16DeepSeek-V3DeepSeek 系列~981.09两个数字差异最大的GPT-3.5 (165) 和 Qwen-3 (92)差近 80%。这意味着处理同样的中文文本Qwen-3 比 GPT-3.5 少用 44% 的 token如果按 API 计费GPT-3.5 处理这段话的成本比 Qwen-3 高 80%同样 32K 上下文窗口Qwen-3 实际能塞下的中文是 GPT-3.5 的 1.8 倍3.3 工程师真正关心的几个问题理解了上面这些我们再来明确本文要解决的工程问题工程问题与 Tokenizer 的关系API 月度账单为什么这么贵Tokenizer 决定输入/输出 token 数32K 上下文为什么不够用中文实际占用比英文多自部署模型选哪个Tokenizer 中文效率是关键输出 JSON 为什么经常坏Tokenizer 对结构化文本的切分领域词汇微调学不会Tokenizer 把术语切碎了多语言混合输入为何乱码Tokenizer 不能正确处理某些字节下面我们一层一层把这些讲清楚。三、Tokenizer 演进史从单词到子词3.1 史前时代Word-level 与 Character-levelWord-level词级别的思路最直观把文本按空格切每个单词一个 token。优点语义清晰致命缺点OOV (Out-Of-Vocabulary) 问题——只要遇到没见过的单词就抓瞎比如chatgpt、vibecoding这些新词而且中文连空格都没有根本切不了。Character-level字符级别反过来——把每个字符当作一个 token。优点词表极小英文 26 字母 标点无 OOV致命缺点序列爆炸——一句话 100 字符就要 100 tokenattention 复杂度 O(n²) 让训练成本飞起两者都不实用。业界很快收敛到了子词Subword路线——切得比词细比字符粗。3.2 BPE所有现代 Tokenizer 的祖先BPEByte Pair Encoding原本是 1994 年的一个数据压缩算法2015 年被 Sennrich 等人引入 NLP成为现代 tokenizer 的奠基。核心思想1. 把每个单词拆成单个字符 2. 统计所有字符对pair的出现频率 3. 把频率最高的 pair 合并成一个新 token 4. 重复步骤 2-3直到达到目标词表大小举例训练语料是low, lower, lowest, newer, new初始字符级l o w /w l o w e r /w l o w e s t /w n e w e r /w n e w /w 第 1 轮发现 (l, o) 出现最频繁合并 → lo l o w → lo w 第 2 轮发现 (lo, w) 频繁合并 → low lo w → low 第 3 轮(e, r) 频繁合并 → er ... 最终词表l, o, w, e, r, s, t, n, lo, low, er, est, new, ...优势词表大小可控通常 30K - 200K高频词作为整体保留如the、function低频词/新词可以由子词拼出如chatgptchatgptGPT-2 / GPT-3 / GPT-3.5 用的就是 BPEOpenAI 开源了实现库tiktoken。3.3 BBPEBPE 的字节级进化普通 BPE 有一个尴尬它工作在字符级别但**「字符」是什么取决于编码**。Unicode 字符成千上万词表会膨胀中文一个字也是一个 字符处理时容易出问题。BBPEByte-level BPE的思路别管 Unicode 字符了直接在 UTF-8字节流上做 BPE。任何文本最终都是字节序列字节只有 256 种可能。这意味着词表可以从 256 个基础字节开始任何文本都能编码无 OOV多语言、表情符号、特殊字符都一视同仁代价单个非 ASCII 字符可能占 2-4 个字节。比如一个汉字在 UTF-8 下占 3 字节初始就要 3 个 token——但通过 BPE 训练后常见汉字组合会被合并成更短的 token。这就是中文 token 效率差异的核心来源训练语料中文占比高 → 常见汉字/词组会被合并 → 中文效率高训练语料英文为主 → 中文很少出现合并机会 → 中文效率低GPT-2 之后大量主流模型都用 BBPEGPT-3.5、GPT-4、Llama 系列、DeepSeek 系列。3.4 WordPieceBERT 的选择WordPiece 和 BPE 非常相似区别在合并标准BPE按频率合并WordPiece按似然度合并合并后让语料整体概率最大化用 WordPiece 的模型BERT、DistilBERT、Electra。由于 BERT 路线在生成式 LLM 时代被 Decoder-only 取代WordPiece 现在主要用于编码器模型生成式大模型很少用它。3.5 SentencePiece多语言友好的整合方案SentencePiece 是 Google 推出的一个 tokenizer框架不是单一算法可以使用 BPE 或 Unigram 两种算法。它解决了 BPE 的一个痛点传统 BPE 需要预先按空格分词但中文/日文没有空格分词就成了问题。SentencePiece 的关键设计直接处理 raw text——不依赖空格预分词把空格本身当作普通字符用特殊符号▁表示支持 Unigram Language Model 算法——给出删除某 token 后语料概率变化的训练目标Llama / Llama-2 / T5 / mT5 / XLM-RoBERTa都用 SentencePiece。Llama-3 切换到了 BBPE基于 tiktoken词表从 32K 扩到 128K中文效率有明显提升但仍然不如 Qwen 等中文优化模型。3.6 主流 LLM 用什么一张总览表模型Tokenizer算法词表大小中文友好度GPT-3.5 / GPT-4tiktokenBBPE100,256★★GPT-4o / GPT-5tiktokenBBPE200,019★★★★Claude 系列自研BBPE 变体~100K★★★Llama-2SentencePieceBPE32,000★Llama-3tiktokenBBPE128,256★★★Qwen-2 / Qwen-3tiktoken 风格BBPE151,643★★★★★DeepSeek-V3自研BBPE128,815★★★★GLM-4SentencePieceBPE151,329★★★★几个观察几乎全员 BBPE 化—— 多语言 字节安全是刚需词表大小普遍 100K—— 早期 32K 已被淘汰中文友好度 ∝ 中文训练语料占比—— Qwen 最强不意外四、中文 Tokenizer 的痛与解4.1 中文为什么这么难中文 tokenization 的难点是三重叠加1. 没有天然分隔符英文 machine learning 有空格分开中文机器学习必须靠模型/词表来识别词语边界。2. UTF-8 编码下汉字占 3 字节在 BBPE 视角下未合并的汉字会占 3 个 token。这就是为什么早期 Llama训练中文数据少处理中文如此低效。3. 中文词汇量极大且组合灵活英文用 26 字母组合词汇总量虽多但形式有限中文常用字 3500但组词能力指数级膨胀开发、开发者、开发工具、开发流程……。4.2 不同模型中文效率的差距有多大我们用一段约 1000 字的标准中文技术文档实测TokenizerToken 数平均每 token 字数相对效率GPT-3.5 (cl100k_base)~16500.61100%基准Llama-2 (32K 词表)~21000.4879%Llama-3 (BBPE 128K)~15000.67110%GPT-4o (o200k_base)~11000.91150%Qwen-3~9101.10181%DeepSeek-V3~9801.02168%这张表意味着什么用 Qwen-3 处理中文比用 GPT-3.5 节省 45% 的 token用 GPT-4o 替换 GPT-3.5光是 tokenizer 升级就让中文成本降 30%Llama-2 时代的中文不友好是真实存在的Llama-3 已大幅改善4.3 中文优化的三条工程路径路径 1从头训练中文友好的 tokenizer代表Qwen、DeepSeek、GLM。训练时让中文语料占比足够高通常 30%BPE 自然会把高频汉字和词组合并成单个 token。例如 机器学习 在 Qwen 词表中是1 个 token而在 Llama-2 词表中可能是5-6 个 token。路径 2扩词表Vocabulary Extension代表Chinese-LLaMA、Chinese-Alpaca 等社区改造版。在原模型基础上训练一个中文专用 tokenizer通常用 SentencePiece合并到原词表中去重后通常加 20K-50K 中文 token扩展 embedding 层新 token 的 embedding 用旧 token 平均初始化继续预训练 SFT代价embedding 层参数增加 必须额外训练。路径 3jieba 等外部分词器已淘汰早期方案先用 jieba/THULAC 把中文分词再喂给 tokenizer。这种方案在 LLM 时代已经被淘汰——会破坏字节安全性且不利于模型自适应。4.4 一个真实的工程教训Llama-2 中文部署的坑2023 年很多团队拿 Llama-2 部署中文服务结果发现同样 4K 上下文中文输入只能塞 1500 字API 替换 Llama-2 后输出延迟反而变长同样 prompt 在 GPT-3.5 上跑得快得多根本原因Llama-2 词表只有 32K中文几乎都被切成单字节 BBPE输入 token 数膨胀 2-3 倍。当时的解法等 Chinese-LLaMA 或者直接换 Qwen。今天的启示为中文场景选模型必须看 tokenizer——这是除了模型能力外最重要的硬指标之一。五、实战实测一个 Tokenizer讲再多不如代码跑一遍。下面是一个可直接运行的实测脚本。 Tokenizer 实测脚本对比不同 tokenizer 的中英文效率 依赖pip install tiktoken transformers import tiktoken from transformers import AutoTokenizer # 测试文本 text_zh 大模型时代工程师面对的最大挑战不再是写好一段代码 而是如何在算力、显存、延迟、成本之间找到平衡点。 从 Transformer 架构到推理优化从 KV Cache 到 PagedAttention 每一个技术细节都决定着最终系统的性能上限。 text_en In the LLM era, engineers biggest challenge is no longer writing good code, but finding the balance between compute, memory, latency and cost. From Transformer architecture to inference optimization, from KV Cache to PagedAttention, every technical detail determines the upper bound of the final systems performance. text_code def attention(q, k, v, maskNone): scores q k.transpose(-2, -1) / math.sqrt(k.size(-1)) if mask is not None: scores scores.masked_fill(mask 0, float(-inf)) return F.softmax(scores, dim-1) v defbenchmark(name, encode_fn, texts): print(f\n {name} ) for label, txt in texts.items(): n_tokens len(encode_fn(txt)) n_chars len(txt) print(f {label:8s} | chars: {n_chars:4d} | tokens: {n_tokens:4d} f| ratio: {n_chars/n_tokens:.2f} char/token) texts {zh: text_zh, en: text_en, code: text_code} # 1. GPT-3.5 / GPT-4 (cl100k_base) enc tiktoken.get_encoding(cl100k_base) benchmark(GPT-3.5/4 (cl100k), enc.encode, texts) # 2. GPT-4o (o200k_base) enc_o tiktoken.get_encoding(o200k_base) benchmark(GPT-4o (o200k), enc_o.encode, texts) # 3. Llama-3 llama_tok AutoTokenizer.from_pretrained( meta-llama/Meta-Llama-3-8B, trust_remote_codeTrue) benchmark(Llama-3, llama_tok.encode, texts) # 4. Qwen-3 qwen_tok AutoTokenizer.from_pretrained( Qwen/Qwen3-8B, trust_remote_codeTrue) benchmark(Qwen-3, qwen_tok.encode, texts)典型输出 GPT-3.5/4 (cl100k) zh | chars: 124 | tokens: 165 | ratio: 0.75 char/token en | chars: 321 | tokens: 62 | ratio: 5.18 char/token code | chars: 192 | tokens: 60 | ratio: 3.20 char/token GPT-4o (o200k) zh | chars: 124 | tokens: 108 | ratio: 1.15 char/token en | chars: 321 | tokens: 58 | ratio: 5.53 char/token code | chars: 192 | tokens: 54 | ratio: 3.56 char/token Llama-3 zh | chars: 124 | tokens: 151 | ratio: 0.82 char/token en | chars: 321 | tokens: 65 | ratio: 4.94 char/token code | chars: 192 | tokens: 65 | ratio: 2.95 char/token Qwen-3 zh | chars: 124 | tokens: 92 | ratio: 1.35 char/token en | chars: 321 | tokens: 71 | ratio: 4.52 char/token code | chars: 192 | tokens: 62 | ratio: 3.10 char/token几个观察中文场景 Qwen-3 完胜1.35 字/token是 cl100k_base 的 1.8 倍英文场景差异较小毕竟训练语料英文为主代码场景接近——所有主流 tokenizer 对常见编程语言都做了优化GPT-4o vs GPT-3.5仅 tokenizer 升级就让中文成本降低 35%实操查看一段中文具体被切成了什么text 我们一起学习大模型推理优化 print(--- GPT-3.5 ---) ids tiktoken.get_encoding(cl100k_base).encode(text) for tid in ids: print(f {tid}: {tiktoken.get_encoding(cl100k_base).decode([tid])!r}) print(\n--- Qwen-3 ---) qwen_tok AutoTokenizer.from_pretrained(Qwen/Qwen3-8B, trust_remote_codeTrue) ids qwen_tok.encode(text, add_special_tokensFalse) for tid in ids: print(f {tid}: {qwen_tok.decode([tid])!r})输出约略--- GPT-3.5 --- 15469: 我 101249: 们一 35987: 起 41642: 学 43240: 习 ...每个汉字大致 1-2 个 token共约 15 个 --- Qwen-3 --- 104198: 我们 104049: 一起 100132: 学习 101049: 大模型 104597: 推理 101924: 优化 6 个 token 搞定可以清楚看到Qwen 把 我们、一起、大模型、推理、优化 都做成了单个 token而 GPT-3.5 把每个汉字都切开。六、Tokenizer 影响的工程链条6.1 上下文窗口的实际容量模型标称的上下文长度都是 token 数不是字符数。把这两者换算清楚模型标称上下文中文实际字数英文实际单词数GPT-3.5 (16K)16,384 tokens~12,000 字~12,000 词GPT-4o (128K)128,000 tokens~140,000 字~96,000 词Claude Opus 4.7 (1M)1,000,000 tokens~1,100,000 字~750,000 词Llama-3 (8K)8,192 tokens~6,000 字~6,000 词Qwen-3 (128K)128,000 tokens~170,000 字~96,000 词关键认知128K 上下文对中文用户的实际容量Qwen 比 GPT-4o 多 20%、比 Llama-3 多 1.5 倍32K 够用的判断要看场景处理一本《三体》第一部27 万字32K 上下文Qwen 实际 ~42K 字放不下128K 才行 长上下文工程详见系列第 5 篇上下文窗口的秘密。6.2 API 计费的真实成本主流 API 都按 token 计费所以token 效率直接转化为成本。实测一个简单业务场景处理 1 万篇中文新闻摘要每篇约 500 字。模型单次 token 数1 万次总 token输入费用GPT-3.5~8258.25 M0.5/M)GPT-4o~5505.5 M2.5/M)Claude Sonnet~5805.8 M3/M)Qwen-3 API~4604.6 M0.4/M)DeepSeek-V3 API~4904.9 M0.27/M)结论在大规模中文场景下Qwen / DeepSeek 的成本优势 单价低 tokenizer 效率高 的双重叠加。6.3 词表大小的代价embedding 层词表大小直接决定embedding 层和 LM head 的参数量Embedding 参数 V × d LM Head 参数 V × d (通常和 embedding 共享或独立)以 Llama-3-8B 为例V 128,256, d 4096 Embedding LM Head ≈ 128,256 × 4096 × 2 ≈ 1.05 B占总参数的 13%——不是可以忽略的小数字。这就是为什么早期 Llama-2 词表只有 32Kembedding 占总参数仅 5%但中文效率受损到 Llama-3 扩到 128K多语言效果改善但参数膨胀。Qwen-3 词表 151K——比 Llama-3 还大 18%embedding 占用更多但换来了中文效率王者。这就是 trade-off。6.4 Tokenizer 与微调6.4.1 微调时词表能改吗短答案能但代价高。新增 tokenembedding 层要扩新 token 的 embedding 要初始化用旧 token 平均或者从相关词汇借鉴改变现有 token会破坏模型已学到的语义通常不建议6.4.2 加入特殊 token 的坑很多场景需要加自定义 token比如tokenizer.add_tokens([my_company, api_key_placeholder]) model.resize_token_embeddings(len(tokenizer))注意事项新 token 的 embedding 是随机初始化的需要训练才能学会含义如果数据中新 token 出现频率太低 几千次模型基本学不会推理时如果 tokenizer 没保存正确新 token 会被切回旧词表 → bug6.4.3 领域词汇切碎问题医学、法律、金融场景常见现象心肌梗死 → [心, 肌, 梗, 死] 区块链分布式账本 → [区, 块, 链, 分, 布, 式, 账, 本]如果业务对这些术语精度要求高建议在预训练/微调数据中大量出现这些术语 → BPE 训练时会自动合并或者手动 add_tokens 大量训练数据 详见系列第 9 篇垂直领域大模型微调。6.5 多模态 Tokenizer新趋势2024 年后大模型全面多模态化图像、音频、视频也都需要tokenize。模态主流 Tokenizer思路图像ViT Patch把图像切成 16×16 patch每个 patch 一个 token音频EnCodec / SoundStream用 VQ-VAE 把音频编码成离散 token视频TimeSformer / VideoMAE时空 patch 切分文档LayoutLM文本 位置坐标融合一个 OCR 处理过的 PDF 文档文本 token 图像 token 位置 token 可能合计 5-10 倍于纯文本。这是多模态模型上下文消耗大的根本原因。 详见系列第 29 篇多模态部署。七、扩展话题与避坑清单7.1 几个被忽视的细节1. 不同 dtype 的 tokenizer 行为可能不同有些 tokenizer 在长文本下会产生不同的切分取决于 normalization 策略。生产环境要做端到端测试。2. BOS / EOS / Pad token 不要乱加模型训练时怎么用的推理时就要怎么用否则效果会跌。3. Chat template 是另一层 tokenization现代 LLM 都有自己的对话模板system / user / assistant 标记。手动拼字符串容易出错用tokenizer.apply_chat_template()是稳妥的做法。7.2 选型清单部署模型前用这个清单测一遍 tokenizer测中文效率用业务真实文本不要用通用语料测代码效率如果有代码场景测 JSON / 结构化输出看是否切碎测领域术语看是否合并合理看 chat template 是否清晰确认特殊 tokenBOS / EOS / Pad用法7.3 决策建议如果你正在选模型场景推荐 Tokenizer 类型纯中文 ToC 应用Qwen / DeepSeek 系列多语言 / 全球服务GPT-4o / Claude / Gemini代码场景DeepSeek-Coder / Claude Sonnet端侧 / 极限优化小词表 量化模型Phi、Gemma自有垂直领域开源底座 扩词表 微调八、结语Tokenizer 不是细节是基础设施读完本文你应该理解Tokenizer 不只是切分文本——它决定了模型的成本结构、上下文容量、语言友好度中文场景下选对 tokenizer 直接省 30-50% 成本量化、推理优化做得再好tokenizer 不行也是白费参考文献Tokenizer 那些事BPE、SentencePiece 与中文分词的爱恨情仇

相关新闻