ARTICLE · 1309212
每天学一点AI知识|DAY 58 - Unicode、字符、字节与 Token
每天学一点AI知识|DAY 58 - Unicode、字符、字节与 Token
我回来啦!!!总算有时间继续更新了,这段时间真的是忙昏头了,不过我忏悔,确实拿了这么一点点时间打了下游戏,耽误了学习🤦,那我们接着来吧 字符、Unicode code point、UTF-8 字节和 Token,是同一段文本在不同系统中的四种表示。它们分别回答“读者看到什么”“字符身份是什么”“怎样保存和传输”以及“模型怎样读取”,所以四种长度没有固定换算关系。
这个问题看似简单,却可能得到四个不同答案。读者会数看见了几个符号;程序可能计算 Unicode code point 的数量;文件系统关心编码后占用多少字节;大语言模型则关心 Tokenizer 最终产生多少 Token。
如果把这些层混在一起,就会遇到一些非常典型的困惑:明明只有三个字符,为什么文件里占了八个字节?明明文字没有变化,为什么换一个模型以后 Token 数变了?为什么文件还没送进模型就已经乱码?为什么 Token ID 很大,却不能说明它表示的概念更复杂?
今天我们用一段只有三个简单符号的文本 A你🙂,把文本从读者眼前一直追踪到 Transformer 的输入端。
核心问题
同一段文字为什么会同时有用户感知的符号数、Unicode code point 数、字节数和 Token 数?Unicode 与 UTF-8 分别负责什么?Tokenizer 在文本正确解码后又做了什么?发生乱码、Token 数异常或模型输入错位时,怎样判断问题属于哪一层?
一句话结论
字符是读者理解文本时使用的抽象单位;Unicode 为字符分配统一编号,这个编号叫 code point;UTF-8 等编码把 code point 转换成字节序列,以便保存和传输;Tokenizer 再根据具体模型的词表和切分规则,把已经正确解码的字符串变成 Token 与 Token ID,Embedding 随后把 ID 映射成向量。
一个可见符号可能由多个 code point 组成,一个 code point 在 UTF-8 中可能占用多个字节,一个 Token 又可能覆盖一个词、词的一部分、一个字符、标点或字节片段。因此,字符数、code point 数、字节数和 Token 数属于不同层次,不能直接互相推算。

一、先看同一段文本的四层表示
从读者角度看,短文本 A你🙂 包含三个可见符号:一个拉丁字母、一个汉字和一个 emoji。但同一段文本进入不同系统时,会出现不同表示:
A你🙂 | ||
|---|---|---|
A、 | ||
U+0041、 | ||
41、 | ||

四层都在描述同一段文本,却没有互相替代:
字符层 → 面向读者和文字结构Unicode 层 → 确定字符的标准身份编码层 → 确定存储和传输字节Tokenizer 层 → 确定模型读取的离散单位
在这个特意选择的简单例子里,三个可见符号恰好对应三个 code point;编码为 UTF-8 后却占 8 个字节;Token 数则必须交给某个明确的 Tokenizer 实际计算。
二、“字符”为什么不是永远明确的计数单位
日常交流中,我们通常把读者看到的一个字母、汉字或 emoji 称为一个字符。对于 A你🙂,说它有三个字符完全自然。
但一旦程序需要精确计数,就必须先说明采用什么规则。至少有两个常被口语统称为“字符”的对象:
Unicode code point: 字符标准中的一个编号; 用户感知的符号: 读者在屏幕上通常看成一个整体的图形。
二者在简单文字中经常一一对应,在组合字符中却可能分开。
看起来一样的 é,内部可以不同
带重音的 é 可以直接使用一个预组合 code point:
é → U+00E9也可以使用普通字母 e 加一个组合重音符号:
e → U+0065重音符号 → U+0301
两种写法在许多字体中看起来几乎一样,但前者是一个 code point,后者是两个。它们编码后的字节也不同,Tokenizer 看到的底层序列还可能不同。
看起来一个家庭 emoji,内部有多个 code point
家庭 emoji 👨👩👧👦 通常显示成一个整体图形,内部却由“男人、女人、女孩、男孩”等多个 emoji code point,通过零宽连接符(Zero Width Joiner,ZWJ)连接而成。
连接符本身通常不单独显示,却参与决定最终图形。因此,屏幕上看见一个符号,不代表字符串内部一定只有一个 code point。

