模型架构 · Tokenizer
学习舱 / 模型架构 / Tokenizer

Tokenizer 与分词(BPE)

整理中 深度 · 应用 更新 2026-09-09

前置 Agent

Tokenizer 与分词(BPE)

一句话定位:Tokenizer 把文本切成模型的最小计算单元(token),主流算法是 BPE——本文讲词表怎么炼成、大小怎么取舍、为什么同一段中文在不同模型下账单差一倍。前置:Agent 基础——先知道模型以 token 为输入输出单位。

先从看不懂的账单说起

同一段一千字的中文项目总结,交给模型 A 计费 1000 个 token,交给模型 B 只要 600 个;反过来一段英文,B 反而比 A 贵。

同一段文字、同样的信息量,账单差出近一倍——预算该怎么估?

问题出在你和账单之间隔着一层:分词器(Tokenizer)。文本不是按「字」或「词」进模型的,而是按 token——token 怎么切、切多细,完全由每个模型自带的分词器决定,而分词器的规则由训练时的统计决定。不搞懂它,计费估不准,上下文预算也估不准。

分词器到底怎么工作?token 数由什么决定?

核心算法叫 BPE(Byte Pair Encoding,字节对编码):从字符出发,反复把最高频的相邻组合合并成新单元,直到词表够大。词表多大、语料里有什么语言,决定了你的文字被切成几块。先把训练过程拆开看:

BPE 训练:把高频组合炼进词表 ① 语料统计 扫一遍训练语料 词与出现频次 初始词表=全部单字符 ② 统计相邻对 每一对相邻单元 数它出现多少次 「th」「he」名列前茅 ③ 合并最高频 把冠军对熔成 一个新单元进词表 语料里同步替换 ④ 循环②③ 直到词表达到目标大小 产出:词表(如 15 万条)+合并规则 使用时按同样规则切分新文本 例:语料里「the」出现百万次 → t+h、th+e 早早进词表 → 「the」最终是 1 个 token 读法:①→②→③→④ 是训练期;使用期不走训练,只按已固化的合并规则切分

图:BPE 训练流程——①语料统计 ②统计相邻对 ③合并最高频 ④循环至词表满;使用期按固化规则切分

渐进拆解:一群人的聊天黑话

BPE 就是给一群老玩家编一部「黑话词典」。最初大家只会说单字(初始词表);「大龙」「开团」这类组合天天说,就合并成一个词收进词典;词典越编越厚(词表变大),说一句话用的词条数就越少——沟通更快(序列更短),但词典本身也更重(词表参数更多)。不同游戏圈编的词典不一样,同句黑话在不同圈里要拆成不同数量的词——这就是不同模型 token 数不同的根源。词典里没有的生僻说法,只能退回一个字一个字说(字符兜底)——生僻内容的 token 数猛涨,就是这个道理。

BPE 流程(①–④)

训练期就四步:扫语料建词频表、逐字符切分并统计相邻对频率、把最高频对熔成新单元收进词表、循环到词表达到目标大小——逐步细节见前图。使用期(你发消息时)不走训练:按已固化的合并规则做贪心匹配,同样的文本永远得到同样的切分。

词表大小:省 token 与省参数的取舍

词表越大,常用词越可能整个进词表,同样文本切出的 token 越少——上下文装得多、计费也便宜;但代价是 embedding(把每个词条映射成向量的那层参数)随词表线性涨,低频词条还训练不充分。行业词表正沿着这条取舍线一路扩表(从早期约 5 万到约 26 万,GPT-4 那代 cl100k 的精确值是 100,277),每一轮扩表的主要动力都是多语言覆盖——词表里中文词条越多,中文越省 token。

特殊 token:词典里的「控制符」

词表里除了从语料学来的词条,还有一批人工指定的特殊 token:标记文本结束(如 <|endoftext|>)、标记对话角色边界、标记工具调用开始结束——它们不是语言学单元,是协议信号,模型被专门训练来响应它们。这也是为什么别在普通文本里手写特殊 token 的字面量:分词器会把这类字面量按特殊 token 切出,可能意外改变模型对文本结构的理解(这颗雷在安全篇会拆)。顺带一提,「字节级」BPE 的意义正在这里兜底:任何新字符——emoji、生僻汉字——都能退回字节表示,模型永远「认识」你的输入,只是代价大。

