面试知识库
高 基础

Tokenizer与分词原理#

一句话答案#

Tokenizer 把文本切成模型词表里的”子词”(subword)单元并映射成整数 id,是 LLM 唯一”看到”的输入;主流算法 BPE / WordPiece / Unigram 都在词表大小(嵌入矩阵 + softmax 开销)与序列长度(上下文占用、推理成本)之间做权衡。模型按 token 计费、按 token 限长,而且很多”怪毛病”(算术、数字母、中文更贵)根子都在分词而不在模型。

核心要点

1. 为什么要分词、为什么是子词#

模型内部只处理整数序列:token id 经 Embedding 层(一个 vocab_size × d_model 的查表矩阵)变成向量再进 Transformer(见 Transformer架构、Embedding原理与选型)。粒度有三档:

  • 字符/字节级:词表极小、无 OOV,但序列太长(注意力 O(n²),成本剧增)
  • 词级:序列短,但词表爆炸、拼错/新词/多语言全变 [UNK]
  • 子词级(主流):高频词保留整词,低频词拆成词根词缀——“unbelievable” → un + believ + able,兼顾词表规模与序列长度,且基本无 OOV

2. 三种主流算法#

算法训练方式切分方式代表
BPE (Byte Pair Encoding)从字符词表起步,反复合并语料中最高频的相邻 pair,直到词表达到目标大小,记录合并规则(merges)按合并规则的学习顺序贪心合并GPT 系、Llama、Qwen
Byte-level BPE基础单元是 256 个字节而非 Unicode 字符,任何文本(含 emoji、生僻字)都能表示同 BPEGPT-2 及之后、Llama 3、Qwen
WordPiece也是合并,但选 pair 的依据是似然提升 freq(ab) / (freq(a)·freq(b)),优先合并”组合后互信息高”的 pair贪心最长匹配,词中续接片段带 ## 前缀BERT 系
Unigram反向思路:从一个很大的候选词表出发,用 EM 估计每个 token 的概率,逐步剪掉对语料似然损失最小的 tokenViterbi 找概率最大的切分(一句话可有多种切法,可做采样做数据增强)SentencePiece(T5、ALBERT、XLNet)

SentencePiece 是工具而非算法(可跑 BPE 或 Unigram):它把输入当原始字符流,空格编码为 ▁,不依赖语言特定的预分词,对中日文这种无空格语言友好。

词表大小权衡:词表越大 → 同样文本 token 越少(上下文更省、推理更快),但 Embedding 矩阵和输出层 softmax 更大、稀有 token 训练不充分。量级参考:GPT-2 约 5 万、Llama 2 约 3.2 万、GPT-4 约 10 万、Llama 3 / Qwen2.5–Qwen3 / GPT-4o 在 13 万–20 万级,Qwen3.5 扩到约 25 万(248,320,含填充),近年趋势是词表变大、多语言覆盖更均匀。同一厂商换代也可能换 tokenizer(如 Anthropic 在 Claude Opus 4.7 起换用新 tokenizer),同样文本的 token 数和费用会变,迁移时要重测。

3. 特殊 token 与 chat template#

  • BOS/EOS(<s> </s>、<|endoftext|>、<|eot_id|>):标记序列开始/结束;EOS 是模型”学会停下来”的信号,也是解码器判停的依据
  • pad:批处理对齐用,不参与 attention(靠 attention mask 屏蔽)
  • 角色标记:对话模型用特殊 token 包裹角色段,如 ChatML 的 <|im_start|>system ... <|im_end|>,Llama 3 的 <|start_header_id|>user<|end_header_id|>。这些 token 在词表里是原子单元,正确实现下用户文本里的同名字符串会被当普通文本切分或被过滤(HF tokenizer 需显式 split_special_tokens,tiktoken 默认直接报错),这也是区分”指令”和”内容”的底层机制之一
  • chat template:每个模型的角色拼接格式不同,用错模板(或裸拼字符串)会显著掉点;HF 用 tokenizer.apply_chat_template() 按模型自带的 Jinja 模板拼装,API 调用时服务端替你做了这一步

4. token 与字符/汉字的换算量级#

  • 英文:大致 1 token ≈ 4 个字符 ≈ 0.75 个单词,常见词一个 token,生僻词拆 2–3 个
  • 中文:与词表强相关。早期以英文为主的词表下,一个汉字常要 1.5–2 个 token(被拆成多个字节片段);GPT-4 词表下约 1–1.5 token/字;Qwen、DeepSeek 等对中文优化的词表,常见词可整词成 token,平均不到 1 token/字
  • 同一段话中英文 token 数能差一倍以上 → 中文用户在英文主导的模型上更贵、有效上下文更短、生成更慢(每个 token 一步解码)
  • 精确数字不要靠估,用模型对应的 tokenizer 数(OpenAI tiktoken、HF AutoTokenizer),不同模型不通用