这就是为什么“字符数”必须带上计数规则。面向用户的输入框可能希望按用户感知的符号计数;程序底层 API 可能返回 code point 数;另一种编程语言还可能使用自己的字符串存储单位。只说“长度是 5”而不说明长度在哪一层,信息并不完整。
三、Unicode 解决的是“它是谁”
如果不同计算机系统各自为文字编号,同一个汉字可能在不同设备上对应不同数字,文本交换就会变得混乱。
Unicode 提供一套跨系统的统一字符标准,为世界各地的文字、标点和大量符号分配编号。这个编号叫code point,常写成 U+ 加十六进制数字:
A → U+0041你 → U+4F60🙂 → U+1F642
可以把 code point 理解成字符在统一名册中的登记号。它回答的是“这个抽象字符是谁”,而不是“文件里具体写入了哪些 byte”。
U+4F60 也不是模型词表里的 Token ID。Unicode code point 来自跨系统字符标准;Token ID 则来自某个具体模型使用的词表。两者恰好都是数字编号,却属于完全不同的编号系统。
四、UTF-8 解决的是“怎样把它写成字节”
计算机文件和网络传输最终处理的是 bit 与 byte。一个 byte 包含 8 个 bit,通常可以表示 0 到 255 之间的一个数。
Unicode code point 仍然需要经过某种字符编码(Character Encoding),才能变成实际字节。UTF-8 是今天最常见的 Unicode 编码方式之一。
Unicode:字符身份与统一编号UTF-8: code point 到字节序列的编码规则

“Unicode 文件”这种说法有时只是泛指文件包含 Unicode 文本,却没有完整说明字节采用 UTF-8、UTF-16 还是其他编码。真正读写文件时,编码规则仍然必须明确。
同一个 code point 也可能用不同 Unicode 编码方式变成不同字节序列。字符身份不变,落到存储介质上的排列方式却可以不同。Unicode 与 UTF-8 关系紧密,但绝不是同一个概念。
五、为什么三个简单字符会占八个 UTF-8 字节
UTF-8 使用变长编码,不是每个字符都固定占相同字节数:
A : U+0041 → 41 → 1 byte你 : U+4F60 → E4 BD A0 → 3 bytes🙂 : U+1F642 → F0 9F 99 82 → 4 bytes
因此:
1 + 3 + 4 = 8 bytes
这里用两位十六进制数字表示一个 byte。例如 E4 是一个 byte,BD 是第二个,A0 是第三个。你 没有因此变成三个字符,只是它的 Unicode code point 按 UTF-8 规则需要三个 byte 才能表示。
英文 ASCII 范围内的字符在 UTF-8 中通常只需一个 byte,常见汉字通常需要三个,许多 emoji 需要四个。但这只是常见情况,不能反过来用“字节数除以三”估计中文字符数,更不能从 UTF-8 字节数直接推断 Token 数。
六、用 Python 同时观察 code point 与字节
下面的代码只检查字符串表示,不涉及模型训练:
text = ”A你🙂”print(len(text))print([f”U+{ord(ch):04X}” for ch in text])utf8_bytes = text.encode(”utf-8”)print(len(utf8_bytes))print(utf8_bytes.hex(” ”))print(utf8_bytes.decode(”utf-8”) == text)
在常见 Python 3 环境中,关键输出是:
3['U+0041', 'U+4F60', 'U+1F642']841 e4 bd a0 f0 9f 99 82True
逐行来看:
len(text) 返回 Python 字符串中的 code point 数;这个简单例子得到 3;ord(ch) 取出单个字符对应的 Unicode code point;.encode("utf-8") 把字符串编码成 UTF-8 字节;len(utf8_bytes) 计算 byte 数,因此得到 8;.hex(" ") 用十六进制展示每个 byte;.decode("utf-8") 按 UTF-8 把字节还原为字符串。
最后的 True 表示“按 UTF-8 编码,再按 UTF-8 解码”恢复了原字符串。
如果把 text 换成组合重音或家庭 emoji,len(text) 计算的是 code point 数,不一定等于读者看到的整体符号数。这也说明:程序返回的字符串长度必须结合该语言和 API 的定义理解。
七、编码和解码为什么必须使用匹配规则
编码是从字符串到字节,解码则从字节恢复字符串:
Unicode 字符串→ 使用 UTF-8 编码→ 得到一串 bytes→ 使用 UTF-8 解码→ 恢复 Unicode 字符串
如果写入时使用 UTF-8,读取时却把相同字节当成另一种编码,程序就会给这些 byte 分配错误的字符身份。可能出现三种结果:
解码器直接报错; 无法解释的位置被替换为 �一类替换字符;字节碰巧能按错误编码解释,却变成一串看似有字符、实际无意义的乱码。