对话协议是特殊 token 最典型的用武之地:多轮对话本是「谁说了什么」的结构化对象,进模型前要被拍平成一维 token 序列——每轮内容用起始与结束标记包住、标上说话人,各家的标记各有各的写法。推理时你的输入也被同样包进协议、再拼上一个「助手轮开始」的标记才交给模型采样——模型接住的从来不是一条裸问题,是一条编码好的对话流。

多语言与代码:切分粒度差异巨大

语料决定词典构成,词典决定各语言的切分粒度。

中文效率实测。 三代口径:

  • GPT-2 旧分词器:词表里几乎没有中文词条,汉字只能按字节拆,实测约 2 token/字
  • cl100k(GPT-3.5/4 代):改进到约 1.1–1.6 token/字(本地 tiktoken 实测)
  • Qwen:官方文档给的口径是中文约 1.5–1.8 字/token(约 0.6 token/字)

同样的中文,Qwen 的分词器比 cl100k 再省约一半。o200k 相比 cl100k 把中文的 token 低效比(同等内容相对英文的 token 倍数)从约 2.08 倍压到约 1.34 倍,非英语覆盖是这一代词表扩容的主因。

代码与计费。 代码场景同理:缩进的连续空格、驼峰变量名、JSON 标点各有切分习惯,成熟分词器都为这些模式做了专门优化。token 数与计费直接挂钩:API 按输入/输出 token 数计价,字数相同、分词器不同,账单可以差出近一倍——估预算要用目标模型的分词器算,不要按字数估。

不同模型的切分差异还有行为层的后果:模型只见 token 不见字母,拼写、数数这类字符级任务天然吃亏——这条因果链的完整展开见LLM 心理学。

词表规模的演进与中文效率的改善,一张图对照(token 数为各来源口径的近似值):

词表规模演进:多语言驱动的一路扩表 Llama 2 3.2 万 GPT-2 约 5 万 · 中文按字节拆、约 2 token/字 cl100k 约 10 万 · 中文约 1.1–1.6 token/字 Llama 3 约 12.8 万 · 官方强调多语言效率提升 Qwen 约 15.2 万 · 中文约 1.5–1.8 字/token o200k 约 20 万(GPT-4o)· 中文较 cl100k 省超三成 Gemma 3 约 26 万 · 开源词表之最 条长 = 词表规模;右注 = 该分词器的中文效率口径(近似值) 读法:扩表潮由多语言覆盖驱动;同段中文在不同词表下 token 数可差近一倍

图:词表演进——3.2 万到 26 万的扩表潮由多语言覆盖驱动;右侧为中文效率口径

大白话:BPE 就是一部按使用频率自动编写的黑话词典——常说的组合收进词典变成一个词。词典编给谁看(语料)、编多厚(词表大小),决定了你的话要被切成几段、算多少钱。

技术定义与生态现状:BPE 最初是 1994 年的压缩算法,2016 年被引入神经机器翻译做子词切分,GPT-2 起采用字节级 BPE(byte-level BPE,任何字符都有兜底表示)并成为主流;头部模型词表基本收敛在 10 万–20 万档,OpenAI 的 tiktoken 是最常用的开源实现之一。

动手:手写一个迷你 BPE,跑通合并全过程

下面是可运行的 Python 代码(语料是经典教材例子:四个词及出现频次),完整走一遍①–④:

word_freqs = {"low": 5, "lower": 2, "newest": 6, "widest": 3}   # 语料统计:词→频次
splits = {w: list(w) + ["</w>"] for w in word_freqs}            # ① 语料统计:逐字符切分,</w> 标词尾
merges = {}
for _ in range(5):                                              # ④ 循环:共学 5 条合并规则
    stats = {}                                                  # ② 统计所有相邻对频率
    for w, sym in splits.items():
        for a, b in zip(sym, sym[1:]):
            stats[a, b] = stats.get((a, b), 0) + word_freqs[w]
    best = max(stats, key=stats.get)                            # ③ 取最高频对合并(与 s+t、t+</w> 并列,取先出现者)
    for w, sym in splits.items():                               #    语料里同步替换
        i = 0
        while i < len(sym) - 1:
            if (sym[i], sym[i + 1]) == best:
                sym[i:i + 2] = [best[0] + best[1]]
            else:
                i += 1
    merges[best] = best[0] + best[1]
