为什么大语言模型处理 Token,而不是文字:Tokenizer 与 Embedding 一次讲清

摘要(先看结论)

  • 模型不“读文字”,只“吃数字”。文字先经 tokenizer 变成 token id 序列,再送进模型。
  • token 不是“字”也不是“词”,更常见是“子词/片段”。同一句话在不同 tokenizer 下 token 数可能不同。
  • token id 只是“词表里的索引”,数字大小本身没有语义远近;语义关系来自后续的向量表示(embedding)与模型训练。
  • RAG 的 embedding 是“把一段文本变成一个向量用于检索”,和大模型内部的 token embedding 不是一回事,但它们都属于“用向量表示语义”的思路。

1. Tokenizer 在做什么:把文字变成模型能处理的输入

把一次对话输入到大模型,可以粗略理解成这条流水线:

  1. 文本 → tokenizer.encode(...) → token id 序列(整数)
  2. token id 序列 → embedding → 向量序列(浮点数向量)
  3. 向量序列 → 模型多层计算 → 预测下一个 token 的概率分布
  4. 采样/解码 → 输出 token id → tokenizer.decode(...) → 文本

这里最关键的一点是:模型的输入不是字符,而是序列化的整数(token id)


2. Token 到底是什么:不是字,也不一定是词

token 更像“拼积木的单位”:

  • 有时一个 token 就是一个完整词(英文里更常见)
  • 有时一个词会被拆成多个 token(例如词根 + 词缀、或罕见拼写)
  • 中文里常见情况是:一个 token 可能对应一个字、一个常见词、或带标点/空格的片段(取决于 tokenizer)

很多人卡住的点在这里:你看到的 token 像“字/词/片段”,但模型真正吃的是数字。

更精确地说有三层东西:

  • token(文本片段):tokenizer 切出来的片段(“看起来像字/词/词组的一部分”)。
  • token id(整数):每个 token 在词表里的编号;模型输入的是这一串编号。
  • embedding(向量):模型把 token id 映射成向量后再进行计算。

那这个“编号(token id)”是怎么来的?

  • 先用 BPE/WordPiece/Unigram 这类算法在海量语料上训练出一份词表(vocab):里面列出了“允许出现的 token 片段集合”。
  • 再把词表里的每个 token 按某种固定顺序排好并依次编号(同时会给 <eos>/<bos>/<pad>/<unk> 等特殊 token 预留固定编号)。
  • 所以 token id 更像“字典索引号”,它的大小通常不代表语义远近;语义远近靠 embedding 与后续训练来形成。

因此,“上下文窗口有多少 token”并不能直接换算成“多少字”,只能做经验估算。


3. 为什么不能让模型直接处理文字

并不是“直接处理字符不行”,而是工程上通常不划算:

  • 字符级别序列更长,计算更重(Transformer 的计算量与序列长度强相关)
  • 子词 token 能在“词表大小”和“序列长度”之间做一个更好的折中
  • token 化还能让模型更容易复用常见片段,减少对完全字符级建模的依赖

这里说的不是“汉字 vs 数字”。因为无论中文还是英文,最终都会变成 token id(数字)进模型;区别在于切分粒度会让“序列长度”差很多,从而影响计算量。

举个中文例子(示意,同一句话不同 tokenizer 可能略有差异):

  • 字符级:人 / 工 / 智 / 能 / 很 / 有 / 趣 → 7 个 token
  • 子词级:人工智能 / 很 / 有趣 → 3 个 token

注意力的计算大致会随着 token 数 T 增长得很快(常见近似是 )。所以把 7 个 token 压到 3 个 token,工程上就能省下不少计算。


4. Embedding 是什么:把 token id 变成向量

token id 只是一个整数索引,本身没有“距离=语义距离”的含义。

embedding 的作用是:把离散的索引映射为连续向量,让模型可以用连续空间表示“相似/相关”的关系。

最常见的 token embedding 本质就是一张可训练的“查表矩阵”:

  • 形状:[vocab_size, d_model]
  • 输入:token id(例如 12345)
  • 输出:长度为 d_model 的向量(例如 4096 维)

工程实现经常是“embedding lookup”(按行取向量),而不是显式做 one-hot 再乘矩阵;但数学本质是一样的。

把上面几个词翻译成人话:

  • vocab_size:词表大小,也就是“token 总共有多少个编号”。例如 50,000 表示词表里一共有 50,000 个 token。
  • d_model:向量维度,也就是“每个 token 用多少个数字来表示”。例如 4096 表示每个 token 都会被映射成一个长度为 4096 的向量。
  • one-hot:把“一个编号”变成“一个很长的 0/1 向量”。例如词表大小是 5,token id=2 的 one-hot 就是 [0, 0, 1, 0, 0](只有第 2 个位置是 1)。
  • embedding lookup:直接“按编号查表取一行”。例如 token id=2,就去 embedding 矩阵里取第 2 行向量作为输出。

为什么说 one-hot 乘矩阵和 lookup 是一回事(用一个极小例子说明):

  • 词表大小 vocab_size=5,向量维度 d_model=3,embedding 矩阵就是一个 5×3 的表。
  • token id=2:
    • lookup:取表的第 2 行,得到一个长度为 3 的向量。
    • one-hot:先得到 [0,0,1,0,0],再乘 5×3 的矩阵,结果也只会“选中第 2 行”,得到同一个向量。

你不需要“在工程里手写 one-hot”。了解它的价值主要是两点:

  • 帮助你理解 embedding 这张表为什么是 [vocab_size, d_model]:每个 token id 对应表里的一行。
  • 帮助你理解 lookup 为什么成立:lookup 只是把“one-hot × embedding 矩阵”这个运算优化成了“直接取一行”,更省内存和算力。

5. 一个常见误区:token id 的大小不代表语义远近

很多直觉会误以为:

  • “苹果的 token id 和 apple 的 token id 如果很接近,那语义应该也接近”

这是不成立的,因为:

  • token id 的分配方式本质是“词表里的编号”,通常只保证“可逆映射”,不保证“编号连续性有语义”
  • 语义关系来自 embedding(以及后续多层计算)把不同 token 拉到“更相近/更可用”的向量空间里

所以你应该记住一句话:

token id 是索引;embedding 才是语义坐标。


6. RAG 里的 Embedding 是什么:把一段文本变成一个向量

RAG(检索增强生成)常见做法是:

  1. 把文档切成若干段(chunk)
  2. 用“文本 embedding 模型”把每段 chunk 变成一个向量(例如 768/1024/1536 维)
  3. 用户提问也变成一个向量
  4. 在向量库里做相似度检索(常见是余弦相似度或内积),拿到最相近的几段
  5. 把这些段落拼进大模型上下文,让大模型回答

这个 embedding 的目标是:让语义相近的文本在向量空间里更接近,从而能“用检索找相似内容”。


7. LLM Embedding vs RAG Embedding:哪里一样,哪里不一样

它们都叫 embedding,都在做“用向量表示语义”,但使用方式与产物不同:

对比项 大模型内部(token embedding / hidden states) RAG(文本 embedding)
输入对象 单个 token(序列中的每个位置) 一段文本(一个 chunk / query)
输出形态 一串向量(长度=token 数) 一个向量(固定维度)
是否上下文相关 是(同一个词在不同上下文向量会变) 通常弱一些(一个文本得到一个向量)
训练目标 主要服务“预测下一个 token” 主要服务“语义相似度检索”
工程用途 让模型能推理与生成 让系统能召回相关资料

一句话区分:

  • 大模型的 embedding 更像“生成任务的内部表示”
  • RAG 的 embedding 更像“检索任务的索引表示”
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