LLM 分词与嵌入:从文本到向量,模型如何"读懂"你的输入
为什么大模型不能直接处理文本?Token 是什么?Embedding 又是干什么的?本文从动手 Demo 出发,把 Tokenization 和 Embedding 这两件事串起来讲清楚。
一、神经网络只认数字
LLM 本质上是一个巨大的神经网络,内部全是矩阵乘法。矩阵里的每个元素都是 浮点数------神经网络只认数字,不认识中文、英文、emoji 这些人类字符。
所以你的 prompt 在喂给模型之前,必须走两步:
原始文本 → Tokenization(分词/编码) → Token IDs(整数序列)
Token IDs → Embedding(嵌入/向量化) → 向量(浮点数数组)
第一步把文字变成离散的数字 ID,第二步把 ID 变成连续的、携带语义的向量。模型真正"看到"的是第二步产出的向量。
二、Tokenization:把文字切成 token
什么是 token?
token 是 LLM 计价和工作的最小单位。模型按 token 收费,上下文窗口也用 token 计算。
一个 token 大致相当于一个常见的英文单词或单词片段,但不等于单词。具体怎么切,由编码器(tokenizer)决定。
cl100k_base 编码器
GPT-4 / GPT-4o 使用的是 cl100k_base 编码器,词汇表大小约 100,256 个 token。它基于 BPE(Byte Pair Encoding)算法,能高效覆盖多语言。
⚠️ 纠正一个常见误区 :readme 里写"1 个英文字符 ≈ 0.3 token,1 个中文字符 ≈ 0.6 token",这是错的。
实际上:
- 英文:1 token ≈ 4 个英文字符(或者说 1 个英文字符 ≈ 0.25 token),常见单词 1 个 token,生僻词会被拆成 2-3 个 token
- 中文:1 个中文字符 ≈ 1.5~2.5 个 token,绝对不是 0.6。以 "你好,世界!" 为例,用 cl100k_base 编码大约消耗 10~12 个 token
简单验证:去 OpenAI Tokenizer 粘贴一段中英文试试就知道。
Demo:用 js-tiktoken 编解码
javascript
import { getEncoding } from "js-tiktoken";
// js-tiktoken 把编码表打包在 npm 包里,getEncoding 是同步的,直接返回即可
const enc = getEncoding("cl100k_base");
const text = "hello tiktoken!你好,世界!";
const tokens = enc.encode(text); // 文本 → token ID 数组
const decodedText = enc.decode(tokens); // token ID 数组 → 文本
console.log(tokens); // [15339, 48866, 0, 57668, ...]
console.log(decodedText); // "hello tiktoken!你好,世界!"
注 :
js-tiktoken的getEncoding是同步函数------编码表(约 5MB)全部打包进了 npm 包,不需要运行时下载。这和 Python 的tiktoken库不同(Python 版首次调用要联网下载编码文件,所以是阻塞的)。这套设计让 js 版即开即用,但代价是 node_modules 多占了几 MB 空间。
为什么必须分词?
两个原因:
-
计算效率:如果每个字符都当作独立单元,序列会非常长(中文几万个字符就爆炸)。分词把常见的词/子词合并成一个 token,显著压缩序列长度。这也解释了为什么中文比英文"贵"------中文分词粒度大,一个字符就是一个 token 甚至更多。
-
语义粒度:token 是 BPE 算法找到的最小语义单元。一个 token 刚好"有意义",又不至于太碎。太细(一个字一个字)没有语义,太粗(一整句话一个 token)泛化不了。
三、Embedding:给 token 赋予语义
Tokenization 只是把文字变成了整数 ID(离散符号),但整数之间没有"距离"的概念------ID 15339 和 ID 48866 在语义上近不近?只看 ID 本身,完全不知道。
Embedding 要解决的就是这个问题:把每个 token ID 映射为一个高维向量,让语义相近的词在向量空间里也相近。
怎么理解"向量化"?
想象每个词都有 1024 个属性来描述它:
- 第 1 维可能描述"动作 vs 物体"
- 第 2 维描述"正面 vs 负面"
- 第 3 维描述"抽象 vs 具体"
- ......
这些维度不是人工定义的,而是训练中自动学习出来的。最终每个文本被表示为 1024 维空间的一个点。
Demo:调用 Embedding API + 计算相似度
javascript
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.DASHSCOPE_API_KEY,
baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
});
async function getEmbedding(text) {
const res = await client.embeddings.create({
model: "text-embedding-v4", // 通义千问 Embedding 模型
input: text,
dimensions: 1024, // 输出 1024 维向量,每个分量在 [-1, 1] 之间
});
return res.data[0].embedding;
}
// 余弦相似度:衡量两个向量方向的接近程度,值域 [-1, 1]
function cosineSimilarity(vecA, vecB) {
let dot = 0, magA = 0, magB = 0;
for (let i = 0; i < vecA.length; i++) {
dot += vecA[i] * vecB[i];
magA += vecA[i] ** 2;
magB += vecB[i] ** 2;
}
return dot / (Math.sqrt(magA) * Math.sqrt(magB));
}
const vec1 = await getEmbedding("Andrej Karpathy LLM Tokenization 分词原理");
const vec2 = await getEmbedding("卡帕西讲解大模型BPE字词分词"); // 语义相近
const vec3 = await getEmbedding("今天天气晴朗,适合出门散步"); // 语义无关
console.log(cosineSimilarity(vec1, vec2)); // 期望 > 0.8(语义相似)
console.log(cosineSimilarity(vec1, vec3)); // 期望 < 0.5(语义无关)
余弦相似度 vs 欧氏距离
相似度计算为什么要用余弦而不是欧氏距离?因为 Embedding 向量的长度 可能受文本长度等因素影响,但方向才是语义的体现。余弦相似度只看夹角、不看长度,更符合"两句话意思近不近"的直觉。
值域是 -1, 1:
- 1:方向完全一致,语义相同
- 0:正交,语义无关
- -1:方向相反,语义对立
四、完整数据流
把 Tokenization 和 Embedding 串起来,LLM 处理一次对话的完整路径是:
scss
用户输入 Prompt(文本)
│
▼
Tokenizer 编码(text → token IDs)
│ [15339, 48866, 0, 57668, 224, 5758, ...]
▼
Embedding 向量化(token IDs → 高维向量)
│ [[0.23, -0.87, 0.41, ...], [-0.12, 0.93, ...], ...]
▼
Transformer / 神经网络推理(矩阵计算 + Attention)
│
▼
输出概率分布 → 采样 → Token IDs
│ [3621, 582, 305, ...]
▼
Tokenizer 解码(token IDs → text)
│
▼
模型回复(文本)
关键点:
- Tokenizer 是可逆的(encode ↔ decode),Embedding 不是------Embedding 把离散映射到连续,破坏了可逆性
- 输入的 tokens + 输出的 tokens = 本次请求的总 token 消耗,API 按这个收费
- Embedding 模型和生成模型通常是分开的:前者专做向量化(用于语义搜索、聚类、RAG),后者做文本生成
五、总结
| 概念 | 一句话 |
|---|---|
| Tokenization | 文本 → 整数序列,可逆映射,决定计价和窗口利用率 |
| BPE 编码器 | 用频率统计找到最优子词切分,cl100k_base 有约 10 万个 token |
| Embedding | Token ID → 高维浮点向量,赋予语义,不可逆 |
| 余弦相似度 | 衡量两个向量的语义接近程度,只看方向不看长度 |
| 完整流程 | 文本 → 编码 → 嵌入 → 推理 → 输出 token → 解码 → 文本 |
理解 Tokenization 和 Embedding 的区别和关系,是理解 LLM 底层工作机制的基础。下次看到 max_tokens 或 context window,你就知道它限制的到底是什么了。