print(merges)

实际运行输出(已验证):

{('e', 's'): 'es', ('es', 't'): 'est', ('est', '</w>'): 'est</w>', ('l', 'o'): 'lo', ('lo', 'w'): 'low'}

对着输出读规律:第一条合并是 e+s→es(9 次,全语料最高频,与 s+t、t+</w> 并列各 9 次),前三条合并(es、est、est</w>)后 newest 变成 n·e·w·est</w>——高频后缀「est」自动被炼成一个单元;随后 l+o→lo、lo+w→low,高频词「low」整个收编为单元。这就是 BPE 的全部魔法:频率驱动的贪心合并。把语料换成中文聊天记录,同样这套代码炼出来的就是中文高频字词——词表差异的来源,至此眼见为实。

边界与取舍:分词不是越省越好

词表大不等于全好

大词表省 token,但 embedding 与输出层的参数量随词表涨(15 万词表的输出层参数以亿计),低频词条训练不充分反而降低质量;小词表序列变长,注意力计算量随序列长度增长。这是一个真正的工程取舍,各家按自己的语料分布与服务定位选点。

对中文用户的实际建议

估预算用目标模型的分词器算(各家多提供 token 计数接口或本地工具);同等条件下,对中文语料做过多语言优化的词表(如约 15 万条级别并明确宣称中文优化的)计费与上下文效率通常更友好;关键场景先拿真实文本抽样测 token 数,别用「1 字 = 1 token」的粗略口径——它对某些模型成立、对另一些差一倍。

坏味道预警:手写特殊 token

拼接用户输入进 prompt 时,若用户文本里含有特殊 token 的字面量(如 <|im_end|>),可能意外改变模型行为——成熟框架会在送入前转义。分词是模型世界的「文本入口」,入口的协议边界问题(注入、越权触发)在后续安全篇整块展开。至于「为什么不统一用一个词表」:词表在训练前锁定、训练后难换(换词表等于重塑输入层),各家语料与侧重不同,统一反而牺牲效率——多样性是取舍的结果,不是分裂。

自测

合上文章先自己答,再展开对照:

同一段中文在不同 tokenizer 下计费为什么差很多?手写 BPE 变体验证过吗?

计费差异的根源在词表构成:分词器训练语料里某语言占比高,高频字词就整体进了词表,切分粒度粗、token 少;反之逐字甚至逐字节切,token 多。实测数据:中文在 Qwen 的分词器下约 1.5–1.8 字/token(约 0.6 token/字);GPT-2 旧分词器按字节拆约 2 token/字,cl100k 实测约 1.1–1.6 token/字——Qwen 比 cl100k 省约一半、比 GPT-2 旧表省约三分之二——所以估预算必须用目标模型的分词器。手写 BPE 已验证:对 {low×5, lower×2, newest×6, widest×3} 语料跑五行循环版 BPE,五轮合并依次得到 es、est、est</w>、lo、low——高频后缀 est 与高频词 low 被自动炼成单元,频率驱动的贪心合并眼见为实。

词表越大越好吗?词表大小影响什么?

不是。大词表的收益是切分粒度粗、序列短(上下文省、计费省);代价是 embedding 与输出层参数随词表线性增长,且低频词条训练不充分。行业从 3.2 万一路扩到 20 万+,主要动力是多语言覆盖——不是单纯越大越好,而是按语料分布与服务定位选点。对使用者的落点是:中文为主的应用优先选对中文做过词表优化的模型,计费与上下文效率都更友好。

参考与延伸

下一站:Sampling 与生成参数——token 备好了怎么选:temperature、top-p 背后的采样机制。

本节点大纲与自测题(3 个子点 · 2 问)

大纲

  • BPE 流程与词表构建
  • 词表大小的取舍与特殊 token
  • 多语言与代码的分词差异、token 与计费的关系

自测——答不上来就是缺口

  • 同一段中文在不同 tokenizer 下计费为什么差很多?手写 BPE 变体验证过吗?
  • 词表越大越好吗?词表大小影响什么?