summaryrefslogtreecommitdiff
path: root/content/article
diff options
context:
space:
mode:
authoreltrac <hi@eltr.ac>2026-09-07 23:41:43 +0800
committereltrac <hi@eltr.ac>2026-09-07 23:41:43 +0800
commit75f2833d128f76af9cda59fd67b59a2272c4618e (patch)
tree25fd6b0c11bdc21fc6d48976c4cc928b80c067ac /content/article
parent9612caade52fb028e1ab5079c5a555f0571bbc9e (diff)
[publish] /article/token-translation
Diffstat (limited to 'content/article')
-rw-r--r--content/article/token-translation.md219
1 files changed, 203 insertions, 16 deletions
diff --git a/content/article/token-translation.md b/content/article/token-translation.md
index 30a14937..b8429d99 100644
--- a/content/article/token-translation.md
+++ b/content/article/token-translation.md
@@ -1,35 +1,39 @@
+++
title = 'Token 应译作「符」'
-subtitle = ''
-slug = ''
-tags = []
-categories = ''
-date = 2026-09-07T08:25:45+08:00
-draft = true
+slug = 'token-translation'
+tags = ["语言","Lisp","编程"]
+categories = '软件杂谈'
+date = 2026-09-07 23:37:10
+draft = false
toc = true
+fedi = 'https://akk.eltr.ac/objects/871e9998-56c8-4222-af8b-33c83d76b174'
+++
-整场「要不要把 Token 翻译成词元?」的争论令我感到诡异,诡异的点在于所有参与者都只看到了 Token 作为大语言模型计价单位的意思,而 Token 一词存在于英语语言中已经有很久了。若把 Token 先前的释义也译作「词元」,场面会非常可笑:
+整场「要不要把 Token 翻译成词元?」的争论令我感到诡异,诡异的点在于所有参与者都只看到了 Token 作为大语言模型计价单位的意思,而 Token 一词存在于英语语言中已经有很久了(对中文的影响也很大)。若把 Token 先前的释义也译作「词元」,场面会非常可笑:
- 这套桌游里的某个词元不见了!
- 请求体里必须携带词元才能通过验证。
- 在构造抽象语法树之前,要先把字符串词元化。(这倒还算接近的)
-- 把你安全词元插入电脑的 USB-C 接口。
+- 把安全词元插入电脑的 USB-C 接口。
- 为保证数据安全,可以把敏感信息词元化。
- 用你的词元兑换优惠或者奖品。
<!--more-->
-正如人们滥用「人工智能」一词指代「大语言模型」,因为他们只知道热度最高的技术,便让 “AI” 这个上世纪就出现的概念被人工智能的一个小分支占据一样,人们滥用 Token 一词,以至于他们不关心其他的意思。
+正如人们滥用「人工智能」一词指代「大语言模型」,因为他们只知道热度最高的技术,便让 AI 这个上世纪就出现的概念被人工智能的一个小分支占据一样,人们滥用 Token 一词,以至于他们不关心其他的意思。更何况「词元」这个意思用在 LLM 里其实相当不准确。
不过,也需要批评前人,Token 一词的翻译本身就很混乱。
-桌游里的 Token 是「指示物」的意思,还有人根据情况译作「代币」「标记」。除了 Token,用来表示人物、角色的指示物还被称作 *Meeple*(My People),音译作「米宝」。能有米宝来指代所有的指示物吗?当然不行!米宝的本意是人物,是 Token 里相当小的一类。同理,能用「词元」对应 Token 吗?不能!词元只指代相当小的范畴,你当然不能说桌游里的指示物是词元。
+## 桌游中的 Token
+
+桌游里的 Token 是「指示物」的意思,还有人根据情况译作「代币」「标记」。大富翁里用来标记地产的房子,飞行棋里的飞机,龙与地下城里的角色小人,都叫作 Token。
+
+用来表示人物、角色的指示物还被称作 *Meeple*(My People),音译作「米宝」。能用米宝来指代所有的指示物吗?当然不行!米宝的本意是人物,是 Token 里相当小的一类。同理,能用「词元」对应 Token 吗?不能!词元只指代相当小的范畴,你当然不能说桌游里的指示物是词元。
不仅桌游里的代币是 Token,游戏厅里的游戏机和赌场里的老虎机使用的代币,也叫作 Token coin,不过如今更多地被 *ticket*(票券)取代了。
-在计算机中,Token 有三类。一类是用来表示「执行某种操作的权限」的对象,另一类是编译器和解释器中划分的语法单元,还有一类是自然语言处理(NLP)中划分的词法记号(Lexical token)或别的语言单元——在讲第三类的时候,我会讲到「词元」这个翻译不仅犯了范畴问题,本身的表意也不准确。
+## 密码学中的 Token
-第一类 Token 通常叫作「令牌」,往往与密码学相关。常见的场景是客户端输入账号密码之后,服务端生成一个(多半有期限)的凭证,一般是字符串,之后客户端只需要在访问时提供这个凭证就好,期限时间内不需要再次输入密码。这个字符串一般保存在 Cookies 里,服务端自动读取,或者客户端携带在 HTTP 请求头里:
+密码学中的 Token 通常叫作「令牌」。常见的场景是客户端输入账号密码之后,服务端生成一个(多半有期限)的凭证,一般是字符串,之后客户端只需要在访问时提供这个凭证就好,期限时间内不需要再次输入密码。这个字符串一般保存在 Cookies 里,服务端自动读取,或者客户端携带在 HTTP 请求头里:
```
Authorization: Bearer eyJhbGci...<snip>...yu5CSpyHI
@@ -37,7 +41,13 @@ Authorization: Bearer eyJhbGci...<snip>...yu5CSpyHI
`Bearer` 之后的部分就是 Token。最常见的令牌之一是 [JWT](https://en.wikipedia.org/wiki/JSON_Web_Token),其他的应用场景也不少,不限于 HTTP 通信。任何能够在通信过程中,从密码学意义上验证用户身份的凭证字符串,都叫令牌。
-第二类 Token 在解析代码的过程中产生,解析(parse)还没有到编译(compile)或者解释(interpret)的地步,无论编译器还是解释器都有这个步骤。仔细想想,源代码只是储存在文件里的文本而已,可以当作字符串处理,解释器和编译器是怎么把字符串变成有意义、可执行的语句的?我们可以现场写个 Lisp 解释器来理解其过程。
+顺带一提,令牌可以当作是时效性很强的密钥,如果泄露,攻击者就可以在令牌过期之前使用你的身份访问相关资源。令牌往往存放在 Cookies 里,这也是为什么[第三方 Cookies](https://developer.mozilla.org/en-US/docs/Web/Privacy/Guides/Third-party_cookies#what_is_the_problem_with_third-party_cookies)(或者说跨域 Cookies)常常受人诟病,现代浏览器也采取了很多限制。
+
+还有一种密码学方法叫作令牌化([Tokenization](https://en.wikipedia.org/wiki/Tokenization_(data_security))),指代把敏感信息替换为不敏感的等价物(即 Token)的过程。我对此了解不多,就不闹笑话了。
+
+## 编译器和解释器中的 Token
+
+这类 Token 在解析代码的过程中产生,解析(parse)或者预处理(preprocess)还没有到编译(compile)或者解释(interpret)的地步,无论编译器还是解释器都有这个步骤。仔细想想,源代码只是储存在文件里的文本而已,可以当作字符串处理,解释器和编译器是怎么把字符串变成有意义、可执行的语句的?我们可以现场写个 Lisp 解析器来理解其过程。
程序员极有可能接触过简单的解释器,比方说,如何给下面这个算式字符串求值?
@@ -47,7 +57,8 @@ Authorization: Bearer eyJhbGci...<snip>...yu5CSpyHI
答案是构造抽象语法树(AST)。不难发现,上述算式可以表示为树形结构,`(1 + 3)` 是一个整体,`4 * 7` 因为乘法的优先级更高,也作为一个整体,那么就可以把 `-` 当作根,前面两个整体作为 `-` 的左右子树。继续递归,不难发现 `(1 + 3)` 是 `+` 为根,`1` 和 `3` 为叶子结点的子树,而 `4 * 7` 是 `*` 为根,`4` 和 `7` 为叶子结点的子树。
-![](https://img.eltr.ac/geedeapro/article/token-translation/math-ast.svg)
+![(1 + 3) - 4 * 7 的树状表示](https://img.eltr.ac/geedeapro/article/token-translation/math-ast.svg)
+{.dark:invert}
接下来只要深度优先遍历 AST 就好了,不难发现结点有两类,非叶子结点是操作符,叶子结点是数字。遍历时若是遇到数字,就直接返回值;若是遇到操作符,就根据操作符计算左右子树的值。在一系列奇妙的递归之后,程序就能求出这棵树的值为 `-24`。
@@ -59,7 +70,7 @@ Authorization: Bearer eyJhbGci...<snip>...yu5CSpyHI
也就是说,把中置操作符换成**前置**操作符,把所有的括号**显式**地写出来。如果把 `()` 看成列表,前置操作符的写法其实是嵌套的列表,而嵌套的列表就是树。这样一来,把字符串解析成 AST 的算法就好写多了。
-假设我们现在不仅仅是要表示算式,要表示程式……
+假设我们现在不仅仅是要表示算式,而是要表示程式……
```lisp
(format t "it's ~a~%"
@@ -68,5 +79,181 @@ Authorization: Bearer eyJhbGci...<snip>...yu5CSpyHI
上面这段代码应该打印 `it's -24`。
-![](https://img.eltr.ac/geedeapro/article/token-translation/lisp-ast.svg)
+同样,手动解析为 AST,变成下图的这个样子:
+
+![上述 Lisp 代码的树形表示](https://img.eltr.ac/geedeapro/article/token-translation/lisp-ast.svg)
+{.dark:invert}
+
+现在,非叶子结点是函数,叶子结点是函数的参数。读者可能会觉得把不同类型的数据都放在一起有点奇怪,和刚才的算式差得有点远。不过,只要想到深度优先遍历和递归求值的思路,就不会觉得奇怪了。只不过函数不仅仅是计算得出了值,还可能带有副作用。比方说 `format` 的副作用就是在指定的输出流(此处是 `stdout`)中打印文本。
+
+手动遍历这棵树:
+
+1. 求值根节点 `format`,发现是函数,于是求它的子树。
+2. 求值 `format` 的左子树,发现是值,返回 `t`。
+3. 求值 `format` 的第二棵子树,发现是值,返回 `"it's ~a~%"`
+4. 求值 `format` 的右子树 `-`,发现是函数,求它的子树。
+5. 求值 `-` 的左子树 `+`,发现是函数,求它的子树
+6. 求值 `+` 的左子树,发现是值,返回 `1`
+7. 求值 `+` 的右子树,发现是值,返回 `3`
+8. 结点 `+` 的子树都求值完毕,对它本身求值得到 `4`
+9. 求值 `-` 的右子树 `*`,发现是函数,求它的子树
+10. 求值 `*` 的左子树,发现是值,返回 `4`
+11. 求值 `*` 的右子树,发现是值,返回 `7`
+12. 结点 `*` 的子树都求值完毕,对它本身求值得到 `28`
+13. 结点 `-` 的子树都求值完毕,对它本身求值得到 `-24`
+14. 结点 `format` 的子树都求值完毕,对它本身求值得到 `nil`,同时发生副作用:`"it's -24"` 被写入 `stdout`
+
+或许用一门非 Lisp 语言表述上面这个程序会更直观:
+
+```go
+// eval 对 AST 或子树求值
+func eval(node Node) any {
+ if node.IsLeaf() {
+ // 如果是叶子结点,直接返回值
+ return node.Value
+ }
+
+ fn := node.Value
+ var args []any
+ for _, child := node.GetChildren() {
+ args = append(args, eval(child))
+ }
+ // applyFn 根据函数名 fn,把某个操作作用在参数 args 上,最后返回值
+ return applyFn(fn, args)
+}
+```
+
+~~你已经学会怎么写 Lisp 解释器了,快去造一门 Lisp 方言吧!~~
+
+不过 `eval()` 函数的前提是 AST 已经被构造好了,而构造 AST 的前提是什么?回顾上面两棵树,你会发现每个结点中包含的都是有意义的语言单元。这个说法可能有点没头没脑的,难道还能包含没意义的语言单位吗?事实是,如果要创造完整的解释器,构造出 AST,就需要先划分出有意义的语言单元。
+
+不能只根据空格划分,不然 `"Hello World"` 就会被分成 `"Hello` 和 `World"` 两个单元。甚者,`(format t "Hello World")` 会变成:`(format`、`t`、`"Hello` 和 `World")`。
+
+假设有一个 `tokenize()` 函数,它的输入是未经处理的字符串,输出是字符串数组,数组里的每个元素都是独立的语言单元。执行 `tokenize("(format t \"Hello World\")")` 应该得到:
+
+```go
+[]string{
+ "(",
+ "format",
+ "t",
+ "\"Hello World\"",
+ ")"
+}
+```
+
+之所以把括号留下,是因为它们需要作为标记传给 `parse()` 函数。一遇到 `(`,就认为下一个是根,之后的一直到 `)` 都是子树。这个函数作为解析器,输入划分好的 Tokens,输出可以递归求值的 AST。`parse()` 具体怎么实现就不赘述了。
+
+除了 Lisp,你还可以参照 C 语言编译器 `gcc` 的文档的 [1.3 Tokenization](https://gcc.gnu.org/onlinedocs/cpp/Tokenization.html) 这一节。Tokenization 发生在预处理(preprocess)阶段。
+
+至此,你应该明白我说的第二类 Token 指什么了。这类 Token 准确来说叫作 [Lexical Token](https://en.wikipedia.org/wiki/Lexical_analysis#Token)(词法记号,或者说…… 词元)。词法的(lexical)和句法的(syntactic,或名词 syntax)指代不同的抽象层次,词法分析就是把字符串划分为词法记号(lexical token)的过程,而句法分析(更常见的说法是语法分析)则是判断词法记号的组合有没有犯句法错误(syntax error)。Token 是词法层次的实体,分析句法时候我们就默认 Token 已经变成 AST 中不可分割的结点了。至于语法(grammar),则是词法和句法的组合,不过编程语言一般不说 *grammar*,都默认使用 *syntax*(句法或语法)一词。
+
+*真是为了一盘醋包了一场酣畅淋漓的饺子啊。*
+
+## 自然语言处理中的 Token
+
+接下来是第三类 Token,即应用在 NLP 领域的 Token。前面我们在处理编程语言,而 NLP 是对自然语言的处理。那么 NLP 中的 Token 是指 [jieba](https://github.com/fxsjy/jieba) 这类分词工具吗?
+
+的确,中文分词算法就是 NLP 分词的一种,叫作 *Word tokenization*。把英文句子打散成一个一个的单词,也属于这类分词。[^1]
+
+其他的分词方式还有:
+
+1. 字符分词(Character tokenization)
+2. 子词分词(Sub-word tokenization)
+3. 句分词(Sentence tokenization)
+4. N 元分词(N-gram tokenization)
+
+第三个是不是看起来很不对劲?如果是以句子为单位,那为什么会叫作分词呢?这里的 Token 不是词法意义上的 Token,把 *tokenization* 译作「分词」显然不对,那么把 *token* 译作「词元」就更不对了。若是译作「词元」,难道不会和 *word* 混淆吗?
+
+上述老派的 NLP 方法,一般用在非 LLM 聊天机器人、搜索引擎的 Query 分析和词频统计中,根据用例不同选用不同的 Tokenization 方法。那么在更新的 NLP 技术,大语言模型中,Token 又指代什么呢?
+
+大语言模型使用的 Tokenization 方法与简单 NLP 类似,至少输入都是自然语言词句,但算法过程和输出完全不同。LLM 用到了字节对编码(Byte-pair encoding,BFE),最早在 1994 年提出的 BFE 算法如下。[^2]
+
+假设有形如 `aaabdaaabac` 的输入,其中 `aa` 这个字节对的出现频率很高,于是替换为整个字符串中没有出现过的其他字符,比如 `Z`,于是就变成了:
+
+```
+ZabdZabac
+Z=aa
+```
+
+可见 `ab` 也出现了多次,替换为 `Y`
+
+```
+ZYdZYac
+Y=ab
+Z=aa
+```
+
+现在又有了多次出现的 `ZY`,替换为 `X`
+
+```
+XdXac
+X=ZY
+Y=ab
+Z=aa
+```
+
+通过递归的字节对编码,我们把 `aaabdaaabac` 压缩成了 `XdXac`。
+
+大模型修改了 BFE 算法,不再用来压缩字符串,而是用来把字符转换为自然数,因为只有数字才能在神经网络算法中处理。字符转换成自然数之后,就成了 Token。
+
+比方说,`aaabdaaabac` 表示为:
+
+```
+0, 0, 0, 1, 2, 0, 0, 0, 1, 0, 3
+a=0, b=1, d=2, c=3
+```
+
+再递归得到:
+
+```
+4, 5, 2, 4, 5, 0, 3
+a=0, b=1, d=2, c=3, aa=4, ab=5
+```
+
+大语言模型的 Token 可以很大,GPT-3.5 和 GPT-4 的 Token 长度为 100258,其中有 258 个特殊 Token。这些自然数被 Unicode 编码之后可能会变成奇怪的字符,比如字节 `32` 会变成 `Ġ`(U+0120,对应的十进制数是 288,即 32+256),在某些大模型里表示空格。
+
+通过字节对编码得到的 Token 显然不是词语,我们是在字节(byte)的层面对其进行编码的,没有任何意义的字符组合也可以被编码为 Token,我们根本没有在「分词」。
+
+老派 NLP 中的 Token 可以是句子,如今的 LLM 中的 Token 则是基于字节的编码,无论怎么看,LLM 的 Token 都不应该译作「词元」。
+
+## Token 应译作「符」
+
+桌游小人是 Token,游戏代币是 Token,通行凭证是 Token,编译器和解释器划分的词法单元是 Token,NLP 中的字符、词语、句子和 N 元是 Token,LLM 中的字节对编码也是 Token。
+
+把上面这些句子里的 Token 换成「词元」这个中文词,说得通吗?哪怕仅仅是在 LLM 的语境下,「词元」也词不达意。字节对编码显然不是词。
+
+Token 不该译作「词元」,我想这没什么争议了。唯一可以译作「词元」的,只有计算机在做词法分析时得到的词法单元,但那也只是小部分而已。
+
+英文里的 Token 本意是什么?它的[词源](https://www.etymonline.com/word/token)是原始印欧语词根 _*deik-_,意思是展示(*to show*),同源的古诺斯语 *teikn* 意味着星座和征兆(omen),古英语对应的单词 *tacen* 意思是指示、符号、证据和征兆。
+
+星座和征兆都是符号或者说对某种符号的解读,符号就是指示含义或另一个物体的东西。*Token* 就是「指示另一个实体的东西」,至于这「另一个实体」是什么,要根据语境来看。
+
+桌游里的 Token 指示游戏人物、生命值、财产等等,通行凭证或者说令牌指示访问某个资源的权限,编译器和解释器中的词法记号指示不能继续分割的词法要素,NLP 中的 Token 根据使用场景指示一层含义(词语表示的含义、句子表示的含义、N 个词语组成的短语或者说 N 元表示的含义),LLM 中的 Token 指示字节对,同时也对字节对的出现频率做统计学表示。
+
+说到这里我想到一个有些偏门的例子,在《星露谷物语》的某些 Mod 里,由于缺乏标识「玩家是否完成某项任务」的机制,所以有的 Mod 会在玩家与某个 NPC 达成关键进度时,给予名为某某 Token 的物品,并提醒玩家要带着这个 Token 进入特定场景才会触发下一部分的剧情。这里的 Token 就指示「玩家已经达成了某项进度」。
+
+说起通行凭证和令牌,不知道读者有没有看过早年间的古装剧。如果有的话,读者可能熟悉「兵符」这种东西:
+
+> 「符」一般分為兩半,右半在帝王,左半在將軍,調兵之時,使者帶符上朝驗合,才可行動。「符」形狀多種,以虎符為主;質料有銅、木、玉等,以青銅為多。
+
+{{< right "——[漢語多功能字庫](https://humanum.arts.cuhk.edu.hk/Lexis/lexi-mf/search.php?word=%E7%AC%A6)" >}}
+
+通行凭证和令牌不就是「符」吗?词法记号和 NLP 中的 token 就更不用说了,语言本身就是符号。编程语言中的关键字、括号、字面量也是词法符号。==就算未来出现更多基于 *token* 本意衍生出的用法,它多半也是符号,是用来指示某个东西的,而汉语中同样表示「指示某个东西的东西」的词,就是「符」。==
+
+---
+
+## 番外:语言学中的…… Lemma
+
+只有 Token 是「词元」吗?不,*Lemma* 也叫「词元」。
+
+语言学的分支词法学(令人意外,英文名和 Lexical 以及 Lemma 没有关系,叫作 Morphology,*morph-* 是变化、变形的意思)中有个概念叫作 *Lemma*,一般译作词目或者**词元**。
+
+Lemma 也可以叫作 *canonical form*(规范形式)、*dictionary form*(词典形式)和 *citation form*(引用形式)。许多欧洲语言的词语都有多种词位变化,而在词典中往往写作一种形式,把变位作为衍生形式写在同一个条目中。词典中规范的形式就叫作词目或者词元。
+
+比方说 *go* 也有 *went*、*going*、*goes*、*to go* 和 *gone* 等变位,但是在编写词典条目和查找词目的时候,我们只找 *go* 这个条目,而不找 *went* 或者 *gone*。有趣的是,*gone* 作为形容词,也可以是独立的词元。
+
+可能这种区分看起来很没必要,但我给读者举个法语的例子就能看出词元这个概念的必要性了。法语词目 *avoir* 表示「有」,第一人称单数现在时直陈式为 *ai*,第二人称单数现在时直陈式为 *as*,第三人称单数现在时直陈式为 *a*,第一人称复数现在时直陈式为 *avons*,第二人称复数现在时直陈式为 *avez*,第三人称复数现在时直陈式为 *ont*,第一人称单数简单将来时直陈式为 *aurai*……
+
+[^1]: 参考:[Tokenization in NLP - geeksforgeeks](https://www.geeksforgeeks.org/nlp/nlp-how-tokenizing-text-sentence-words-works/)
+[^2]: 参考:[Byte-pair encoding](https://en.wikipedia.org/wiki/Byte-pair_encoding)