跳到正文

从文本到 Token 与向量 ​

一句话:模型不会读字,它只做矩阵运算。分词器先把文本切成词表里的 token,再把每个 token 查表变成一个向量;上下文长度、费用和 KV 显存,全部从 token 数开始算。

为什么需要 ​

模型的输入必须是一个固定维度的数值矩阵,而文本是变长的离散符号。中间要跨过两道坎:怎么把字符串切成离散单元,以及怎么把离散单元变成连续向量。

切分粒度只有三种选择,每种都有明显代价:

  • 按词切:词表会爆炸,新词、拼写变体、专有名词都成了未登录词;中文还要先分词,分词错误会一路传下去。
  • 按字符切:词表很小,但序列变得很长,一个汉字一个 token,同样一段话的注意力成本成倍增加;字符本身携带的语义也太少。
  • 按子词切:把高频片段作为一个单元,低频词拆成更小的片段。这是现在所有主流模型的共同选择。

分词结果直接决定 token 数,而 token 数是后面所有账本的单位:上下文窗口按 token 算,API 按 token 计费,KV 缓存按 token 占显存,生成长度也按 token 限制。同一个句子换一个分词器,token 数可能差出两三成,成本和容量估算都会跟着偏。

核心机制 ​

一、子词分词:从字符开始合并 ​

以 BPE(Byte Pair Encoding)为例,训练过程只有三步:

  1. 把所有训练文本拆成最小单元。按字节切可以覆盖任意 UTF-8 字符,不会出现未登录词;按字符切则词表更贴近语言。
  2. 统计相邻单元对的出现次数,把频率最高的一对合并成一个新单元。
  3. 重复第 2 步,直到词表达到预定大小。

合并规则就是分词器。推理时用同一组规则,从左到右把文本切成词表里最长的片段。高频词会成为单个 token,低频词退回更小的片段——这就是“常见词短、生僻词长”的原因。

SentencePiece 在此之上把空格也当成普通符号处理,因此不依赖语言的分词边界,中文、日文这类没有空格的语言也能直接训练。GPT 系列常用 byte-level BPE,Llama 系列用 SentencePiece 的 BPE 变体,词表大小从 3 万到 20 万不等。

二、从 token ID 到向量 ​

分词器输出的是整数 ID。模型里有一张嵌入矩阵 E,形状是 词表大小 × d_model;ID 就是行号,取出对应的一行,就得到这个 token 的向量。

text
token id  →  E[id]  →  d_model 维向量

这张矩阵是训练出来的,不是人工标注的。训练开始时随机初始化,反向传播只更新被用到的行;出现次数少的 token,向量也练得不准。很多模型让输入嵌入和输出投影共享同一张矩阵(weight tying),既省参数,也让“输入某个词”和“预测某个词”使用同一套坐标。

得到的是上下文无关的向量:同一个词在任何句子里,查表结果都一样。让“苹果”在“吃苹果”和“苹果发布会”里表示不同意思的,是后面的注意力层,不是嵌入层。

这张图把“文本进模型”拆成四步:字节化、按规则切分、映射成 ID、查表成向量。前两步由分词器完成,后两步由模型完成;分词器版本和嵌入矩阵必须匹配,换分词器而不换嵌入,ID 的含义就全变了。

三、token 不等于词 ​

这是实际估算时最容易错的地方:

  • 一个英文单词可能是一个 token,也可能被拆成三四个;越生僻、越长,越容易被拆。
  • 一个汉字通常是一到两个 token,取决于词表和该字在训练语料里的频率。
  • 数字常被拆成多位一组,2026 可能是 202 + 6,模型对数字逐位运算的能力因此受影响。
  • 开头的空格、标点、大小写都可能是独立 token;" hello" 和 "hello" 的 ID 不同。
  • 代码里的缩进、括号、常见变量名会被合并;罕见标识符会被拆得很碎。

所以“1000 个汉字大约多少 token”没有统一答案,必须用目标模型的分词器实测。不同模型的 token 数不可直接比较,它们的词表、合并规则和特殊 token 都不一样。

四、词表大小的取舍 ​

词表越大,同样的文本切出的 token 越少,上下文更省、推理更快;代价是嵌入矩阵和输出层更大,低频 token 的训练样本更少,尾部 token 的向量质量更差。词表越小,序列越长,注意力的平方级成本越高。

