从 Token 到 Embedding:一文搞懂 LLM 是怎么"理解"文字的
你是否好奇过:为什么大模型按 token 计费?中英文的 token 数量为什么不一样?模型又是怎么"读懂"我们输入的文字的?本文带你从零开始,用代码实操搞懂 Tokenization 和 Embedding 这两个 LLM 最基础也最重要的概念。
为什么你需要了解 Token?
用过大模型 API(ChatGPT、Claude、通义千问等)的同学一定见过这样的计费方式:
按 token 计费,百万 token 几块钱。
那 token 到底是什么?为什么大模型不按字数,非要按 token 来算钱?
答案很简单:token 是 LLM 理解和处理文字的最小单位,就像我们人类阅读时的"词"一样。
先记住一个直观的换算关系:
| 文字类型 | 大约换算 |
|---|---|
| 1 个英文字符 | ≈ 0.3 个 token |
| 1 个中文字符 | ≈ 0.6 个 token |
也就是说,"hello" 这个英文单词大约 1.5 个 token,而"你好世界"大约 2.4 个 token。
知识点 :不同模型使用不同的分词器(tokenizer),同一个文本在不同模型中的 token 数量可能不同。比如 GPT 系列使用
cl100k_base编码,而其他模型可能使用不同的词表。
一、为什么必须分词?------ 从"文字"到"数字"的桥梁
这是一个很核心的问题。我们输入的 prompt 是文本,但底层的神经网络只能处理数字(向量、矩阵运算),压根看不懂中文、英文这些字符。
这就需要一个"翻译官"把文字转成数字------这个过程就是 Tokenization(分词)。
arduino
文字 "你好世界" → Tokenization → [57668, 53973, 11410] → 模型运算 → [57668, 53973, 11411] → 解码 → "你好世界!"
为什么是 token 而不是字符?
- 如果把每个字拆成一个字符,会丢失词语的语义信息,"苹"和"果"分开就没有"苹果"的含义了
- 如果把整个句子当一个单位,那组合数量会爆炸,模型根本处理不过来
- token 是折中方案------既保留了语义单元,又控制了词表大小
知识点 :现代大模型普遍使用 BPE(Byte Pair Encoding,字节对编码) 算法来做分词。简单理解就是:从大量文本中统计哪些字符经常一起出现,把它们合并成一个"词"。比如 "app" 和 "le" 经常一起出现,就合并成 "apple" 这个 token。
二、Tokenization 实战:用 js-tiktoken 自己试试
光说不练假把式,我们直接写代码来感受一下 tokenization。
2.1 安装依赖
bash
npm install js-tiktoken
js-tiktoken 是 OpenAI 官方分词器 tiktoken 的 JavaScript 移植版,它能用和 GPT-4 一模一样的规则来分词。
2.2 编码与解码
javascript
import { getEncoding } from 'js-tiktoken';
// 使用 GPT 官方的 cl100k_base 编码器
const enc = getEncoding('cl100k_base');
const text = "Hello, tiktoken! 你好,世界!";
// 编码:文本 → token ID 数组
const tokens = enc.encode(text);
console.log("Token IDs:", tokens);
// 输出: [9906, 11, 235, 340, 83, 137, 404, 0, 57668, 53973, 20412, 105796, 6447, 227]
console.log("Token 数量:", tokens.length);
// 输出: 14
// 解码:token ID 数组 → 文本
const decodedText = enc.decode(tokens);
console.log("解码后文本:", decodedText);
// 输出: "Hello, tiktoken! 你好,世界!"
运行结果一目了然:
- 短短一句 "Hello, tiktoken! 你好,世界!" 被拆分成了 14 个 token
- 每个 token 对应一个整数 ID(比如 9906 代表 "Hello")
- 解码后能完美还原原文
让我们再看看每个 token 到底对应什么:
javascript
// 看看每个 token ID 分别代表什么文本
for (const token of tokens) {
console.log(`ID: ${token} → "${enc.decode([token])}"`);
}
// 输出类似:
// ID: 9906 → "Hello"
// ID: 11 → ","
// ID: 235 → " t" ← 注意!空格也被编码进去了
// ID: 340 → "ik"
// ID: 83 → "token"
// ID: 137 → "!"
// ID: 404 → " 你" ← 中文"你"前面有个空格
// ID: 0 → "好" ← 注意 ID=0,这是特殊 token
// ...
知识点 ------ cl100k_base:这是 GPT-4 和 GPT-3.5-turbo 使用的编码器名称。"cl" 代表 "contrastive learning","100k" 表示词表大小约为 10 万个 token。"base" 说明它是基础版本。这 10 万个 token 覆盖了几乎所有常见的中英文组合、标点、空格组合。在 GPT 眼中,你的输入就是这些 token ID 组成的一串数字。
2.3 一次调用的 token 怎么算?
LLM 计费时考虑的是 总 token 数:
总 token 数 = 输入的 tokens + 输出的 tokens
比如你用 GPT-4,输入 100 个 token,它回复了 200 个 token,那这次调用就消耗了 300 个 token。
三、Embedding:给 Token 注入"语义"
3.1 从 Token 到 Embedding
Tokenization 只是第一步------把文字变成了一串 ID。但这些 ID 只是编号,"苹果" = 1234 和"香蕉" = 5678 之间,数字上没有任何关系。
神经网络要理解语义,需要把这些离散的 ID 进一步转换成连续的高维向量 ,这个过程叫 Embedding(嵌入/向量化)。
yaml
文本 → Tokenization → token ID 数组 → Embedding → 高维向量(如 1024 维)
这条流水线就是:
scss
prompt(文本输入) → tokens(编码器) → 向量化(embedding 数字语义) → LLM(Transformer) → tokens(解码器) → 文本输出
知识点:Embedding 向量中的每个维度都在 -1, 1 之间,1024 个维度共同表达了一个词的语义。这就像给每个词画了一张"语义身份证"------意思相近的词,它们的向量在空间中距离更近。
3.2 Embedding 实战:语义相似度计算
下面我们用阿里百炼的 embedding 接口来实际感受"语义"是如何被计算出来的。
bash
npm install openai dotenv
配置环境变量 .env:
ini
DASHSCOPE_API_KEY=你的阿里百炼API密钥
完整代码:
javascript
import OpenAI from 'openai';
import dotenv from 'dotenv';
dotenv.config();
const client = new OpenAI({
apiKey: process.env.DASHSCOPE_API_KEY,
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1'
});
// 获取文本的 embedding 向量
async function getEmbedding(text) {
const res = await client.embeddings.create({
model: 'text-embedding-v4', // 嵌入模型
input: text,
dimensions: 1024 // 输出 1024 维向量
});
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; // A 的模长平方
magB += vecB[i] ** 2; // B 的模长平方
}
return dot / (Math.sqrt(magA) * Math.sqrt(magB));
}
async function run() {
const text1 = "Andrej Karpathy LLM Tokenization 分词原理";
const text2 = "卡帕西讲解大模型BPE字词分词";
const text3 = "今天天气晴朗,适合出门散步";
const vec1 = await getEmbedding(text1);
const vec2 = await getEmbedding(text2);
const vec3 = await getEmbedding(text3);
// 语义相似的内容 → 高相似度
console.log("相似内容:", cosineSimilarity(vec1, vec2));
// 输出: 0.85+ (text1 和 text2 都在讲"分词",相似度高)
// 语义无关的内容 → 低相似度
console.log("无关内容:", cosineSimilarity(vec1, vec3));
// 输出: 0.5 左右(text1 讲分词,text3 讲天气,几乎无关)
}
run();
3.3 为什么用余弦相似度?
你可能注意到了,我们用的不是欧几里得距离,而是余弦相似度。
知识点 :余弦相似度衡量的是两个向量方向的相似程度,而不是绝对位置。它的值在 -1, 1 之间:
- 1 表示方向完全一致(语义完全相同)
- 0 表示正交(毫无关系)
- -1 表示方向完全相反(语义相反)
在高维空间中(1024 维),向量的方向比它的"长度"更能反映语义关系,所以余弦相似度是 NLP 领域计算语义相似度的标准方法。
余弦相似度公式:
css
cos(θ) = (A · B) / (|A| × |B|)
= 点积 / (A的模长 × B的模长)
四、一张图总结全流程
css
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 输入文本 │ ──→ │ Tokenization │ ──→ │ Embedding │ ──→ │ Transformer │
│ "你好世界" │ │ [57668,...] │ │ [0.12,-0.34 │ │ (模型运算) ││ │ │ cl100k_base │ │ ,...,0.78] │ │ │
└──────────────┘ └──────────────┘ └──────────────┘ └──────┬───────┘
│
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ 输出文本 │ ←── │ 解码器 │ ←── │ 输出 Token │ ←─────────┘
│ "你好!" │ │ token→文字 │ │ [57668,...] │
└──────────────┘ └──────────────┘ └──────────────┘
五、核心知识点清单
| 概念 | 一句话解释 |
|---|---|
| Token | LLM 处理文本的最小单位,介于"字符"和"单词"之间 |
| Tokenization | 将文本转为 token ID 序列的过程,也叫"分词" |
| BPE | 字节对编码,主流分词算法,通过统计高频字符组合来构建词表 |
| cl100k_base | GPT-4/3.5 使用的编码器,词表大小约 10 万 |
| Embedding | 将 token ID 转为高维连续向量,注入语义信息 |
| Dimension(维度) | 向量有多少个数字,常见 768、1024、1536、3072 |
| 余弦相似度 | 衡量两个向量方向相似度的指标,-1, 1,NLP 标准做法 |
| 总 Token 数 | 输入 token + 输出 token,LLM 计费的依据 |
六、写在最后
理解 Token 和 Embedding,是深入理解大模型的第一道门槛。掌握了这些,你会明白:
- 为什么 prompt 越长越贵(输入 token 多)
- 为什么中英文混用不影响模型理解(都会被映射到统一的 token 空间)
- 为什么 RAG(检索增强生成)要用 embedding 做语义检索
- 为什么模型能"理解"两个不同表述说的是同一件事(语义向量相似)
这些概念构成了 LLM 世界的基石。在此基础上,你才能更好地理解 Transformer 架构、Attention 机制、Fine-tuning(微调) 等更高级的话题。
如果这篇文章对你有帮助,欢迎点赞、收藏、关注!有疑问欢迎在评论区讨论。