5. token 与计费、上下文预算#

  • 计费按 输入 token + 输出 token 分别计价,输出通常贵数倍;max_tokens 只限制输出长度,撞上会以 finish_reason=length 截断(见 [LLM API调用与流式输出](/topics/llm-serving/LLM API调用与流式输出))
  • 上下文窗口 = 输入(system + 历史 + RAG 片段 + 工具定义)+ 预留输出,必须留够输出预算,否则输入塞满后模型一个字都吐不出来
  • 工程上:多轮对话要做历史裁剪/摘要,RAG 要控制召回片段总 token,工具 Schema 也占 token;缓存命中(prompt cache)按前缀 token 算,前缀稳定才省钱

6. 分词导致的典型问题#

  • 算术与数字:数字被按 token 切分(不同词表切法不一,有的按 1–3 位数字切块,有的逐位切),模型看到的是”片段”而不是位值,长数乘法、对齐进位天然吃亏;新一代 tokenizer 倾向于数字逐位或定长切分来缓解
  • 拼写/字母级任务:“strawberry 有几个 r”答错,是因为模型看到的是 str aw berry 这类 token id,看不到字母;倒序拼写、数字母、押韵同理。让模型先把单词逐字母拆开再数,通常就对了
  • 多语言不均衡:低资源语言切分更碎 → 更贵、更慢、效果更差;中文词表优化是国产模型的一个实际优势
  • 空格与大小写: the(带前导空格)和 the 是不同 token;prompt 末尾多一个空格会改变下一个 token 的分布
  • token 边界与约束解码:语法/正则是按字符定义的,token 不一定落在字符边界上(一个 token 可能跨越 " 和内容),Grammar 要在 token 级做映射,否则会误杀合法 token 或放过非法 token;相关”token healing”与掩码构建见 结构化输出与约束解码
  • glitch token:词表里有、但训练语料中几乎没出现过的 token,嵌入训练不充分,输入它们可能触发异常输出

面试回答(2分钟版)

Tokenizer 的作用是把文本切成词表里的子词单元再映射成整数 id,模型只看 id,不看字符。之所以用子词而不是字或词,是在词表大小和序列长度之间折中:字符级序列太长,词级词表爆炸又有 OOV,子词高频整词保留、低频词拆词根词缀,基本没有 OOV。主流算法有三种:BPE 从字符起步反复合并最高频的相邻 pair,GPT、Llama 用的都是它,现在基本是 byte-level 版本,基础单元是 256 个字节,任何文本都能表示;WordPiece 是 BERT 用的,合并依据是似然提升而不是纯频率;Unigram 反过来,从大词表往下剪,用 Viterbi 找最优切分,SentencePiece 常跑这个。词表大小是个权衡,词表大同样文本 token 少、上下文更省,但 embedding 和 softmax 更大,现在趋势是从三五万涨到十几二十万甚至二十多万,多语言覆盖更均匀。工程上要注意几点:第一,计费和上下文都按 token 算,中文在英文主导的词表上一个字可能要一个多 token,比英文贵、有效上下文短,要用对应 tokenizer 实测而不是估;第二,max_tokens 只限输出,上下文要给输出留预算;第三,特殊 token 像 BOS/EOS 和 chat template 里的角色标记是原子单元,拼错模板会明显掉点;第四,很多怪现象根子在分词——strawberry 数不对 r 是因为模型看到的是 token 不是字母,算术差是因为数字被切碎,约束解码要处理 token 跨字符边界的问题。

追问与易错

追问方向:

  • BPE 和 WordPiece 的本质区别? → 都是自底向上合并,区别在选哪个 pair 合并:BPE 选频率最高的,WordPiece 选 freq(ab)/(freq(a)·freq(b)) 似然提升最大的;切分时 BPE 按合并规则顺序,WordPiece 贪心最长匹配并用 ## 标记词内续接
  • 为什么 GPT 用 byte-level BPE 而不是字符级? → 字符级要覆盖全部 Unicode,词表里仍会有没见过的字符变 UNK;以 256 个字节为基础单元,任何字符串都能表示、彻底无 OOV,代价是非拉丁文字会被拆成更多 token
  • 词表越大越好吗? → 不是。大词表省序列长度,但 Embedding 矩阵和 LM head 参数线性增长、长尾 token 训练不充分、小模型上还会显著拉高参数占比;一般随模型规模和语种覆盖需求放大,主流在 10 万–25 万级
  • 为什么模型数不清 strawberry 里有几个 r? → 模型看到的是 str aw berry 这类 token id,字母信息只能从训练中间接学到;让它先逐字母拆开再数,或调用工具,就基本不会错
  • 中文一个字等于几个 token? → 取决于词表:英文主导的旧词表可能 1.5–2 token/字,GPT-4 级词表约 1–1.5,中文优化的 Qwen/DeepSeek 词表常见词能整词成 token,平均不到 1 token/字;精确值用 tiktoken 或 AutoTokenizer 实测

易错点:

  • ❌ “token 就是单词/汉字” → token 是子词单元,一个词可能拆成多个 token,一个 token 也可能是一个词带前导空格
  • ❌ “各家模型的 token 数差不多,按字数估就行” → 词表不同切法差异可到一倍,计费和上下文预算必须用目标模型的 tokenizer 实测
  • ❌ “max_tokens 是总上下文长度” → 它只限输出;输入 + 输出之和受上下文窗口限制
  • ❌ “strawberry 数错 r 说明模型不会数数” → 根因是 tokenization 让模型看不到字母,不是算术能力问题