常见词表在 3 万到 20 万之间。Llama 3 的词表是 128,256,GPT-4o 的 o200k_base 约 20 万——大词表是为了更好地压缩多语言和代码,不是单纯“越大越好”。词表在预训练前就固定,之后新增 token 必须扩展嵌入矩阵并继续训练,否则新 token 的向量是随机初始化的。

五、特殊 token 与对话模板 ​

除了文本 token,分词器还定义了一批特殊 token:句子开始/结束、填充、角色标记(system/user/assistant)、工具调用边界。对话模型靠这些标记区分“谁在说话”和“哪里是工具参数”。

这也是为什么不能把消息手工拼成字符串再发给模型:角色、边界和结束位置都要用模型规定的模板。模板错了,模型看到的就不是它训练时见过的结构,轻则答非所问,重则把工具参数当成普通文本生成。

最小上手 ​

下面用一个 20 行的 BPE 训练器,观察“高频片段被合并”的过程:

python
from collections import Counter

def train_bpe(words, merges=6):
    vocab = [list(w) + ["</w>"] for w in words]
    rules = []
    for _ in range(merges):
        pairs = Counter()
        for chars in vocab:
            pairs.update(zip(chars, chars[1:]))
        if not pairs:
            break
        pair = pairs.most_common(1)[0][0]
        rules.append(pair)
        for i, chars in enumerate(vocab):
            out, j = [], 0
            while j < len(chars):
                if j + 1 < len(chars) and (chars[j], chars[j + 1]) == pair:
                    out.append("".join(pair)); j += 2
                else:
                    out.append(chars[j]); j += 1
            vocab[i] = out
    return rules, vocab

words = ["low"] * 5 + ["lower"] * 2 + ["newest"] * 6 + ["widest"] * 3
rules, vocab = train_bpe(words)
print(rules)
print(sorted({"/".join(v) for v in vocab}))

输出(合并规则和最终切分):

text
[('e', 's'), ('es', 't'), ('est', '</w>'), ('l', 'o'), ('lo', 'w'), ('n', 'e')]
['low/</w>', 'low/e/r/</w>', 'ne/w/est</w>', 'w/i/d/est</w>']

可以看到 est 先被合并成整体,随后 low 也成为一个 token;newest 和 widest 共享了 est,但前缀不同,切分结果也不同。真实分词器只是把这个过程放大到几十万词表,并对字节而不是字符操作。

生产动作 ​

  • 按 token 做预算,不按字符。上下文预算 = 系统提示 + 工具 Schema + 历史消息 + 检索内容 + 输出预留,每一项都用目标模型的分词器计数。
  • 输出要留余量。max_tokens 不是“最多写多少”,而是“必须从窗口里扣掉多少”;不预留会触发截断,JSON 或工具调用会在中途断掉。
  • 截断按优先级,不按位置。先保留系统约束和最近轮次,再压缩检索材料;不要从中间硬切,切掉的可能正是任务目标。
  • 分词器版本跟模型绑定。升级模型时同步升级分词器和对话模板;自己扩词表要重训嵌入,并重新评估旧任务。
  • 对中文、英文、代码分别统计。同一套业务在中英文混合场景下的 token 成本可能相差一倍,按单一比例估算会失真。

边界 ​

  • token 不是语义单元。一个 token 可能是一个词、半个词或几个字符,token 数少不代表句子简单。
  • 嵌入向量不是“词义”。它是训练得到的坐标,只在当前模型的坐标系里有意义,不能跨模型比较或直接做语义运算。
  • 分词器不会理解文本。它只做字符串到整数的映射;分词错误会放大,但分词正确也不保证模型理解正确。
  • 词表不是越大越好。大词表省 token,但尾部 token 训练不足,嵌入和输出层也更占显存。
  • 特殊 token 是协议的一部分。手工拼接消息、忽略结束标记、错用角色标记,都会让模型处在训练分布之外。

讲清楚 ​

模型只接受数值矩阵,所以文本要先过分词器:按 BPE 这类子词规则把字符串切成词表里的 token,再用 token ID 去嵌入矩阵里查表,得到向量。token 是上下文、费用和 KV 显存的统一单位,但它不等于词——同一个句子换分词器,token 数会变,不同模型之间也不能直接比。词表大小是压缩率与尾部质量的取舍;特殊 token 和对话模板则是模型协议的一部分,不能手工拼字符串替代。

实际做预算时,用目标模型的分词器数 token,给输出留余量,按优先级截断,并把分词器版本和模型版本绑定。

适用:所有基于子词分词的大模型;词表与模板以具体模型为准 | 初稿 2026-10

把 AI 工程讲到既懂也会用