乱码发生在编码与解码层,此时文本甚至还没有进入 Tokenizer。直接更改模型参数、Embedding 或提示词,无法修复上游已经被错误解码的字符串。
正确的排查问题是:
原始字节是什么?写入或传输时使用什么编码?读取时按什么编码解码?错误解码是否已经丢失原始信息?
如果原始 byte 仍在,使用正确编码重新解码通常还有机会恢复;如果乱码文本已经被再次保存,部分信息可能已经不可逆地丢失。
八、Tokenizer 在正确解码的字符串上工作
文件与网络负责把 byte 正确恢复成 Unicode 字符串。接下来,模型才开始处理文本。
Day 06 已经介绍过 Tokenizer 的基本职责:根据模型的词表和切分规则,把文本拆成 Token,再将每个 Token 映射成整数 ID。
Token 可能是:
一个完整单词; 一个词的一部分; 一个汉字或其他字符; 空格或标点; 一段 byte 或其他短片段。

因此,不能把 Token 固定翻译成“词”,也不能认定一个汉字永远对应一个 Token。某个常见片段可能在词表中拥有一个完整条目;罕见片段则可能被拆成多个 Token。不同 Tokenizer 面对同一段文本,还可能选择完全不同的边界。
下面只是一种抽象示意,不代表某个具体模型的真实输出:
同一单词Tokenizer A → [un] [believable] [!]Tokenizer B → [un] [believe] [able] [!]Tokenizer C → [u] [n] [b] [e] [...] [!]
要知道真实 Token 数和切分位置,必须使用目标模型配套的 Tokenizer 实际计算。
九、Unicode code point 与 Token ID 都是编号,却不是一回事
Unicode code point 是字符在 Unicode 标准中的身份编号:
你 → U+4F60Token ID 是某个 Token 在特定模型词表中的索引:
某个 Token → 词表中的第 12345 项这里的 12345 只是示意,不代表任何具体模型中“你”的真实 ID。

两种编号有四个重要区别:
Token ID 的数值大小没有“语义更复杂”“更罕见”或“与另一个 ID 更接近”的固定含义。ID 只是数组或词表的索引。模型通过 Embedding 查表,把离散 ID 映射成向量,语义关系主要体现在学习到的向量和后续计算中,而不是 ID 数字本身。
十、文本进入模型的完整主链路
文本进入模型前,可以拆成两条相连的路径。
第一条负责存储与传输:
Unicode 字符串→ UTF-8 编码→ bytes 写入文件或通过网络传输→ UTF-8 解码→ 恢复 Unicode 字符串
第二条负责模型输入:
正确解码的 Unicode 字符串→ Tokenizer 切分→ Token→ Token ID→ Embedding 向量→ Transformer 继续计算

