中英文 token 数量差一倍?两段 JS 代码搞懂 LLM 底层是怎么"读"文字的

从 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(微调) 等更高级的话题。

如果这篇文章对你有帮助,欢迎点赞、收藏、关注!有疑问欢迎在评论区讨论。

相关推荐
山河木马1 小时前
矩阵专题1-怎么创建模型矩阵(uModelMatrix)
javascript·webgl·计算机图形学
Patrick_Wilson2 小时前
Squash Merge 的血缘陷阱:为什么删掉的代码又活了过来
前端·git·程序员
程序员cxuan4 小时前
GPT-5.6 还不发布?不过大家可以先看看 Codex 的白皮书。
人工智能·后端·程序员
白鲸开源4 小时前
一文读懂DolphinScheduler插件机制:如何轻松扩展任务类型与数据源
java·架构·github
棒槌开发师4 小时前
动态组件设计(elpis)
架构
Apifox4 小时前
从 Postman 迁移到 Apifox:Workspace、Collection、Environment 现在可以一起导入了
前端·后端·程序员
前端开发爱好者6 小时前
支持 110 种文件预览!兼容 Vue、React、Svelte!
前端·javascript·vue.js
陈随易7 小时前
VSCode古法神器fnMap v9开发故事
前端·后端·程序员
大家的林语冰8 小时前
👍 尤大重学 Webpack,Vite 8.1 再进化,打包模式复活!
前端·javascript·vite