🧠 深入理解 LLM Tokenization:从文本分词到语义向量化的完整旅程
📌 本文带你从零理解大模型如何处理文本 ------ 从 Token 分词到 Embedding 向量化,再到语义相似度计算,附完整可运行代码。
一、为什么理解 Tokenization 很重要?
当我们使用 ChatGPT、Claude 等大模型时,按 Token 计费是常态。但 Token 到底是什么?为什么 "Hello, world!" 不是 13 个 Token?为什么中文往往比英文消耗更多 Token?
理解 Tokenization(分词)能帮你:
- 🪙 控制 API 成本(按 Token 计费)
- 📐 合理设计 Prompt(模型有上下文窗口限制)
- 🔍 理解模型处理语言的方式
- 🛠️ 排查模型输出异常(某些词被奇怪地拆分)
二、Tokenization 全景架构
LLM 处理文本的完整流水线分为六个环节:
第一步:用户输入原始文本,进入分词器(Tokenizer),将自然语言文本拆解为模型能理解的最小单位------Token。
第二步:每个 Token 被映射为一个唯一的数字 ID,形成数字序列。模型不"读"文字,它只"读"数字。
第三步:Token ID 序列进入 Embedding 层,每个 ID 被映射为一个高维浮点数向量(如 1024 维)。这个向量承载了该 Token 的语义信息。
第四步:向量序列送入 Transformer 模型进行推理计算。
第五步:模型输出 logits(概率分布),解码器从中采样生成新的 Token ID 序列。
第六步:De-Tokenizer(分词器的反向操作)将 Token ID 序列还原为人类可读的文本。
核心要点:模型不"读"文字,它只"读"数字。Tokenization 是文本和数字之间的双向桥梁。
三、实战一:用 tiktoken 亲手体验分词
3.1 什么是 tiktoken?
tiktoken 是 OpenAI 开源的快速 BPE(Byte Pair Encoding)分词库,GPT-4/GPT-3.5 等模型直接使用它进行分词。
关键概念:
cl100k_base:GPT-4 和 GPT-3.5-turbo 使用的编码表,约 100,000 个 Token。它是基于大规模多语言语料训练出来的 BPE 词表。- encode:将文本转为 Token ID 序列(数字数组)
- decode:将 Token ID 序列还原为文本
3.2 完整代码
javascript
import { getEncoding } from "js-tiktoken";
// 获取 GPT 官方的 Token 编码表 cl100k_base
// 底层基于 UTF-8 编码
const enc = getEncoding("cl100k_base");
// 中英混合文本
const text = "Hello, tiktoken! 你好,世界!";
// 编码:文本 → Token ID 数组
const tokens = enc.encode(text);
console.log("Token IDs:", tokens);
console.log("Token 数量:", tokens.length);
// 解码:Token ID 数组 → 文本
const decodedText = enc.decode(tokens);
console.log("解码还原:", decodedText);
// 查看每个 Token 对应的文本片段
for (const token of tokens) {
console.log(`Token ${token} → "${enc.decode([token])}"`);
}
3.3 运行结果分析
以 "Hello, tiktoken! 你好,世界!" 为例,运行后你会看到每个 Token ID 对应的文本片段。你会发现:
- 英文单词 通常是 1~2 个 Token(如
Hello是一个 Token) - 标点符号 和空格也是独立的 Token
- 中文字符往往是 1~2 个 Token 一个汉字
- 中文比英文"贵"的根本原因:英文单词本身就承载丰富语义,一个词就是一个 Token;而中文需要多个 Token 组合才能表达一个词
四、深入 BPE 分词算法原理
4.1 分词方式对比
在 BPE 出现之前,有几种传统的分词方式:
- 按空格分词 :将
"I love NLP"拆成["I", "love", "NLP"]。问题很明显------中文、日语等语言根本没有空格分隔。 - 按字符分词 :将
"hello"拆成["h", "e", "l", "l", "o"]。序列变得极长,Transformer 的计算复杂度是 O(n²),效率急剧下降。 - 按词分词:维护一个巨大的词表,每个完整单词一个 ID。词表太大,且无法处理未见过的词(OOV 问题)。
BPE(Byte Pair Encoding) 是一个优雅的折中方案。它的核心思想是:从字符级别出发,反复合并出现频率最高的相邻符号对,直到达到目标词表大小。
4.2 BPE 训练过程
假设我们有语料:"low low low low low lower lower newest newest newest" 代表词频。
初始状态:将每个字符视为独立符号(包括空格和单词边界标记)。
第一轮合并 :统计所有相邻符号对的出现频率,发现 "l" + "o" 组合出现次数最多(在 low 和 lower 中都出现了),于是将其合并为一个新符号 "lo"。
第二轮合并 :"lo" + "w" 的组合出现频率最高(low 反复出现),合并为 "low"。
第三轮合并 :"e" + "r" 的组合出现频率最高(lower 和 newest 中),合并为 "er"。
持续迭代,直到达到预设词表大小(如 100,000)。最终词表中既有高频完整词(如 "the"、"low"),也有常见的子词片段(如 "ing"、"er"、"tion")。
4.3 BPE 的优势
| 维度 | 优势 |
|---|---|
| 🆕 OOV 处理 | 通过子词组合可表示任意未见词,没有"未知词"问题 |
| 📦 词表大小可控 | 通常在 30K~100K 之间,由训练者决定 |
| 🌍 多语言友好 | 无需针对不同语言设计不同的分词器 |
| ⚡ 编解码高效 | 词表适中,查找和合并速度都很快 |
五、实战二:从 Token 到语义向量(Embedding)
5.1 为什么需要 Embedding?
了解 Token 之后,下一个问题是:模型怎么"理解" Token 的含义?答案是 Embedding(嵌入) 。
Token ID 只是一个整数标识符,本身不携带任何语义信息------ID 为 9906 和 1234 之间没有任何数值关系。Embedding 层的作用,就是将每个 Token ID 映射到一个高维稠密向量(如 1024 维的浮点数数组),让语义相似的词在向量空间中的距离也接近。
实际效果是:"猫" 和 "狗" 的向量在空间中距离很近(都是宠物),而 "猫" 和 "汽车" 的向量距离很远(无关概念)。更神奇的是,这种语义关系是跨语言的------中文的 "猫" 和英文的 "cat" 在空间中也非常接近。
5.2 调用 Embedding API
这里使用阿里云 DashScope(兼容 OpenAI SDK),选择 text-embedding-v4 模型输出 1024 维向量:
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",
});
/**
* 文本向量化封装
* @param {string} text - 输入文本
* @returns {number[]} - 1024维浮点数向量
*/
async function getEmbedding(text) {
const res = await client.embeddings.create({
model: "text-embedding-v4", // 嵌入模型
input: text,
dimensions: 1024, // 输出维度
});
return res.data[0].embedding;
}
Embedding API 参数详解:
| 参数 | 说明 |
|---|---|
model |
嵌入模型名称。阿里云用 text-embedding-v4,OpenAI 用 text-embedding-3-small |
input |
要向量化的文本,支持批量传入字符串数组 |
dimensions |
输出向量的维度。越高保留的语义信息越精细,但计算和存储开销也更大。常用 768、1024、1536 |
⚠️ 重要提示 :不同 Embedding 模型产出的向量不可混用!如果用 v3 模型向量化了知识库,用 v4 模型去做查询,得出的相似度毫无意义。务必保证编码和查询使用同一个模型。
六、实战三:余弦相似度 ------ 用数学衡量语义
6.1 算法原理
有了向量之后,如何衡量两个向量的相似程度?答案是 余弦相似度。
它衡量两个向量在方向上的接近程度,值域为 [-1, 1]。1 表示方向完全一致(语义相同),0 表示正交(无关),-1 表示方向完全相反(语义对立)。
公式很简单:两个向量的点积除以它们模长的乘积。
6.2 代码实现
ini
/**
* 余弦相似度计算
* @param {number[]} vecA - 向量A
* @param {number[]} vecB - 向量B
* @returns {number} - 相似度 [-1, 1]
*/
function cosineSimilarity(vecA, vecB) {
let dotProduct = 0;
let magnitudeA = 0;
let magnitudeB = 0;
for (let i = 0; i < vecA.length; i++) {
dotProduct += vecA[i] * vecB[i]; // 点积:对应位置相乘再求和
magnitudeA += vecA[i] ** 2; // A 向量模的平方
magnitudeB += vecB[i] ** 2; // B 向量模的平方
}
return dotProduct / (Math.sqrt(magnitudeA) * Math.sqrt(magnitudeB));
}
6.3 语义对比实验
设计两组对照实验来验证效果:
ini
async function run() {
// 实验组1:中英文表达同一语义(Andrej Karpathy 讲解 LLM 分词)
const text1 = "Andrej Karpathy LLM Tokenization 分词原理";
const text2 = "卡帕西讲解大模型BPE字词分词";
// 实验组2:完全不相关的语义
const text3 = "今天天气晴朗,适合出门散步";
const vec1 = await getEmbedding(text1);
const vec2 = await getEmbedding(text2);
const vec3 = await getEmbedding(text3);
// 跨语言同语义对比
const similarity_en_zh = cosineSimilarity(vec1, vec2);
console.log("跨语言同语义相似度:", similarity_en_zh);
// 预期:0.85+ ------ 高度相似
// 不同语义对比
const similarity_diff = cosineSimilarity(vec1, vec3);
console.log("不同语义相似度:", similarity_diff);
// 预期:0.3~0.5 ------ 明显更低
}
run();
6.4 结果解读
典型运行结果类似:
makefile
跨语言同语义相似度: 0.876
不同语义相似度: 0.342
尽管 text1 是英文、text2 是中文,它们表达的是同一个主题,余弦相似度高达 0.87。而 text1 和 text3 主题完全不搭边,相似度骤降到 0.34。
这揭示了一个重要特性:Embedding 向量天然具有跨语言语义对齐能力。无论你用哪种语言表达,只要语义相同,向量就会聚在一起。这正是语义搜索、跨语言检索等技术的基础。
七、完整流水线代码
将上述三部分串联------这就是 LLM 处理文本的完整前端流水线:
javascript
import { getEncoding } from "js-tiktoken";
import OpenAI from "openai";
import dotenv from "dotenv";
dotenv.config();
// ==================== 阶段1: Tokenization ====================
const enc = getEncoding("cl100k_base");
function tokenize(text) {
const tokens = enc.encode(text);
console.log(`[Tokenize] "${text}" → ${tokens.length} tokens`);
return tokens;
}
// ==================== 阶段2: Embedding ====================
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",
input: text,
dimensions: 1024,
});
console.log(`[Embedding] "${text.slice(0, 20)}..." → [${res.data[0].embedding.length}维向量]`);
return res.data[0].embedding;
}
// ==================== 阶段3: 相似度计算 ====================
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));
}
// ==================== 完整流程 ====================
async function fullPipeline(text1, text2) {
console.log("========== LLM 文本处理流水线 ==========\n");
const tokens1 = tokenize(text1);
const tokens2 = tokenize(text2);
const vec1 = await getEmbedding(text1);
const vec2 = await getEmbedding(text2);
const similarity = cosineSimilarity(vec1, vec2);
console.log(`\n[Result] 余弦相似度: ${similarity.toFixed(4)}`);
console.log(`[Result] 百分比: ${(similarity * 100).toFixed(1)}%`);
console.log("==========================================");
}
// 运行
fullPipeline(
"LLM Tokenization explained by Andrej Karpathy",
"Andrej Karpathy 讲解大模型分词技术"
);
八、应用场景
掌握了 Tokenization + Embedding + 相似度这条流水线,你可以落地很多实用功能:
| 应用场景 | 核心原理 |
|---|---|
| 🔍 语义搜索 | 将查询和文档都向量化,找余弦相似度最高的 |
| 📊 文本聚类 | 将相似文本的向量聚在一起(K-Means 等算法) |
| 🏷️ 文本分类 | 基于向量距离做零样本分类,无需额外训练 |
| 🌐 跨语言检索 | 中英文问句用同一 Embedding 模型编码后直接匹配 |
| 🚫 内容去重 | 相似度超过阈值则视为重复内容 |
| 💬 RAG 问答 | 用户问题向量化 → 检索最相关文档片段 → 拼接后喂给 LLM |
这些都是目前 AI 应用开发中非常成熟和常见的技术模式。
九、关键要点总结
Tokenization 是 LLM 的入口------它将人类文字翻译成模型能理解的数字,也是计费和上下文管理的基础。
三个环节的分工:
| 环节 | 核心工具/算法 | 输入 | 输出 |
|---|---|---|---|
| 分词 | tiktoken (BPE, cl100k_base) |
文本字符串 | Token ID 数组 |
| 向量化 | Embedding API (text-embedding-v4) |
文本字符串 | 高维浮点向量 |
| 相似度 | 余弦相似度公式 | 两个向量 | -1 到 1 的标量 |
记忆三件事:
- BPE 是主流分词算法:通过统计字符对频率迭代合并构建子词词表,平衡了效率和覆盖面。
- Embedding 是语义的数学表达:将语言映射到高维空间,让"意思相近"变成"距离相近",且天然支持跨语言。
- 余弦相似度是语义比较的标准工具:关注方向而非绝对大小,是向量检索的基石。
十、扩展阅读
- 📄 OpenAI tiktoken 源码
- 📺 Andrej Karpathy - Let's build GPT from scratch(Tokenization 章节极其经典)
- 📄 OpenAI Embeddings 官方文档
- 📄 Byte Pair Encoding 原论文