这两条路径不能跳着理解。模型接口收到字符串时,通常已经完成字节解码;Tokenizer 的任务不是替文件系统猜测错误编码。UTF-8 byte 与 Token ID 也不是前后直接对应的一组数字:中间还有解码、规范化或模型特定的预处理与切分规则。
每一层回答的问题不同:
字符与 Unicode: 这是什么符号,怎样跨系统统一识别? 编码与 byte: 怎样可靠保存和传输这些符号? Tokenizer: 目标模型怎样把字符串切成离散单位? Token ID: 每个离散单位在词表中的索引是什么? Embedding: 怎样把索引变成后续网络能够计算的向量?
十一、视觉相同的文本为什么仍可能产生不同 Token
前面提到,预组合的 é 和 e 加组合重音符号可能显示成同一个图形,但 code point 序列与 UTF-8 byte 序列不同。
某些文本系统会进行 Unicode 规范化,把可以等价表示的序列转换到统一形式;某些 Tokenizer 也包含自己的规范化或预处理步骤;另一些流程则可能保留原始序列。
因此:
屏幕上看起来相同不一定代表 code point 序列相同不一定代表 UTF-8 bytes 相同也不一定代表 Token 序列相同
这在数据去重、字符串比较、用户名校验和 Token 计费中都可能产生影响。可靠做法不是假设“看起来一样就一定完全相同”,而是明确系统采用的规范化、编码与 Tokenizer 规则。
十二、为什么不能从字节数推断 Token 数
UTF-8 字节数由字符及其编码决定;Token 数则由模型词表、切分算法和可能的预处理决定。二者之间没有一个对所有语言、文本和模型都成立的固定比例。
相同 byte 数的两段文本可能产生不同 Token 数,因为一段包含词表中的常见组合,另一段可能被拆成许多小片段。相同文本换一个 Tokenizer,byte 完全没变,Token 边界和数量却可能改变。
因此,下面这些任务都应该调用目标模型的真实 Tokenizer:
估算输入占用多少上下文; 判断文本将在什么位置被截断; 估算按 Token 计算的推理成本; 预留输入与输出 Token 空间; 检查模板、空格、换行和特殊符号带来的额外占用。
经验换算可以用于非常粗略的容量预估,不能替代上线前的实际计数。

十三、为什么 Tokenizer 必须与模型匹配
模型训练时,Embedding 表中的第 100 行、第 101 行等向量,都是围绕训练所使用的 Token ID 含义学出来的。
假设训练时的 Tokenizer 把“你好”转换成:
[ID 7, ID 19]模型于是学会把 Embedding 表中的第 7 行和第 19 行当作对应输入。后来换用另一个 Tokenizer,即使表面仍然输出“你好”,也可能得到:
[ID 7, ID 9]新的第 9 行并不承担训练时第 19 行的含义。模型实际收到的离散输入已经改变。

这就是为什么模型文件、词表、Tokenizer 配置和特殊 Token 规则应当作为一个整体匹配。不能只因为两个 Tokenizer 都能处理中文,或者词表大小相同,就随意互换。
上下文长度、成本和截断位置也应使用目标模型的 Tokenizer 计算,因为这些结果依赖真实的 Token 序列,而不是读者看到的字符数。
十四、遇到问题时先判断发生在哪一层
文件打开后已经乱码
优先检查写入编码、传输过程中是否改变 byte,以及读取时使用的解码规则。此时通常还没进入 Tokenizer。
字符串比较结果与视觉感受不同
检查 code point 序列和 Unicode 规范化。两个字符串可能显示相似,内部组成却不同。
Token 数、成本或截断位置异常
确认调用的是目标模型配套的 Tokenizer,并直接查看 Token 数与切分结果。不要用 UTF-8 byte 数或字符数替代。
模型输出完全不合理
除了检查提示内容,还要确认 Tokenizer、词表和特殊 Token 规则是否与模型训练时一致。文字表面相同,不代表输入 ID 序列相同。
解码后出现替换字符
检查原始 byte 是否仍然可用,以及解码阶段是否已经用替换字符吞掉了错误。替换以后再保存,可能让原始 byte 信息丢失。
一个实用的排错顺序是:
先确认原始 bytes→ 确认编码和解码匹配→ 检查 Unicode 字符串与 code point→ 再使用目标 Tokenizer 检查 Token 和 ID→ 最后检查 Embedding 与模型行为
十五、字符限制、字节限制与 Token 限制不能混用
真实产品经常同时出现三种限制:
输入框最多允许多少个用户感知字符; 数据库字段或网络请求最多允许多少 byte; 模型上下文最多允许多少 Token。
它们保护的资源不同。
如果产品要求用户名最多 20 个“可见符号”,直接限制 UTF-8 为 20 bytes 会对英文、汉字和 emoji 产生完全不同的实际长度。如果数据库最多允许 100 bytes,只检查 Python len(text) <= 100 又可能让多字节文本超过存储上限。如果模型只剩 100 个 Token 上下文,只检查 100 个字符同样不可靠。
正确做法是分别在对应层计数:
用户界面规则 → 按明确的字符或用户感知符号规则存储传输规则 → 按指定编码后的 byte 数模型上下文规则 → 按目标模型 Tokenizer 的 Token 数
“文本长度”不是一个单一属性,而是相对于某种表示和资源限制定义的量。
十六、常见误解

