“通信的根本问题是:在一点上精确或近似地复现在另一点上所选取的消息。”
— Claude Shannon, A Mathematical Theory of Communication (1948)
为什么需要编码?
你正在屏幕上阅读这篇文章。这些汉字、标点、空格——它们是怎么出现在屏幕上的?
这是一个细想就会觉得不可思议的事情:计算机根本不认识"字"。在计算机的世界里,没有横竖撇捺,没有 ABCD,只有两样东西——0 和 1。每一块内存、每一块硬盘、每一根网线里流淌的,都是密密麻麻的电信号:高电平是 1,低电平是 0。
但我们每天用计算机做的事情——写文档、发消息、刷网页——全都离不开文字和符号。那么问题来了:
怎么把人类的符号,塞进只有 0 和 1 的世界里?
答案就是:编码。
从符号到数字,从数字到二进制
编码做的事情,可以拆成两步:
- 给每个符号编一个号码:比如 A 是 65,B 是 66,“中” 是 20154。这一步建立了符号到数字的映射。
- 把数字转成二进制:65 变成
01000001,20154 变成0100111000111010。这一步让数字能在计算机里存储和传输。
用一个表格来感受一下:
| 符号 | 编号(十进制) | 编号(二进制) |
|---|---|---|
| A | 65 | 01000001 |
| Z | 90 | 01011010 |
| 中 | 20154 | 0100111000111010 |
| 😂 | 128514 | 00000001 11110110 00000010 |
所以,当你打出一个"中"字的时候,真正在计算机里发生的事情是:键盘输入 → 查编码表找到编号 20154 → 转成二进制 0100111000111010 → 存进内存。读取的时候反过来:从内存读到二进制 → 转回编号 20154 → 查编码表 → 在屏幕上渲染出"中"字。
编码,就是人类符号和二进制世界之间的翻译官。
一个更根本的矛盾
但这里有一个更深层的问题:人类的符号是无限的。
全世界有上百种语言、数万个汉字、无穷多的 emoji,而且还在不断增加。但计算机的存储是有限的——你用 8 个 bit(1 个字节)只能表示 256 个不同的东西,用 16 个 bit 能表示 65536 个,用 32 个 bit 能表示约 43 亿个。
所以编码的核心矛盾,从一开始就是:
用有限的 0 和 1,去表示无限的符号。
这个矛盾贯穿了整个编码的历史。从 ASCII 到 Unicode,从 UTF-8 到 LLM 的 Tokenizer,每一次演进都是在尝试更好地回答同一个问题:
我们到底需要多少个 bit?哪些符号值得占一个位置?怎么在空间和兼容性之间取得平衡?
接下来,让我们从最古老的 ASCII 开始,看看人类是怎么一步步走到今天的。
ASCII 编码
1963 年,美国国家标准协会(ASA)发布了一份编码标准——ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)。
这是计算机历史上第一个被广泛采用的字符编码标准,也是理解一切后续编码的起点。
7 个 bit,128 个位置
ASCII 的设计极其简洁:用 7 个 bit 来表示一个字符,总共 $2^7 = 128$ 个位置,编号从 0 到 127。
为什么是 7 个 bit 而不是 8 个?因为在 1960 年代,通信线路的带宽非常宝贵,每多传一个 bit 都是成本。7 个 bit 已经够用了,第 8 个 bit 留作奇偶校验位(parity bit),用来检测传输过程中是否出错。
这 128 个位置被精心分配成了两大类:
| 范围 | 数量 | 内容 |
|---|---|---|
| 0–31 | 32 个 | 控制字符:换行、回车、制表符、删除等 |
| 32–127 | 96 个 | 可打印字符:大小写字母、数字、标点符号 |
控制字符今天不太常见了,但有几个你一定认识:
0x0A(编号 10):换行符(\n),每一行文字的结尾都有它0x0D(编号 13):回车符(\r),光标回到行首0x09(编号 9):制表符(\t),代码缩进用的就是它
可打印字符就更熟悉了:
32: (空格)
48–57: 0 1 2 3 4 5 6 7 8 9
65–90: A B C ... X Y Z
97–122: a b c ... x y z
这里有一个很巧妙的设计:大写字母 A 是 65,小写字母 a 是 97,刚好差了 32。这意味着只要翻转第 6 个 bit(从 0 变 1 或从 1 变 0),就能在大写和小写之间切换,不需要额外的查表操作。
一个精心设计的排列
ASCII 的编号并不是随意分配的。仔细观察你会发现:
- 数字
0的编号是 48,而不是 0——但所有数字是连续排列的(48–57),所以只要减去 48 就能得到数字本身的值 - 字母也是连续排列的,这让排序、查找、大小写转换都可以用简单的数学运算完成
这不是巧合,而是刻意的设计。在那个计算资源极其匮乏的年代,能用硬件级别的简单操作完成字符处理,是一个巨大的优势。
够用吗?
如果你只写英文,ASCII 其实够用了。26 个字母(大小写 52 个)、10 个数字、一些标点和控制字符,128 个位置绑绑有余。
但问题也很明显:
- 法语有
é、è、ê、ç——不在 ASCII 里 - 德语有
ü、ö、ä、ß——不在 ASCII 里 - 中文?几万个汉字——远远超出了 128 个位置的容量
ASCII 是为英语设计的,而且是为美式英语设计的。在那个年代,计算机还是个昂贵的庞然大物,主要使用者是美国政府和军方,所以这个局限性并不算问题。
但随着计算机开始走向全世界,128 个位置就远远不够了。于是,一场"各自为政"的编码混战开始了……
本地编码的混乱时代
ASCII 留下了 128 个空位(编号 128–255),并且当时还有一个约定:一个字节是 8 个 bit,能表示 256 个值。ASCII 只用了一半,剩下的一半,每个人都可以自由发挥。
于是,一场"填空题"竞赛开始了。
各自为政
每种语言、每个地区都设计了自己的编码方案:
| 编码 | 地区/语言 | 亮点 |
|---|---|---|
| ISO-8859-1 (Latin-1) | 西欧语言 | 填满了 128–255,覆盖法语、德语、西班牙语等 |
| ISO-8859-2 | 中欧语言 | 波兰语、捷克语、匈牙利语 |
| ISO-8859-5 | 西里尔字母 | 俄语、乌克兰语 |
| GB2312 | 中文简体 | 中国国家标准,收录 6763 个汉字 |
| GBK | 中文简体 | GB2312 的扩展,收录 21886 个汉字 |
| Big5 | 中文繁体 | 台湾地区标准,繁体汉字 |
| Shift-JIS | 日语 | 包含平假名、片假名和汉字 |
| EUC-KR | 韩语 | 包含韩文字母和汉字 |
这些编码方案有一个共同的特点:它们都兼容 ASCII。也就是说,前 128 个位置(0–127)完全一样,A 还是 65,\n 还是 10。区别只在于 128 之后的部分。
这很聪明——至少英文内容在任何编码下都不会乱码。
问题出在哪里?
但一旦你跨越了语言边界,灾难就来了。
问题一:同一个编号,不同的字符
编号 200,在 Latin-1 里是 È,在 Cyrillic 里是 Ш,在 GBK 里是一个完全不同的汉字。你用 Latin-1 编码保存的文件,用 GBK 打开,看到的就是乱码。
问题二:中文/日文/韩文需要两个字节
ASCII 的一个字节最多表示 256 个字符,但汉字有几万个,一个字节根本塞不下。所以 GBK、Shift-JIS 这些编码用了双字节方案:第一个字节标记"这是一个汉字",第二个字节指定具体是哪个汉字。
这就产生了一个麻烦的问题:你怎么知道一个字节是独立的 ASCII 字符,还是某个双字节字符的一半?GBK 的做法是看第一个字节是否大于 127——如果是,就把它和下一个字节一起解读为一个汉字。
这种方案叫做变长编码,后面的 UTF-8 也用了类似的思路,但设计得优雅得多。
问题三:一封邮件里的巴别塔
想象一下:你用 GBK 编码写了一封包含中英文的邮件,发给一个日本同事。他的邮件客户端默认用 Shift-JIS 解码。结果?中文部分变成了一堆乱码,因为同样的两个字节在 Shift-JIS 下对应的是完全不同的字符。
这不是假设——这是 1990 年代每天都在发生的事情。人们不得不在邮件末尾加上"请用 GBK 编码查看"这样的提示,或者在论坛帖子里讨论"怎么设置编码才不乱码"。
一个讽刺的局面
到了 1990 年代初,计算机世界里并存着几十种编码方案,它们彼此不兼容。你甚至可能在同一台电脑上安装多个"代码页"(code page),在不同软件之间切换。
这就像一个巴别塔——每个人都只说自己的语言,彼此听不懂。
很明显,这个世界需要一种统一的编码方案:一个能包含所有语言所有字符的标准。于是,Unicode 登场了。
Unicode:统一的字符集
1987 年,Joe Becker(来自 Xerox)和 Lee Collins、Mark Davis(来自 Apple)开始讨论一个大胆的想法:能不能创建一个涵盖全世界所有文字的统一字符集?
三年后,1991 年,Unicode 1.0 发布。它的目标简单而宏大:
为世界上每一个字符分配一个唯一的编号,从此告别乱码。
先澄清一个关键概念:字符集 ≠ 编码方案
在继续之前,我们需要弄清一个经常被混淆的区别:
- 字符集(Character Set):定义了"有哪些字符"以及"每个字符的编号是多少"。它就像一本巨大的花名册,给每个字符发了一个身份证号。
- 编码方案(Encoding):决定这个编号在计算机里怎么存储——用几个字节、按什么规则排列。
Unicode 是一个字符集,不是编码方案。
它只做一件事:给每个字符分配一个唯一的编号,叫做码位(Code Point)。至于这个码位在内存里占几个字节、怎么排列,Unicode 不管,那是 UTF-8、UTF-16、UTF-32 这些编码方案的事。
这个区分非常重要。很多人说"用 Unicode 编码",其实这是一个不准确的表述——Unicode 只是给字符编号,编码是 UTF-8 等方案做的事。
码位(Code Point)
Unicode 给每个字符分配的编号叫做码位,书写格式是 U+ 后面跟着十六进制数。例如:
| 字符 | 码位 | 十进制 |
|---|---|---|
| A | U+0041 | 65 |
| 中 | U+4E2D | 20013 |
| 🎉 | U+1F389 | 127881 |
| 𝟙 | U+1D7D9 | 120793 |
是的,emoji 也是 Unicode 的一部分——它们并不是什么特殊的东西,和 A、中一样,都是 Unicode 字符集里的一个编号。
Unicode 的空间有多大?
Unicode 目前的编码空间从 U+0000 到 U+10FFFF,总计 1,114,112 个码位。这个空间被划分成了 17 个平面(Plane),每个平面有 65,536 个码位:
| 平面 | 范围 | 状态 |
|---|---|---|
| 第 0 平面(BMP,基本多文种平面) | U+0000 – U+FFFF | 已分配大量字符,最常用 |
| 第 1 平面(SMP,补充多文种平面) | U+10000 – U+1FFFF | emoji、古文字、数学符号等 |
| 第 2 平面(SIP,补充表意文字平面) | U+20000 – U+2FFFF | 罕见汉字(CJK 扩展 B–I) |
| 第 3–13 平面 | U+30000 – U+DFFFF | 暂未使用或预留 |
| 第 14 平面(SSP,补充特殊用途平面) | U+E0000 – U+EFFFF | 标签字符、变体选择符 |
| 第 15–16 平面 | U+F0000 – U+10FFFF | 私用区 |
绝大多数日常使用的字符都在第 0 平面(BMP)里——英文、中文、日文、韩文、阿拉伯文……基本上你能想到的文字都在这里。emoji 和一些特殊符号在第 1 平面。
110 万个码位听起来很多,但实际上 Unicode 是很"节约"的。截至 Unicode 15.0,已经分配的码位只有约 15 万个,还有大量空间留给未来的新字符。
Unicode 解决了什么?
回到上一节的混乱:
- 编号 200 在 Latin-1 和 GBK 里是完全不同的字符 → 在 Unicode 里,每个字符有且只有一个码位,不存在歧义
- 不同编码互不兼容 → 全世界的软件都用同一本花名册,只要大家都支持 Unicode,就不会乱码
- 新字符无处安放 → 110 万个码位,足够容纳未来几十年甚至更久的新字符
但问题来了:怎么存储?
Unicode 解决了"谁是谁"的问题,但没有解决"怎么存"的问题。
U+4E2D(“中”)需要几个字节来存储?一个最直觉的想法是:每个码位用固定 4 个字节表示。这样可以覆盖所有码位,简单直接——这就是 UTF-32。
但 UTF-32 的问题是浪费空间。对于英文内容来说,每个字符本来只需要 1 个字节(ASCII 时代),现在硬生生扩展到了 4 个字节,文件大小翻了 4 倍。一篇 1MB 的英文文章用 UTF-32 存就变成了 4MB。 能不能有一种方案,让 ASCII 字符还是 1 个字节,汉字用 2–3 个字节,emoji 用 4 个字节——按需分配,不浪费?
这就是 UTF-8 要解决的问题。
UTF-8:最成功的编码方案
1992 年,Ken Thompson 和 Rob Pike——两位 Unix 传奇人物——在一条晚餐餐巾纸上设计出了 UTF-8 的原型。第二年,RFC 2279 正式发布了 UTF-8 标准。
这个编码方案如此优雅,以至于今天互联网上 98% 以上的网页 都在使用它。如果你打开一个网页,看到 <meta charset="UTF-8">,那就是它在工作。
核心思想:变长编码
UTF-8 的核心思想很简单:不同的字符用不同数量的字节来表示,高频字符少用字节,低频字符多用字节。
具体规则如下:
| Unicode 码位范围 | UTF-8 字节数 | 二进制格式 |
|---|---|---|
| U+0000 – U+007F | 1 字节 | 0xxxxxxx |
| U+0080 – U+07FF | 2 字节 | 110xxxxx 10xxxxxx |
| U+0800 – U+FFFF | 3 字节 | 1110xxxx 10xxxxxx 10xxxxxx |
| U+10000 – U+10FFFF | 4 字节 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
看着有点复杂?别急,我们拆开来看。
规则解读
UTF-8 的设计其实非常直觉:
规则一:以 0 开头的是单字节字符
0xxxxxxx——第一个 bit 是 0,剩下 7 个 bit 用来存储数据。这 7 个 bit 刚好就是 ASCII 的 0–127。所以 UTF-8 完全兼容 ASCII——一个纯 ASCII 文件就是合法的 UTF-8 文件,不需要任何转换。
规则二:以 10 开头的是续字节
10xxxxxx——这种字节永远不会出现在第一个位置,它只能跟在某个起始字节后面,作为多字节序列的一部分。
规则三:以 110、1110、11110 开头的是起始字节
开头连续 1 的数量表示这个字符总共占几个字节:
110xxxxx→ 2 字节序列的开头1110xxxx→ 3 字节序列的开头11110xxx→ 4 字节序列的开头
这个设计有一个非常聪明的地方:你可以从数据流的任何一个字节开始,迅速判断它的角色——
- 以
0开头?这是一个独立的 ASCII 字符 - 以
10开头?这是一个续字节,往回找起始字节 - 以
11开头?这是一个多字节序列的起始字节
这意味着即使数据流中间损坏了一部分,你也能快速重新同步,不会像 GBK 那样"错一个字节,后面的全乱"。
实际编码演示
让我们用几个例子来走一遍编码过程:
例 1:字母 A(U+0041)
码位 65(十进制),落在 U+0000 – U+007F 范围内,用 1 字节:
码位:U+0041 = 0000000 1000001 (实际 7 bit: 1000001)
UTF-8:0xxxxxxx → 01000001
结果:01000001——和 ASCII 完全一样。
例 2:汉字"中"(U+4E2D)
码位 20013(十进制),落在 U+0800 – U+FFFF 范围内,用 3 字节:
码位:U+4E2D = 0100 111000 101101 (16 bit)
模板:1110xxxx 10xxxxxx 10xxxxxx
填入:11100100 10111000 10101101
[0100] [111000] [101101]
结果:E4 B8 AD(十六进制)——三个字节。
例 3:emoji 😂(U+1F602)
码位 128514(十进制),落在 U+10000 – U+10FFFF 范围内,用 4 字节:
码位:U+1F602 = 000 011111 011000 000010 (21 bit)
模板:11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
填入:11110000 10011111 10011000 10000010
[000] [011111] [011000] [000010]
结果:F0 9F 98 82——四个字节。
所以一个 emoji 占用的空间是英文字母的 4 倍。这就是为什么一条全 emoji 的推文比纯文字推文更长(从字节数来说)。
为什么 UTF-8 赢了?
在 UTF-8 之外,还有 UTF-16 和 UTF-32 两种 Unicode 编码方案。它们之间的对比很能说明问题:
| UTF-8 | UTF-16 | UTF-32 | |
|---|---|---|---|
| 编码方式 | 变长(1–4 字节) | 变长(2 或 4 字节) | 定长(4 字节) |
| ASCII 兼容 | ✅ 完全兼容 | ❌ 不兼容 | ❌ 不兼容 |
| 英文空间 | 1 字节/字符 | 2 字节/字符 | 4 字节/字符 |
| 中文空间 | 3 字节/字符 | 2 字节/字符 | 4 字节/字符 |
| 随机访问 | ❌ 需要遍历 | ⚠️ BMP 内可以 | ✅ 直接定位 |
UTF-8 胜出的原因可以总结为三点:
1. 完全兼容 ASCII——这是最关键的一点。互联网上已经有海量的 ASCII 内容,UTF-8 让这些内容无需任何转换就能继续使用。现有的 C 代码里 strcmp()、strlen() 这些函数处理 UTF-8 编码的英文文本时行为完全正确。
2. 空间高效——对于以英文为主的内容,UTF-8 比 UTF-16 和 UTF-32 节省了大量空间。即使是混合语言的内容,UTF-8 的总体空间效率也通常优于 UTF-16。
3. 容错性强——由于起始字节和续字节的格式不同,UTF-8 可以从损坏的数据流中快速恢复同步。而 UTF-16 由于所有字节看起来都一样(都是成对出现的),一旦错位就很难恢复。
UTF-16 并非没有用处——Java、JavaScript、Windows 内部都使用 UTF-16 来表示字符串(这是一个历史遗留的选择)。但在文件存储和网络传输领域,UTF-8 已经是绝对的霸主。
一个小插曲:BOM
在实际使用 UTF-8 的过程中,你可能遇到过一个问题:文件开头莫名其妙多出了 EF BB BF 三个字节。这就是 BOM(Byte Order Mark)。
BOM 本来是为 UTF-16 设计的。UTF-16 用两个字节表示一个码位,这就有一个问题:先存高位字节还是先存低位字节(大端序还是小端序)?BOM(U+FEFF)放在文件开头,读取时检查它的字节顺序,就能判断整个文件用的是什么字节序。
但 UTF-8 是单字节序列,根本不存在字节序的问题。然而有些软件(比如 Windows 记事本)仍然习惯在 UTF-8 文件开头加上 BOM(EF BB BF),用来标识"这是一个 UTF-8 文件"。这个做法虽然无害,但经常导致一些奇怪的 bug——比如 PHP 文件开头的 BOM 会导致 header() 函数报"headers already sent"错误,因为 PHP 把 BOM 当成了输出内容的一部分。
最佳实践:UTF-8 文件不需要 BOM,不要加。
从 ASCII 的 128 个字符,到 Unicode 的 110 万个码位,再到 UTF-8 的优雅编码——人类终于在"怎么用 0 和 1 表示文字"这个问题上达成了一个近乎完美的方案。
但故事还没有结束。在 2020 年代的今天,有一种新的"编码"正在深刻地改变我们与文字的关系——LLM 的 Tokenizer。
回到当下:LLM 的 Tokenizer
当你向 ChatGPT 输入一句话时,它并不是逐个字符来理解你的输入的。在文字进入模型之前,有一道关键的工序——分词(Tokenization)。
这道工序把文本切分成一个个 token,然后把每个 token 映射成一个整数编号。这个过程,本质上也是一种"编码"。
Token 不是字符
让我们看一个具体的例子。假设你输入:
I love encoding.
使用 GPT-4 的 tokenizer(cl100k_base),它会被切分成:
I | love | enc | oding | .
对应的 token ID 是:
| Token | ID |
|---|---|
| I | 40 |
| love | 7944 |
| enc | 29688 |
| oding | 296 |
| . | 13 |
注意到了吗?encoding 这个词被拆成了 enc 和 oding 两个 token,而不是逐字母拆分,也不是作为一个整体保留。这不是随机的,而是 tokenizer 根据训练语料中的统计规律做出的选择。
再看一个中文的例子:
编码很有趣
可能会被切分成:
编 | 码 | 很 | 有趣
有时候一个汉字就是一个 token,有时候两个汉字被合在一起——取决于这两个字在训练语料中经常一起出现的频率。
这就是 token 和字符的本质区别:字符是人为定义的书写单位,token 是从数据中自动学到的统计单位。
BPE:怎么把文本切成 token?
目前主流的 tokenizer 算法是 BPE(Byte Pair Encoding,字节对编码),它的思路出奇地简单:
- 从字符级别开始:先把所有文本拆成单个字符(实际上是 UTF-8 的字节)。
- 统计频率:找出所有相邻的字节对(pair),看哪个出现得最多。
- 合并最高频的字节对:把它变成一个新的 token,加入词表。
- 重复步骤 2–3:直到词表大小达到预设的上限(比如 100,000 个 token)。
用一个简化例子来说明。假设训练语料中有这些词:
low lower lowest
第一步,按字符拆分:
l o w l o w e r l o w e s t
统计相邻字符对,发现 l + o 出现了 3 次(最多),合并成 lo:
lo w lo w e r lo w e s t
继续统计,lo + w 出现了 3 次,合并成 low:
low low e r low e s t
再继续,e + r 出现 1 次,e + s 出现 1 次……以此类推。
最终,常见的组合(如 low、er、est)会被合并成一个 token,而罕见组合则保持字符级别的拆分。
和 UTF-8 相似的哲学
如果你仔细想一想,BPE 和 UTF-8 有着惊人的相似之处:
| UTF-8 | BPE Tokenizer | |
|---|---|---|
| 编码单位 | 字节 | Token |
| 编码方式 | 变长 | 变长 |
| 高频单元 | ASCII 字符占 1 字节 | 常见词/词根占 1 个 token |
| 低频单元 | 罕见字符占 3–4 字节 | 罕见词被拆成多个 token |
| 设计目标 | 节省存储空间 | 节省模型计算量 |
两者都是变长编码,核心哲学一致:高频的用短码,低频的用长码。 这是信息论中的基本原理——Shannon 在 1948 年就证明了,这是编码的最优策略。
Tokenizer 的实际影响
Tokenizer 不只是一个技术细节,它直接影响了 LLM 的行为和成本:
1. “Token 计费”
OpenAI 的 API 按 token 数量计费。同一个意思,用英文表达和用中文表达,token 数量可能差很多。一般来说,中文文本消耗的 token 数量是英文的 2–3 倍——因为中文的常见词组合在 BPE 词表中的覆盖率不如英文。这意味着同样的预算,用中文能处理的内容比英文少得多。
2. 拼写能力差
你有没有注意到 ChatGPT 有时候数不清一个单词有几个字母?比如你让它输出 “strawberry” 中有多少个 r,它可能会答错。
这不是模型"笨",而是 tokenizer 的副作用。在 BPE 中,strawberry 可能是一个完整的 token——模型看到的是一个 ID,而不是 s-t-r-a-w-b-e-r-r-y 这些独立的字母。就像你看到"苹"这个字时,你不会意识到它是"艹"和"平"组成的一样——你是整体识别的,不是逐笔画的。
3. 代码能力
BPE tokenizer 在处理代码时表现不错,因为编程语言的关键字和常见模式(如 function、return、=>)频率很高,会被合并成单独的 token。这也是为什么 LLM 对代码的理解通常比对自然语言更"精准"的一个原因。
4. 多语言不公平
由于 BPE 是基于训练语料的统计频率,英语训练数据最多的语言天然占优势——英语的常见词几乎都有专属 token,而一些小语种的常见词可能被拆成好几个 token。这意味着模型处理不同语言时的效率和精度存在系统性差异。
一个新的编码层
从某种意义上说,Tokenizer 是在 UTF-8 之上又加了一层编码:
人类文字 → Unicode 码位 → UTF-8 字节 → BPE Token → Token ID
每一层都在解决上一层的不足:
- Unicode 解决了"每个字符有一个唯一的编号"的问题
- UTF-8 解决了"怎么高效地存储这些编号"的问题
- BPE Tokenizer 解决了"怎么让神经网络高效地处理这些字符"的问题
而贯穿所有这些层的,是同一个设计哲学:用尽可能紧凑的方式表示信息,让高频的常用单元更短,低频的罕见单元更长。
这正是 Shannon 在 1948 年告诉我们的事情。
我们从计算机只认识 0 和 1 出发,经过 ASCII 的 128 个字符、本地编码的巴别塔、Unicode 的统一码位、UTF-8 的优雅变长编码,最终来到了 LLM 的 Tokenizer——又一层新的编码抽象。
每一次演进,都是在回答同一个问题:怎么用有限的符号去承载无限的意义?
香农说,通信的根本问题是忠实地在另一端复现消息。六十多年来,我们发明了一个又一个编码方案来逼近这个目标。而 Tokenizer 告诉我们,这个旅程可能还远没有结束。
下一次当你打出一个 emoji 😂 的时候,也许可以想一想:这几个简单的像素背后,经历了怎样一段漫长的编码之旅。