很多人刚开始学习大模型时,会先从 Prompt、Agent、RAG、Vibe Coding 这些更直观的东西入手。这没有问题,AI 工具本来就应该先用起来。
但如果想真正理解下面这些问题,就必须补上两个基础概念:
- 为什么大模型按 token 计费?
- 为什么上下文窗口不是按字数算?
- 为什么长文档不能一股脑塞进 Prompt?
- 为什么 RAG 要先做 Embedding?
- 为什么"关键词相同"和"语义相似"不是一回事?
这两个基础概念就是:
txt
Tokenization:把文本切成 token,并映射成 token id
Embedding:把文本映射成高维向量,用数学方式表达语义
csharp
下面用两个 Node.js 示例把它们讲清楚。
## 环境准备
需要安装 3 个依赖:
```bash
pnpm add js-tiktoken openai dotenv
package.json 中的依赖大概是这样:
json
{
"dependencies": {
"dotenv": "^17.4.2",
"js-tiktoken": "^1.0.21",
"openai": "^6.44.0"
}
}
如果要调用 Embedding 接口,还需要准备 .env:
env
DASHSCOPE_API_KEY=你的_API_Key
注意:.env 不要提交到 Git 仓库。真实 API Key 一旦泄露,应该立刻去平台控制台轮换或删除。
一、为什么大模型要先做 Tokenization
大模型不能直接处理中文、英文、标点、空格这些原始字符串。
从工程视角看,模型最终处理的是数字。文本进入模型前,需要先经过 tokenizer,被转换成一串离散编号,也就是 token ids。
普通文本生成流程可以简化成这样:
txt
文本 Prompt
-> Tokenizer
-> token ids
-> 模型内部的向量表示
-> Transformer 计算
-> 输出 token ids
-> 解码成文本
对于常见的自回归语言模型来说,它的核心训练目标可以粗略理解为:
txt
根据前面的 token,预测下一个 token
注意,这里说的是"下一个 token",不是严格意义上的"下一个词"。
因为 token 不一定等于自然语言里的一个词。它可能是一个完整英文单词,也可能是单词的一部分,可能是一个中文字符,也可能是多个中文字符,还可能是标点、空格、换行或代码符号。
二、用 js-tiktoken 看看文本如何变成 token
下面是一个最小可运行示例:
js
import { getEncoding } from "js-tiktoken";
const enc = getEncoding("cl100k_base");
const text = "Hello tiktoken! hello world";
const tokens = enc.encode(text);
const decodedText = enc.decode(tokens);
console.log("原始文本:", text);
console.log("Token IDs:", tokens);
console.log("Token 数量:", tokens.length);
console.log("解码结果:", decodedText);
可能输出:
txt
原始文本: Hello tiktoken! hello world
Token IDs: [ 9906, 87272, 5963, 0, 24748, 1917 ]
Token 数量: 6
解码结果: Hello tiktoken! hello world
这里最关键的点是:
txt
"Hello tiktoken! hello world"
被 tokenizer 转成了:
txt
[9906, 87272, 5963, 0, 24748, 1917]
也就是说,模型真正处理的不是原始字符串,而是 token id 数组。
三、cl100k_base 是什么
示例里用的是:
js
const enc = getEncoding("cl100k_base");
cl100k_base 是一种 tokenizer 编码方案,可以理解为一套 token 和 token id 的映射规则。
这里要特别注意:
txt
cl100k_base 不等于所有大模型的 tokenizer
它可以用来学习 tokenization,也常用于估算部分 OpenAI 模型的 token 数。但不同模型、不同厂商可能使用不同 tokenizer。
所以工程实践里要区分三件事:
- 学习原理:可以用
js-tiktoken - 粗略估算:尽量选目标模型对应的 tokenizer
- 精确计费:以 API 返回的 usage 或厂商账单为准
四、Token 为什么重要
Token 不是一个抽象概念,它直接影响真实开发。
第一,影响上下文长度。
大模型的上下文窗口通常按 token 计算,不是按字符数计算。系统提示词、用户输入、历史对话、工具调用结果、RAG 检索内容,都会消耗 token。
第二,影响 API 成本。
很多模型的计费方式可以简化理解为:
txt
输入 token + 输出 token = 总 token 消耗
第三,影响回答质量。
如果上下文塞得太满,模型可能看不到关键内容,或者被大量无关内容干扰。做 Agent、RAG、代码生成时,控制 token 是很实际的工程问题。
所以不要把 token 简单理解成"字数"。它更像是模型的输入输出预算单位。
五、Embedding 解决什么问题
Tokenization 解决的是:
txt
文本如何变成模型可以处理的离散编号
Embedding 解决的是另一个问题:
txt
文本如何变成可以计算语义相似度的向量
比如下面两句话:
txt
Andrej Karpathy LLM Tokenization 分词原理
卡帕西讲解大模型 BPE 字词分词
它们字面上并不一样,甚至中英文混杂,但语义上很接近。
再看这句:
txt
今天天气晴朗,适合出门散步
它和前两句主题明显不同。
如果只做字符串匹配,很难知道"Karpathy"和"卡帕西"有关,也很难知道"Tokenization"和"BPE 字词分词"有关。
Embedding 的价值就在这里:它把文本映射到高维向量空间,让我们可以用数学方法比较语义接近程度。
六、完整 Embedding 示例
下面是一个更适合学习和复习的完整版本:
js
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"
});
async function getEmbedding(text) {
const res = await client.embeddings.create({
model: "text-embedding-v4",
input: text,
dimensions: 1024
});
return res.data[0].embedding;
}
function cosineSimilarity(vecA, vecB) {
if (vecA.length !== vecB.length) {
throw new Error("Vectors must have the same length");
}
let dot = 0;
let magA = 0;
let magB = 0;
for (let i = 0; i < vecA.length; i++) {
dot += vecA[i] * vecB[i];
magA += vecA[i] ** 2;
magB += vecB[i] ** 2;
}
if (magA === 0 || magB === 0) {
return 0;
}
return dot / (Math.sqrt(magA) * Math.sqrt(magB));
}
async function run() {
const text1 = "Andrej Karpathy LLM Tokenization 分词原理";
const text2 = "卡帕西讲解大模型 BPE 字词分词";
const text3 = "今天天气晴朗,适合出门散步";
const [vec1, vec2, vec3] = await Promise.all([
getEmbedding(text1),
getEmbedding(text2),
getEmbedding(text3)
]);
console.log("向量维度:", vec1.length);
console.log("text1 vs text2:", cosineSimilarity(vec1, vec2));
console.log("text1 vs text3:", cosineSimilarity(vec1, vec3));
}
run().catch(console.error);
这段代码做了 4 件事:
- 调用 Embedding 模型,把文本转成 1024 维向量
- 用
Promise.all并发请求 3 段文本的向量 - 用余弦相似度比较两个向量的方向是否接近
- 对比"语义相近"和"语义不相关"的文本得分
正常情况下,text1 vs text2 应该比 text1 vs text3 更高。
七、代码要点拆解
先看客户端初始化:
js
const client = new OpenAI({
apiKey: process.env.DASHSCOPE_API_KEY,
baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1"
});
这里使用的是 openai SDK,但 baseURL 指向 DashScope 的 OpenAI 兼容接口。也就是说,SDK 的调用方式像 OpenAI,但实际请求发给阿里云 DashScope。
再看 Embedding 调用:
js
const res = await client.embeddings.create({
model: "text-embedding-v4",
input: text,
dimensions: 1024
});
关键参数有 3 个:
model:使用哪个 Embedding 模型input:要向量化的文本dimensions:希望返回的向量维度
返回值里真正需要的是:
js
res.data[0].embedding
它是一个浮点数数组,类似:
js
[
0.0123,
-0.0345,
0.0876
]
实际长度由 dimensions 决定。示例里设置为 1024,所以返回的是 1024 维向量。
八、余弦相似度是什么
拿到两个向量以后,怎么判断它们语义是否接近?
常见方法是余弦相似度:
txt
cosine_similarity(A, B) = A·B / (|A| * |B|)
直觉上,它比较的是两个向量的方向是否接近。
代码里这部分是在算点积:
js
dot += vecA[i] * vecB[i];
这部分是在算两个向量的模长:
js
magA += vecA[i] ** 2;
magB += vecB[i] ** 2;
最后得到相似度:
js
return dot / (Math.sqrt(magA) * Math.sqrt(magB));
一般可以粗略理解为:
- 越接近
1:语义越相似 - 接近
0:相关性弱 - 小于
0:方向相反,通常表示不相似
但业务里不要机械套阈值。不同模型、不同数据集、不同场景下,0.75、0.8、0.9 的含义可能完全不同。阈值要用自己的业务数据调。
九、Tokenization 和 Embedding 的关系
很多人会问:
txt
既然 Embedding 可以直接把文本变成向量,那还需要 Tokenization 吗?
需要。
因为模型仍然不能直接处理原始字符串。文本进入 Embedding 模型前,通常也会先经过 tokenizer。只是当我们调用 Embedding API 时,这一步被服务端封装了,开发者不一定直接看到。
可以这样理解:
txt
Tokenization:把文本变成离散 token ids
Embedding:把文本或 token 序列变成连续向量表示
一个偏编码,一个偏语义表达。
十、为什么 RAG 离不开 Embedding
RAG,全称 Retrieval-Augmented Generation,中文通常叫检索增强生成。
典型流程是:
txt
文档
-> 切分 chunk
-> 生成 embedding
-> 存入向量数据库
用户问题
-> 生成 query embedding
-> 向量相似度检索
-> 找到相关 chunk
-> 拼进 Prompt
-> 交给 LLM 生成回答
Embedding 在这里负责"找资料",LLM 负责"组织语言生成答案"。
如果只靠关键词匹配,会遇到很多问题:
- 用户说"卡帕西",资料里写的是
Karpathy - 用户说"大模型分词",资料里写的是
Tokenization - 用户问"怎么计算语义接近程度",资料里写的是
cosine similarity
这些场景里,字面不一定匹配,但语义向量可能能找回来。
十一、为什么长文档要切块
RAG 不是把所有文档都塞给大模型。
原因很直接:
txt
上下文窗口有限,token 成本也有限。
所以通常会先把文档切成 chunk,再分别生成 Embedding。
切块的价值有两个:
- 降低单次输入的 token 压力
- 让检索系统更精确地召回相关片段
但切块也不是越小越好。
切得太大,检索结果可能不精准;切得太小,语义上下文可能丢失。实际项目里通常要结合文档结构、段落长度、业务问题类型来调。
十二、学习路线建议
如果你是应用开发者,可以按这个顺序学习:
- 先用 AI 工具解决真实问题,比如写代码、总结资料、生成文案。
- 理解 Prompt、上下文窗口、输入 token、输出 token。
- 学 Tokenization,搞懂文本为什么会变成 token id。
- 学 Embedding,搞懂语义相似度、向量检索、RAG。
- 做一个完整作品,比如知识库问答、客户资料检索、文档助手或代码库问答。
这个顺序比一上来啃 Transformer 论文更适合大多数应用开发者。
总结
Tokenization 解决的是:
txt
文本如何变成 token id
Embedding 解决的是:
txt
文本如何变成可以计算语义相似度的向量
记住几个关键点:
- token 不等于字符,也不等于自然语言里的词
- 模型通常预测的是下一个 token,不是严格意义上的下一个词
cl100k_base是一种 tokenizer 编码方案,不代表所有模型都使用它- Embedding 向量是高维浮点数组,维度取决于模型和参数
- 余弦相似度可以比较语义接近程度,但阈值要结合业务数据调
- RAG 的核心不是"把文档全塞给模型",而是"先检索相关片段,再让模型生成回答"