“通信的根本问题是:在一点上精确或近似地复现在另一点上所选取的消息。”

— Claude Shannon, A Mathematical Theory of Communication (1948)

为什么需要编码?

你正在屏幕上阅读这篇文章。这些汉字、标点、空格——它们是怎么出现在屏幕上的?

这是一个细想就会觉得不可思议的事情:计算机根本不认识"字"。在计算机的世界里,没有横竖撇捺,没有 ABCD,只有两样东西——0 和 1。每一块内存、每一块硬盘、每一根网线里流淌的,都是密密麻麻的电信号:高电平是 1,低电平是 0。

但我们每天用计算机做的事情——写文档、发消息、刷网页——全都离不开文字和符号。那么问题来了:

怎么把人类的符号,塞进只有 0 和 1 的世界里?

答案就是:编码。

从符号到数字,从数字到二进制

编码做的事情,可以拆成两步:

  1. 给每个符号编一个号码:比如 A 是 65,B 是 66,“中” 是 20154。这一步建立了符号到数字的映射。
  2. 把数字转成二进制: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,字节对编码),它的思路出奇地简单:

  1. 从字符级别开始:先把所有文本拆成单个字符(实际上是 UTF-8 的字节)。
  2. 统计频率:找出所有相邻的字节对(pair),看哪个出现得最多。
  3. 合并最高频的字节对:把它变成一个新的 token,加入词表。
  4. 重复步骤 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 😂 的时候,也许可以想一想:这几个简单的像素背后,经历了怎样一段漫长的编码之旅。