(余弦相似度...实战)从 Token 到 Embedding:用 JavaScript 理解大模型怎么“看懂”文本
最近在学 AI Agent,发现一个绕不开的概念:Token。
刚开始我以为 Token 只是“计费单位”,比如输入多少 token、输出多少 token、百万 token 多少钱。后来写了一点 demo 才发现,Token 不只是计费问题,它其实是大模型处理文本的第一步。
这篇文章用 JavaScript 做两个小实验:
- 用
js-tiktoken把文本编码成 token,再解码回文本 - 用 embedding 模型把文本变成向量,再计算两段文本的语义相似度

项目结构大概是这样:
Token
├── readme.md
└── demo
├── index.mjs
├── main.mjs
├── package.json
└── .env
index.mjs 用来演示 Token 编码和解码。
图中"Hello, tiktoken! 你好,世界!"被划为13个token
main.mjs 用来演示 Embedding 和余弦相似度。
余弦相似度(cos)越接近1,就说明意思越接近。
一、学习 LLM,为什么要先理解 Token
如果只是使用 ChatGPT、Claude、通义千问、豆包这类工具,可以不关心 Token。
但如果要做 AI 全栈、AI Agent、RAG、知识库、智能客服、代码助手,就必须理解 Token。
原因很直接:
用户输入的文本,不会被大模型直接理解。
大模型真正处理的是数字。
文本进入模型之前,会先经过 tokenizer,被切成一个个 token,然后映射成 token ID。
可以粗略理解为:
文本 -> token -> token ID -> embedding 向量 -> 模型计算 -> 输出 token -> 文本
完整一点的流程是:
Prompt 文本
↓
Tokenizer 分词器
↓
Token IDs
↓
Embedding 向量化
↓
Transformer 模型计算
↓
输出 Token IDs
↓
Decode 解码成文本
所以 Token 不是一个孤立概念,它连接了文本、计费、上下文长度、embedding、模型推理。
二、Token 到底是什么
Token 可以理解成大模型处理文本的最小单位。
但注意,Token 不一定等于“一个字”,也不一定等于“一个单词”。
比如英文里,一个单词可能被切成一个 token,也可能被切成多个 token。
中文里,一个汉字可能对应一个 token,也可能和周围字符组合成 token。
所以不要把 Token 简单理解成“字数”。
更准确的说法是:
Token 是 tokenizer 按照特定规则切出来的文本片段。
然后每个 token 会被映射成一个数字 ID。
大模型真正处理的是这些 ID 背后的向量表示。
三、为什么必须分词
计算机不能直接理解中文、英文、标点符号这些东西。
神经网络处理的是数字,通常是向量、矩阵这类数学对象。
所以模型不能直接吃这段文本:
你好,大模型
它需要先把文本转成数字。
大概过程是:
你好,大模型
↓
切成 token
↓
[token1, token2, token3]
↓
映射成 token ID
↓
[57668, 53901, 3922, ...]
这些 ID 再进入 embedding 层,变成高维向量。
这一步之后,模型才有办法用数学方式计算文本之间的关系。
四、Token 和计费、上下文长度的关系
我们平时调用大模型 API,经常会看到这些说法:
输入 token
输出 token
上下文窗口
百万 token 价格
这些都和 Token 有关。
一次 API 调用的总 token 通常可以理解为:
总 token = 输入 token + 输出 token
输入 token 就是你发给模型的 prompt、历史对话、系统提示词、工具结果等。
输出 token 就是模型生成的回答。
如果你做 AI Agent,这个点尤其重要。因为 Agent 往往会带很多上下文:
系统提示词
用户问题
历史对话
工具调用结果
RAG 检索片段
代码文件内容
中间思考过程或执行记录
这些都会占 token。
上下文塞得越多,成本越高,响应越慢,也更容易超过模型上下文限制。
五、项目依赖
package.json 里主要用了三个依赖:
{
"dependencies": {
"dotenv": "^17.4.2",
"js-tiktoken": "^1.0.21",
"openai": "^6.44.0"
}
}
分别作用如下:
dotenv:读取 .env 环境变量
js-tiktoken:在 JavaScript 中计算 token
openai:调用兼容 OpenAI SDK 的模型 API
安装方式:
pnpm install
或者:
pnpm i dotenv js-tiktoken openai
六、用 js-tiktoken 做 Token 编码和解码
先看 index.mjs。
import { getEncoding } from 'js-tiktoken';
const enc = getEncoding('cl100k_base');
const text = "Hello, tiktoken! 你好,世界!";
const tokens = enc.encode(text);
console.log("Token IDs:", tokens, tokens.length);
const decodedText = enc.decode(tokens);
console.log("Decode Text", decodedText);
这里最核心的是:
const enc = getEncoding('cl100k_base');
cl100k_base 是一种常见的 GPT 系列 tokenizer 编码规则。
可以把它理解成一张映射表:
文本片段 -> token ID
然后这行代码负责编码:
const tokens = enc.encode(text);
它会把文本转成 token ID 数组。
比如:
Hello, tiktoken! 你好,世界!
会被转成类似这样的数组:
[9906, 11, 87272, ...]
具体数字不重要,重要的是理解这个过程:
文本被 tokenizer 转成了模型能处理的数字 ID
接着这行代码负责解码:
const decodedText = enc.decode(tokens);
也就是把 token ID 再还原成文本。
所以 encode 和 decode 是一对操作:
encode:文本 -> token ID
decode:token ID -> 文本
七、为什么要自己统计 Token
如果只是聊天,可以不用管。
但写 AI 应用时,自己统计 Token 很有用。
比如做 RAG 知识库时,不能把整篇文档全部塞给模型。
需要先切片:
长文档 -> 多个 chunk -> 每个 chunk 控制 token 数
如果不统计 token,很容易出现两个问题。
第一个问题是超过上下文长度。
比如模型最多支持一定上下文,你却塞了太多文档进去,请求可能直接失败。
第二个问题是成本失控。
用户问一个简单问题,你却把几十页文档全塞进去,效果不一定更好,但 token 费用一定更高。
所以工程里经常需要做这些事:
统计 prompt token
限制单个 chunk token
控制历史对话长度
按 token 截断文本
预估 API 调用成本
js-tiktoken 就适合做这类事情。
八、Token 之后是什么:Embedding
Token ID 只是离散数字。
比如:
[9906, 11, 87272]
这些数字本身还不直接表示语义。
模型还需要把 token ID 转成向量。
这个过程就是 embedding。
Embedding 可以理解成:
把文本变成一串高维数字
比如一个文本被转成 1024 维向量:
[ 0.012, -0.034, 0.156, ...]
这些数字不是随便来的,而是模型学到的语义表示。
如果两段文本语义相近,它们的 embedding 向量在空间中的方向通常也会更接近。
九、调用 Embedding 模型
再看 main.mjs。
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',
});
这里用了 openai SDK,但 baseURL 指向的是阿里云百炼 DashScope 的兼容模式接口:
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1'
API Key 放在 .env 里:
DASHSCOPE_API_KEY=你的密钥
注意:密钥不要提交到 GitHub。
然后封装一个获取 embedding 的函数:
async function getEmbedding(text) {
const res = await client.embeddings.create({
model: 'text-embedding-v4',
input: text,
dimensions: 1024
});
return res.data[0].embedding;
}
这段代码做了三件事:
传入一段文本
调用 embedding 模型
返回这段文本对应的向量
这里指定了:
dimensions: 1024
表示希望返回 1024 维向量。
十、什么是语义相似度
有了 embedding 之后,就可以比较两段文本是不是语义接近。
比如这两句话:
Andrej Karpathy LLM Tokenization 分词原理
卡帕西讲解大模型 BPE 字符分词
它们表面文字不完全一样。
但语义上很接近,都在说 Karpathy、大模型、Tokenization、分词。
再看第三句话:
今天天气晴朗,适合出门运动
这句话和前两句明显不是一个主题。
人能看出来这个区别,embedding 也可以通过向量相似度体现出来。
十一、余弦相似度
项目里用了余弦相似度:
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));
}
它计算的是两个向量方向是否接近。
可以粗略理解为:
结果越接近 1,语义越相似
结果越接近 0,语义越不相关
结果越接近 -1,方向越相反
在文本 embedding 场景里,我们更常用它判断两段文本是否语义接近。
余弦相似度公式是:
cosine(A, B) = A · B / (|A| * |B|)
代码里的 dot 是点积:
dot += vecA[i] * vecB[i];
magA 和 magB 是两个向量长度的平方累加:
magA += vecA[i] ** 2;
magB += vecB[i] ** 2;
最后除以两个向量的模:
return dot / (Math.sqrt(magA) * Math.sqrt(magB));
十二、完整运行逻辑
run 函数大概是这样:
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(vec1);
console.log(vec1.length);
console.log("相似度:", cosineSimilarity(vec1, vec2));
console.log("相似度:", cosineSimilarity(vec1, vec3));
}
run();
这里预期会看到:
vec1.length = 1024
说明 embedding 返回的是 1024 维向量。
然后比较:
text1 和 text2 的相似度
text1 和 text3 的相似度
正常情况下,text1 和 text2 的相似度应该更高。
因为它们都在讨论大模型分词。
而 text3 是天气和运动,主题完全不同,相似度应该更低。
十三、这个 demo 和 RAG 有什么关系
这其实已经是 RAG 的基础了。
RAG 的核心流程可以简化为:
文档切片
↓
每个切片生成 embedding
↓
用户问题生成 embedding
↓
计算问题和文档切片的相似度
↓
取最相似的几个切片
↓
塞给大模型生成回答
也就是说,RAG 不是简单地把所有资料都丢给大模型。
它会先用 embedding 做语义检索。
比如用户问:
Tokenization 为什么重要?
系统会把这个问题转成向量。
然后去文档库里找语义最接近的片段。
找到之后,再把这些片段作为上下文交给大模型。
所以这个 demo 虽然很小,但它已经覆盖了 RAG 里最关键的一块:
文本 -> embedding -> 相似度计算
十四、Token、Embedding、LLM 的关系
可以用一条链路串起来:
文本
↓
Tokenizer
↓
Token IDs
↓
Embedding
↓
向量表示
↓
Transformer
↓
输出 Token
↓
Decode 成文本
Tokenization 解决的是:
怎么把文本切成模型能处理的基本单位
Embedding 解决的是:
怎么把 token 或文本变成带有语义信息的向量
LLM 解决的是:
基于上下文预测接下来应该生成什么
这几个概念连起来,才是大模型真正工作的样子。
十五、我对学习 AI Agent 的理解
如果要学 AI Agent,不建议一上来就堆框架。
比如一开始就冲 LangChain、AutoGen、CrewAI、各种 Agent 框架,很容易只会调 API,不知道底层发生了什么。
更好的路径是先把这些基础概念吃透:
Prompt 是什么
Token 是什么
上下文窗口是什么
Embedding 是什么
相似度检索是什么
Function Calling / Tool Calling 是什么
RAG 是什么
Agent 为什么需要工具和记忆
然后再去做项目。
比如:
个人知识库问答
代码仓库问答助手
简历优化 Agent
AI 学习笔记助手
客服知识库机器人
网页内容总结工具
这些项目都离不开 Token 和 Embedding。
十六、容易踩的坑
1. 把 token 当成字数
Token 不是字数。
中文、英文、标点、空格、代码都会影响 token 数。
所以要用 tokenizer 工具实际统计。
2. 忽略输入 token
很多人只关注模型输出花了多少 token。
但实际成本里,输入 token 也很重要。
尤其是 RAG 和 Agent,输入上下文经常很长。
3. 把 embedding 当成模型回答
Embedding 不负责生成回答。
它负责把文本转成向量,方便做检索、聚类、推荐、相似度计算。
真正生成自然语言回答的,是聊天模型或文本生成模型。
4. 直接把密钥写进代码
不要这样写:
apiKey: "sk-xxxx"
应该放到 .env:
DASHSCOPE_API_KEY=你的密钥
然后代码里读取:
apiKey: process.env.DASHSCOPE_API_KEY
5. 只看文字是否相同,不看语义是否相近
Embedding 的价值就在这里。
两段文本可以字面不同,但语义相近。
比如:
Tokenization 分词原理
BPE 字符分词机制
字面不完全一样,但主题很接近。
十七、总结
这次 demo 其实就做了两件事。
第一,用 js-tiktoken 理解 Token:
文本 -> token ID -> 文本
第二,用 embedding 理解语义相似度:
文本 -> 向量 -> 余弦相似度
如果把 AI 应用比作一栋楼,Token 和 Embedding 就是地基。
不理解 Token,就很难控制上下文、成本和模型输入。
不理解 Embedding,就很难理解 RAG、知识库、语义搜索和 Agent 记忆。
所以这两个概念值得认真写代码跑一遍。
代码不复杂,但能帮我们把“大模型到底怎么处理文本”这件事看得更清楚。
更多推荐


所有评论(0)