Token 分词(中篇)| Tokenizer 的工作原理
上篇讲了 token 是什么、BPE 怎么工作。这篇讲Tokenizer 内部几个阶段分别在做什么、算法怎么选、Chat Template 如何吃掉你的 token、以及那个让所有后端开发者困惑的问题为什么我算的 token 数和 API 返回的总对不上Tokenizer 不只是切词很多人以为 Tokenizer 就是把文本切成 token其实它内部有一条完整的流水线每个阶段都有自己的逻辑也都可能成为你 debug 时的死角。Normalization文本进门前先洗一遍Normalization 做的事情是统一文本格式消除一些底层差异。比如这四行文字人类看起来差不多但底层 Unicode 表示完全不同Normalization 可能做的事情包括Unicode 标准化NFC/NFD/NFKC/NFKD、全角转半角、大小写处理、空白字符合并、控制字符清理。这里有个关键点Normalization 是有损操作。如果把所有字母转小写Apple就变成apple大小写信息永久丢失。在处理代码、法律文档、合同这类场景时过度规范化会破坏原始信息。现代生成式模型的 Tokenizer 越来越倾向于轻 Normalization尤其 Byte-level BPE 的方案基本保留原始文本可逆性更好。这是选型时值得关注的细节。Pre-tokenization在子词切分之前先切一刀粗的Subword 算法并不直接在原始文本上跑而是先经过预分词把文本切成更粗粒度的片段再对每个片段做子词切分。GPT 系列的预分词规则大概是这样的注意 world带了前导空格逗号和感叹号被单独切出来。这不是随意设计把空格归属到后面的词而不是单独作为一个 token可以减少总 token 数同时让模型能区分单词在句首和单词在句中。这个行为是 GPT/tiktoken 系列的特有设计不是所有 tokenizer 的通用规则。BERT 系列不这么处理Qwen 也不一样。你在 debug 时不要想当然地把一个模型的分词行为套到另一个上。中文的预分词更复杂因为没有天然空格边界不同 tokenizer 处理中英文边界、标点、数字的方式差异很大。主流子词算法横向对比上篇讲了 BPE这里把几个主流算法放在一起比较算法核心思路代表模型特点BPE高频相邻对反复合并GPT-2/3/4, LLaMA, Qwen训练快推理确定性强WordPiece选择能提升语料概率的子词合并BERT, DistilBERT用##标记词内子词对语义更敏感Unigram LM从大词表开始删减概率最低的 tokenT5, mBART支持概率采样同一文本可能有多种切分SentencePieceBPE 或 Unigram 的封装框架直接在原始字节序列上操作T5, LLaMA底层, 多语言模型不依赖空格预分词用▁表示空格对中文友好Byte-level BPE从字节层面起步的 BPEGPT-4, Claude, Qwen零 OOV鲁棒性强几点容易混的地方WordPiece 的##表示这个 token 只能出现在词内部不是词的开头。[play, ##ing]解码还原成playing。这是 WordPiece 的实现约定其他算法不一定有。SentencePiece 不是单一算法而是个框架——它里面跑的可能是 BPE也可能是 Unigram。它最大的贡献是解决了强依赖空格预分词的问题让多语言场景的 tokenizer 训练更统一。Unigram LM 的概率采样——同一段文本可以有多种切分方式训练时有时会随机采样不同切分增加模型的鲁棒性。但推理时通常取概率最高的那种。作为后端开发者你不需要自己实现这些算法。你用的模型都自带训练好的分词器正确加载就行。选型时更重要的是看目标语言的压缩率、核心业务词的切分质量、encode/decode 是否可逆。Token 解码还原文本不是简单拼接模型输出的是 token ID 序列Tokenizer 的 Decoder 负责把它还原成文本。不同算法的解码规则不一样encode/decode 不一定完全可逆。如果 Normalization 阶段做了大小写转换或 Unicode 归一化解码结果和原始文本可能不完全一致。在代码、日志、法律文档等对格式敏感的场景这一点值得注意。Chat 模型里的特殊 Token聊天模型在普通文本 token 之外还有一套特殊 token 用来表示结构和控制信息。常见的有特殊 Token作用BOS序列开始EOS序列结束模型停止生成的信号PAD批处理时补齐长度|im_start|/|im_end|角色消息的开始/结束Qwen 的 ChatML 格式[INST]/[/INST]LLaMA 2 的指令格式|system|/|user|部分模型用这种格式区分角色这些特殊 token 告诉模型哪里是系统指令、哪里是用户输入、哪里是助手回答、哪里该停止生成。没有它们聊天模型根本不知道自己在处理一段多轮对话。特殊 token 也消耗 context。一个完整的 Qwen 对话格式大概长这样每轮对话的模板开销大概 7–10 个 token多轮下来积累不少计算 token 预算时别漏掉这部分。Chat Template你算的 token 数为什么总对不上这是开发者最常遇到的困惑之一。你用 tokenizer 自己算了 1200 tokens但 API 返回的usage.input_tokens是 1350差了 150哪来的原因是Chat Template。你往 API 里传的是这个[ {role: system, content: 你是电商客服助手}, {role: user, content: 我的订单什么时候发货} ]但模型内部看到的不是这个 JSON而是经过 Chat Template 处理之后的东西大概长这样|im_start|system 你是电商客服助手 |im_end| |im_start|user 我的订单什么时候发货 |im_end| |im_start|assistant角色标记、换行符、分隔符全都是 token都计入 context。如果你只是tokenizer.encode(你是电商客服助手 我的订单什么时候发货)来计算当然差。正确做法是用apply_chat_templatefrom transformers import AutoTokenizer tokenizer AutoTokenizer.from_pretrained(Qwen/Qwen2.5-7B-Instruct) messages [ {role: system, content: 你是电商客服助手}, {role: user, content: 我的订单什么时候发货} ] # 这才是模型实际看到的文本 prompt tokenizer.apply_chat_template( messages, tokenizeFalse, add_generation_promptTrue ) ids tokenizer.encode(prompt) print(f实际 token 数: {len(ids)}) print(f实际 prompt:\n{prompt})你会看到同样的内容走 chat template 之后 token 数明显多于直接 encode。Tool Calling 的隐性成本Agent 系统里少不了工具调用。每次请求你定义的工具 schema 都会进入 context{ name: getOrder, description: 根据订单号查询订单详情, parameters: { type: object, properties: { orderId: {type: string, description: 订单号} }, required: [orderId] } }这个 schema 大概 60–80 tokens。如果你的 Agent 一次性暴露了 30 个工具仅工具定义就吃掉 2000 tokens而且每次请求都要付这个成本。更糟的是工具越多模型选错工具的概率越高生成质量下降。正确做法是按业务场景动态选择工具客服场景只传getOrder、getLogistics、createTicket不要把所有工具一起堆进去。为什么 Tokenizer 和模型参数是绑定的这个问题很多人搞不清楚后果可能很严重。模型训练时Embedding 矩阵是围绕这个 Tokenizer 的词表训练出来的vocab_size 151643Qwen2.5 embedding_matrix 151643 × hidden_size # 矩阵第 i 行 token_id i 对应的向量如果训练时hello对应 ID 15339推理时换了个 Tokenizerhello变成了 ID 8912那模型查到的 Embedding 向量就完全错了相当于数据库主键对不上什么输出都是乱的。这就是为什么不同模型必须用对应的 Tokenizer绝对不能混用更换 Tokenizer 意味着要重新训练或至少做大量微调新增特殊 token 时不只是往词表加条目还要扩展 Embedding 矩阵并通过训练让模型学会它评估 Tokenizer 是否适合你的业务如果你在做模型选型或者评估某个开源模型是否适合自己的业务场景Tokenizer 是一个值得专门测试的维度。看压缩率。把你的真实业务数据跑一遍算token 数 / 字符数的比值。重点看中文文本、代码标识符、业务术语、JSON 字段、日志。比值越低token 越少越省钱。看核心词的切分质量。把你业务里最高频的词汇丢进去看看怎么切words [订单超时取消, RedisClusterConfig, payment_timeout, 用户实名认证] for word in words: tokens tokenizer.tokenize(word) print(f{word:25s} → {tokens})如果核心术语被切得很碎说明这个 tokenizer 的训练语料里这类文本很少模型对这些概念的理解也可能偏弱。看可逆性。tokenizer.decode(tokenizer.encode(text)) text是否成立在代码、JSON、日志等格式敏感的场景这一点很重要。小结Tokenizer 是完整流水线Normalization → Pre-tokenization → 子词切分 → ID 映射 → 解码Normalization 可能有损代码和格式敏感文档要注意空格处理方式因 tokenizer 而异不要把 GPT 的行为当作通用规则特殊 token 和 Chat Template 都消耗 context计算预算时不能漏Tool schema 每次请求都付成本按场景动态选工具Tokenizer 和模型参数绑定不能混用不能随便换下一篇讲企业级项目里的 Token 工程实践——Token Budget Manager 怎么设计、RAG 分块策略、多租户限流、成本治理和安全风控。