在 AI 技术遍地开花的今天, Token (词元) 绝对是你绕不开的核心概念. 你大概率知道大模型按 Token 计费,也听说过它是模型处理信息的基础单元.
但你是否曾深入思考:这些 Token 究竟是怎么从文本里 "变" 出来的?
今天这篇文章, 我想和你一起, 聊聊大模型是如何分词的
我是印刻君, 一位探索 AI 的前端程序员, 关注我, 让 AI 知识有温度, 技术落地有深度
在线体验 Token 分词
讲理论之前,先上手体验一把才够直观
你可以打开 OpenAI 的分词网站 platform.openai.com/tokenizer, 亲眼看看 OpenAI 的大语言模型是怎么把文本切成 Token 的:
比如我输入这句话: I'm InkJun, a front-end programmer exploring AI. Follow me to make AI knowledge more engaging and its practical applications more in-depth.
GPT-4o 给出的分词结果长这样:

体验完后,你会发现两个有意思的现象:
第一,Token 并非简单的字或词,而是一种介于两者之间的"子词"单元.
第二,不同模型对同一句话的切分结果, 往往还不一样.
这自然会引出一个更深层的问题: 大模型为何不直接采用我们熟悉的"字"或"词"作为处理单元?
为什么要切分 Token?而不是直接用字或词?
要回答这个问题, 我们需要探讨 AI 设计中的一个核心考量:在词汇表规模与语义信息保留之间寻求平衡
如果按"整词"切分, 会出现两个问题:
- 词汇表越变越大, 语言中存在大量词形变化(如 apple, apples, applepie), 词汇表规模可轻易达到数十万乃至上百万,给模型训练带来巨大压力
- 遇到新词就 "卡壳", 旦碰到训练语料里没见过的词(也就是未登录词, Out-of-Vocabulary),模型就没法识别了
那如果按 "单字 / 字符" 切分呢? 同样有问题:
- 序列太长,计算慢, 基本单元太小,文本序列会变得很长,计算开销急剧增加
- 语义稀疏, 单个字符(如 "a", "p")携带的语义信息有限,模型难以从中学习到复杂的语言结构
因此, 我们需要一种折中方案 ------既能将词汇表控制在合理范围内, 又能有效保留语义信息. 这正是 Subword (子词) 切分法的核心价值, 而其中的代表性算法就是我们接下来要讨论的 BPE
核心分词算法:BPE (Byte Pair Encoding)
需要指出的是, 当前主流的大模型(如 GPT-4, LLaMA, Qwen)实际上采用的是 BPE 的变体 ------ BBPE (Byte-Level BPE).
不过, 为了理解 BBPE, 我们必须先掌握其核心思想------BPE
BPE 算法 ------ 基于频率的迭代式合并
BPE 的原理非常直观,可以概括为:在语料库中, 找出频率最高的相邻符号对, 把它们合并为一个新的符号, 然后重复这个过程.
它不依赖任何语法知识,仅通过统计大规模语料中符号出现的频率,来动态地构建词表
工作原理 (以中文绕口令为例)
⚠️ 学习提示:
为便于理解 "统计与合并" 的核心逻辑, 此处以汉字为符号进行演示.
请注意, 这仅为一个思想实验 . 实际的大模型为了实现跨语言兼容, 其操作的对象是字节,这将在下一节 BBPE 中详述
假设输入文本为: 八百标兵奔北坡, 北坡炮兵并排跑, 炮兵怕把标兵碰, 标兵怕碰炮兵炮.
- 初始状态下, 每个汉字都是一个独立的符号:
['八', '百', '标', '兵', '奔', '北', '坡', ',' , '北', '坡', '炮', '兵', ...]
-
合并 "标兵" (频次 3)
算法扫描语料, 发现相邻符号对
('标', '兵')出现了 3 次, 为当前最高频对. 因此,将其合并为新符号 "标兵":['八', '百', '标兵', '奔', '北', '坡', ',', '北', '坡', '炮', '兵', ...] -
合并 "炮兵" (频次 3)
算法继续扫描,发现
('炮', '兵')同样出现 3 次,执行合并['八', '百', '标兵', '奔', '北', '坡', ',', '北', '坡', '炮兵', ...] -
合并 "北坡" (频次 2)
符号对
('北', '坡')出现 2 次, 成为当前最高频对,执行合并['八', '百', '标兵', '奔', '北坡', ',', '北坡', '炮兵', ...] -
处理剩余低频符号
剩余符号对的频次均为 1,算法会继续按频率排序进行合并(实际训练中通常会设定停止条件,如达到预设词表大小)
这么一步步迭代下来, "标兵""炮兵""北坡" 这些高频组合就被合并出来了
BPE 的局限
然而, 如果我们直接以汉字作为基础符号集, 将面临一个严峻的问题:
- 仅常用汉字就有数千, 再加上生僻字, Emoji, 多语言符号, 初始词表规模会非常庞大, 使得 BPE 的优势难以发挥
- 同时, 对于超出基础字符集的符号, 仍可能出现 UNK (Unknown) 问题
为了克服上述局限,工程师将 BPE 的思想应用到了更底层的 "字节" 层面, 从而催生了 BBPE 算法
终极方案:BBPE ------ 现代大模型的标配
BBPE (Byte-Level BPE) 将 BPE 算法应用于字节 序列, 是当前 GPT-4, Claude, LLaMA 等先进模型采用的主流分词方案
在深入 BBPE 之前, 我们先补上关键的背景知识: Unicode 码与字节编码的关系
Unicode 与字节的关联
我们日常看到的文字,符号 (比如 "八" "a" "😊"), 在计算机里本质上都需要被编码成数字才能处理, 而 Unicode 就是这套 "字符→唯一数字" 的映射标准. 比如汉字 "八" 对应的 Unicode 码是 U+516B, 英文字母 "a" 是 U+0061, Emoji"😊" 是 U+1F60A
Unicode 解决了 "字符有唯一标识" 的问题, 但它只是 "数字编号", 并没有规定这些数字该如何转换成计算机存储和传输的字节------ 这就需要 UTF-8 和 UTF-16 等编码方式, 其中 UTF-8 是目前最主流的字节编码方案
UTF-8 采用 "变长编码" 规则:
- 英文、数字等 ASCII 字符: 1 个字节就能表示(比如 "a" → 0x61);
- 常用汉字: 3 个字节 (比如 "八" → 0xE5 0x85 0xAB);
- 特殊符号 / Emoji:可能需要 4 个字节 (比如 "😊" → 0xF0 0x9F 0x98 0x8A)
简单说: Unicode 给每个字符发了唯一 "身份证号", UTF-8 则把这个 "身份证号" 转换成了计算机能看懂的字节序列. 而 BBPE 就是从这串字节序列开始工作的.
BBPE 是怎么做的?
- 万物皆字节 : 在计算机的视角下,所有文本------无论是英文, 中文还是 Emoji------其本质都是一串字节序列
- 英文 "a" = 1 个字节
0x61 - 中文 "八" = 3 个字节
0xE5 0x85 0xAB
- 英文 "a" = 1 个字节
- 基础词表极小 : 字节只有 256 种可能的值 (0x00-0xFF) 这意味着, 初始词表的大小固定且仅为 256,从根本上解决了未登录词 (OOV) 问题
- 从字节开始的层级合并 : BBPE 算法处理的不是"八百...", 而是其底层的字节表示:
E5 85 AB E7 99 BE ...- 算法发现字节对
(0xE5, 0x85)高频共现 -> 合并为新单元 - 接着发现
(E5 85, 0xAB)高频共现 -> 再合并 -> 由此构建出汉字"八"的表示 - 最终, 算法发现"八"和"百"的表示也高频共现 -> 再次合并 -> 从而学习到词语"八百"的表示
- 算法发现字节对
BBPE 使得模型无需预先定义汉字或词汇. 它从最底层的字节开始, 通过统计频率自底向上地学习.这便是 GPT 能够使用同一套精简的词表,高效处理中文、英文、代码乃至多样化 Emoji 的根本原因
总结
Token 作为大模型处理信息的核心单元, 既不是简单的字也不是完整的词, 而是兼顾效率与语义的子词单元. 大模型之所以选择子词切分而非字符或整词, 本质是为了平衡词汇表规模与语义信息保留的需求.
BPE 算法通过 "高频相邻符号对迭代合并" 的思路, 实现了这种平衡, 但直接以字符为基础会面临词表过大, 兼容多语言困难等问题.
而 BBPE 将 BPE 的核心逻辑下沉到字节层面, 凭借 256 个基础字节的极小初始词表, 不仅解决了未登录词难题, 还实现了跨语言跨符号体系的高效分词, 成为 GPT-4 等现代大模型的标配方案
我是印刻君, 一位探索 AI 的前端程序员, 关注我, 让 AI 知识有温度, 技术落地有深度
(附) 代码实现示例
如果你对技术实现感兴趣,可以运行下面这段 Python 代码来模拟 BPE 的合并过程:
python
import collections
def get_stats(vocab):
"""统计相邻字符对的频率"""
pairs = collections.defaultdict(int)
for i in range(len(vocab) - 1):
pair = (vocab[i], vocab[i+1])
pairs[pair] += 1
return pairs
def merge_vocab(pair, v_in):
"""将频率最高的字符对合并"""
v_out = []
bigram = pair
replacement = ''.join(pair) # 确保合并后的结果是字符串
i = 0
while i < len(v_in):
if i < len(v_in) - 1 and v_in[i] == bigram[0] and v_in[i+1] == bigram[1]:
v_out.append(replacement)
i += 2
else:
v_out.append(v_in[i])
i += 1
return v_out
# 输入文本
text = "八百标兵奔北坡,北坡炮兵并排跑,炮兵怕把标兵碰,标兵怕碰炮兵炮"
tokens = list(text)
# 模拟合并过程
for i in range(5):
pairs = get_stats(tokens)
if not pairs: break
best = max(pairs, key=pairs.get)
if pairs[best] < 2: break # 频率小于2则停止
tokens = merge_vocab(best, tokens)
print(f"Step {i+1}: 合并 {best} -> {''.join(best)}")
print(f"当前 tokens: {tokens}")