Unicode 就是 UTF-8 吗? 不是。Unicode 规定字符身份与 code point,UTF-8 规定这些 code point 怎样编码成 byte。 一个汉字就是一个 byte 吗? 不是。常见汉字在 UTF-8 中通常占 3 bytes。 一个可见字符一定对应一个 code point 吗? 不一定。组合重音和 ZWJ emoji 序列可能由多个 code point 共同显示成一个符号。 Python 的 len(text)永远等于用户看到的字符数吗? 不一定。它在 Python 3 字符串中计算 code point 数,组合符号可能让两种计数不同。一个 Token 就是一个字、一个词或一个字符吗? 都不一定。Token 边界由具体 Tokenizer 决定。 Token ID 越大,语义就越复杂吗? 不是。Token ID 只是特定词表中的索引。 两个 Token ID 相邻,语义就相近吗? 不一定。语义关系不由 ID 的数值距离定义。 UTF-8 字节数相同,Token 数就相同吗? 不一定。Token 数还取决于词表、切分规则和预处理。 文件乱码可以通过换 Tokenizer 修复吗? 通常不能。应先回到编码与解码层检查。 两个 Tokenizer 词表大小一样,就可以互换吗? 不可以。相同 ID 在不同词表中可能指向完全不同的 Token。
十七、本日总结
同一段文本可以同时拥有用户感知符号数、code point 数、byte 数和 Token 数。 字符是面向读者的抽象单位,但精确计数前必须说明采用什么规则。 一个可见符号可能由多个 code point 组成,视觉相同的文本也可能拥有不同 code point 序列。 Unicode 为抽象字符分配统一 code point,解决跨系统身份识别问题。 UTF-8 把 code point 编码成变长 byte 序列,服务于文件存储与网络传输。 A你🙂 在简单计数下有 3 个字符和 3 个 code point,编码为 UTF-8 后占 8 bytes。编码与解码规则不匹配会产生报错、替换字符或乱码,这时文本还没有进入 Tokenizer。 Tokenizer 在正确解码的字符串上,按具体模型的词表和规则产生 Token 与 Token ID。 Token 可能是词、词的一部分、字符、标点或 byte 片段,数量必须使用目标 Tokenizer 实际计算。 Unicode code point 是字符标准编号,Token ID 是模型词表索引,二者不能混用。 Token ID 的数值大小或距离不直接表示语义复杂度与语义距离。 Tokenizer 必须与模型训练时使用的词表和规则匹配,否则相同文字会变成不同离散输入。 排查文本问题时,应按 bytes、编码解码、Unicode 字符串、Tokenizer、模型输入的顺序定位层次。 用户字符限制、存储 byte 限制和模型 Token 限制应分别计算,不能互相替代。
自检
对Python3字符串 text="A你🙂",len(text)和len(text.encode("utf-8"))分别是多少?这两个结果各自在数什么?x = "\u00e9"用一个预组合码点表示 é,y = "e\u0301"\用字母加组合重音表示它。两者通常都显示为一个用户感知的符号。len(x)与 len(y)分别是多少?这个例子说明,能否直接用 len(text)统计用户看到的符号数?一份文件写入时使用 UTF-8,读取后却显示乱码。你会先检查哪一步?为什么此时更换 Tokenizer 通常解决不了问题? 给已训练好的模型换一个词表大小相同、但 Token 与 ID 对应关系不同的 Tokenizer,会有什么问题?为什么词表大小相同仍不足以保证二者匹配? 产品分别要求“最多 10 个用户感知的符号”“文本编码后不超过 100 字节”“模型输入不超过 100 Token”,应该分别怎样计数?最后一项为什么要使用目标模型的 Tokenizer,而不能直接根据前两项换算?















































































