从 Tokenization 到 Embedding:用 Node.js 搞懂大模型为什么先“分词”再“向量化”

很多人刚开始学习大模型时,会先从 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.750.80.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 压力
  • 让检索系统更精确地召回相关片段

但切块也不是越小越好。

切得太大,检索结果可能不精准;切得太小,语义上下文可能丢失。实际项目里通常要结合文档结构、段落长度、业务问题类型来调。

十二、学习路线建议

如果你是应用开发者,可以按这个顺序学习:

  1. 先用 AI 工具解决真实问题,比如写代码、总结资料、生成文案。
  2. 理解 Prompt、上下文窗口、输入 token、输出 token。
  3. 学 Tokenization,搞懂文本为什么会变成 token id。
  4. 学 Embedding,搞懂语义相似度、向量检索、RAG。
  5. 做一个完整作品,比如知识库问答、客户资料检索、文档助手或代码库问答。

这个顺序比一上来啃 Transformer 论文更适合大多数应用开发者。

总结

Tokenization 解决的是:

txt 复制代码
文本如何变成 token id

Embedding 解决的是:

txt 复制代码
文本如何变成可以计算语义相似度的向量

记住几个关键点:

  • token 不等于字符,也不等于自然语言里的词
  • 模型通常预测的是下一个 token,不是严格意义上的下一个词
  • cl100k_base 是一种 tokenizer 编码方案,不代表所有模型都使用它
  • Embedding 向量是高维浮点数组,维度取决于模型和参数
  • 余弦相似度可以比较语义接近程度,但阈值要结合业务数据调
  • RAG 的核心不是"把文档全塞给模型",而是"先检索相关片段,再让模型生成回答"
相关推荐
vanuan1 小时前
MCP协议实战(Java版):用Spring Boot让AI直接查你的数据库
人工智能
雪隐1 小时前
个人电脑玩AI-06让5060 Ti给你打工——不光能画画,Qwen3-TTS还能学人说话,连我老板都信了!
人工智能·后端·python
Coffeeee2 小时前
帮你快速理解AI Agent之我想招个Android实习生
android·人工智能·agent
新新技术迷2 小时前
AI聊天自动跟随滚动,附回到底部按钮
人工智能
先锋部队2 小时前
用Web Worker解析AI返回的大文本不卡UI
人工智能
把你拉进白名单2 小时前
8.OpenClaw源码解析——三层洋葱重试
人工智能·llm·agent
用户632415031782 小时前
拖文档进AI对话框解析,前端要处理哪些脏活
人工智能
姗姗来迟了2 小时前
AI回答里的引用来源卡片,前端怎么做
人工智能
用户7106207733402 小时前
Codex-端口配置错误排查案例(stream disconnected before completion)
人工智能