阅读视图

发现新文章,点击刷新页面。

AI 的「词元」:Token 到底是什么?

一、Token 是什么?

Token,直译为「词元」或「令牌」,是大语言模型(LLM)处理文本的基本单位。

你输入的文字,在进入模型之前,会先经过一个叫做 Tokenizer(分词器) 的程序,把文本切碎成一个个 Token。这些 Token 再被转换为数字 ID,模型才能「读懂」它们。

一个 Token ≠ 一个字。Token 可以是:

  • 一个完整的英文单词:hello → 1 token
  • 一个英文词的一部分:unbelievableun + believ + able = 3 tokens
  • 一个中文汉字(有时): → 1 token
  • 一个标点符号:, → 1 token
  • 几个空格: → 1 token

二、Tokenizer 是怎么工作的?

目前主流的分词算法有三种:

1. BPE(Byte Pair Encoding,字节对编码)

最常见的方法,被 GPT 系列、LLaMA、Mistral 等广泛使用。

核心思想:从单个字节出发,反复合并出现频率最高的相邻字节对,直到达到预设词表大小。

举例

语料:"aaabdaaabac"
初始:a a a b d a a a b a c
第1轮合并最频繁的 "aa":aa a b d aa a b a c
第2轮合并 "aa":aaa b d aaa b a c
...
最终词表中就会有 "aa"、"aaa" 这样的合并单元

2. WordPiece

Google BERT 系列使用。与 BPE 类似,但合并标准是「最大化语言模型似然」,而非纯粹频率。

对中文的处理:mBERT 会将中文字符逐字切分,"你好"["你", "好"],基本保持 1 字 = 1 token。

3. SentencePiece

Google T5、mT5 使用,也被 LLaMA、BLOOM 等采用。

最大特点:不依赖语言的空格分词习惯,把原始文本当作字节流处理,天然支持中日韩等语言,无需预处理。


三、各大模型 Token 大比拼

同一句话,不同模型切法不同

我们拿这句话做实验:

「人工智能正在改变世界」

模型/分词器 大致 Token 数 说明
GPT-3(p50k) 约 14–18 个 旧版 BPE,中文多走 UTF-8 字节,1 字≈2-3 token
GPT-4 / GPT-4o(cl100k) 约 7–9 个 优化后的 BPE,CJK 词表扩充
Claude 3.x 约 7–10 个 Anthropic 自研 tokenizer,中文效率与 GPT-4 相近
LLaMA 2 约 10–14 个 SentencePiece,中文支持一般
LLaMA 3 约 7–9 个 词表从 32K 扩展到 128K,大幅改善 CJK
Qwen2.5 / 通义千问 约 7–8 个 针对中文优化的 BPE,接近 1 字 1 token
DeepSeek-V3 约 7–8 个 自研 tokenizer,中文友好
Gemini 1.5 约 8–11 个 Google SentencePiece 衍生,多语言均衡

结论:国产大模型(Qwen、DeepSeek)和经过优化的新版国际模型(GPT-4、LLaMA 3)对中文都相当友好,而早期 GPT-3 对中文极不友好——同样的内容要消耗多 2-3 倍的 token。


英文 Token 效率对比

用句子 "The quick brown fox jumps over the lazy dog" 来测试:

模型 Token 数
GPT-4o(cl100k) 9
Claude 3(Anthropic) 9
LLaMA 3(128K 词表) 9
GPT-3(p50k) 9
BERT(WordPiece) 10

英文差距不大,主要是常见单词基本都在词表里,直接 1 词 1 token。


四、Token 为什么重要?

1. 直接决定 API 费用

所有大模型的 API 计费都以 Token 为单位。

关键洞察:如果你的业务场景大量涉及中文,使用中文友好的模型(Qwen、DeepSeek)不仅价格低,而且 token 效率更高,双重节省!


2. 决定上下文窗口(Context Window)

所谓「上下文长度」,本质上就是模型一次能处理多少个 Token。

模型 上下文窗口
GPT-4o 128K tokens
Claude 3.5 Sonnet 200K tokens
Claude 3.7 Sonnet 200K tokens
Gemini 1.5 Pro 1M tokens(实验版 2M)
Gemini 2.0 Flash 1M tokens
LLaMA 3.1(70B) 128K tokens
DeepSeek-V3 128K tokens
Qwen2.5-72B 128K tokens

200K tokens ≈ 约 15 万汉字 ≈ 一部长篇小说。


3. 影响模型的「注意力范围」

Token 越多,模型计算的注意力(Attention)矩阵越大,计算量以平方级增长。这也是为什么长上下文模型推理慢、成本高的根本原因。


五、一个有趣的实验:数 Token

你可以亲自去 OpenAI 的 Tokenizer 工具 数数看。

下面是几个有趣的例子(GPT-5 分词器):

"Hello, world!"4 tokens
"你好,世界!"4 tokens
"1+1=2"5 tokens
"😀"1 tokens(emoji 用多个字节表示)
"GPT"1 token(常见词直接收录)
"ChatGPT"2 tokens:"Chat" + "GPT"
"Supercalifragilistic"6 tokens

六、Token 与中文的特殊关系

中文处理是大模型 tokenizer 设计中的一大挑战。

为什么早期模型对中文「不友好」?

早期 GPT-3 的 tokenizer 词表主要基于英文语料训练。中文汉字不在词表里,就会被拆成 UTF-8 字节来表示。一个中文字符在 UTF-8 编码下占 3 个字节,因此变成 3 个 token。

对比

"人工智能" (4个汉字)
  GPT-3:    8  token(每字≈2 token)
  GPT-4:    4-5  token(CJK 词表扩充)
  Qwen:     4  token(1字≈1 token)

这意味着:同样的中文内容,在 GPT-3 上的 token 成本是 Qwen 的 2 倍

国产模型的优势

以 Qwen(通义千问)为例,阿里在训练 tokenizer 时专门加入了大量中文语料,词表中收录了常见汉字和常用词组,实现了接近 1:1 的字-token 比例。

DeepSeek 同样如此,其 tokenizer 词表约 100K,中文字符基本都有专属 token。



总结

AI 在发展,token的计算也在不断优化,本文提到的token数仅供参考!

Token 是 AI 语言模型的「DNA」——一切理解与生成,都从这个最小单位开始。

维度 核心要点
是什么 文本被切分后的最小处理单元,≠ 字词
怎么切 BPE / WordPiece / SentencePiece 三大算法
中文效率 新模型(GPT-5、Qwen、DeepSeek)已接近 1字1token
为什么重要 决定 API 费用、上下文长度、推理速度
开发技巧 提前计算、精简 prompt、善用缓存、选合适模型

Three.js 场景完全入门指南:让你的 3D 场景不在乱成一团

场景图到底是什么?一句话说清楚

场景图 = 你 3D 世界里的「家族族谱」

你在 Three.js 里创建的每一个物体——立方体、球体、灯光、相机——它们不是孤立存在的,而是像家族成员一样,有爸爸、有儿子、有孙子,形成一个树状的层级结构。

这个结构,就叫场景图。

想象一下你在玩乐高:

  • 你先搭了一个车身(父节点)
  • 然后在车身上装了 4 个轮子(子节点)
  • 每个轮子上又装了轮毂装饰(孙节点)

当你拿起整个车身移动时,轮子和轮毂会自动跟着动。你不需要一个一个去移动它们。

这就是场景图的核心逻辑:父节点动,子节点自动跟着动。

02-concept-scenegraph.png

为什么需要场景图?

假设你要做一个太阳系模型:

  • 太阳在中心
  • 地球绕着太阳转
  • 月球绕着地球转

如果没有场景图,你得这么写:

// 每一帧都要手动计算位置
function animate() {
  // 地球绕太阳转
  earth.position.x = Math.cos(time) * 10;
  earth.position.z = Math.sin(time) * 10;

  // 月球绕地球转,还要加上地球的位置
  moon.position.x = earth.position.x + Math.cos(time * 2) * 2;
  moon.position.z = earth.position.z + Math.sin(time * 2) * 2;

  // 如果再加个火星、木星、土星...
  // 你的代码会变成一坨屎
}

有了场景图,你只需要:

// 把月球设为地球的子节点
earth.add(moon);

// 把地球设为太阳的子节点
sun.add(earth);

// 每一帧只需要旋转父节点
function animate() {
  sun.rotation.y += 0.01;  // 太阳自转
  earth.rotation.y += 0.02; // 地球自转,月球自动跟着转
}

场景图让你从「手动计算每个物体的绝对位置」,变成「只管理父子关系,让系统自动计算」。


场景图的三大核心规则

规则 1:每个物体都有自己的「局部坐标系」

这是最容易搞混的地方。

在 Three.js 里,每个物体的 positionrotationscale 都是相对于它的父节点的,不是相对于整个世界的。

举个例子:

const car = new THREE.Group(); // 汽车
const wheel = new THREE.Mesh(wheelGeometry, wheelMaterial); // 轮子

wheel.position.x = 2; // 轮子在汽车坐标系里,向右偏移 2 个单位
car.add(wheel);

car.position.x = 10; // 汽车在世界坐标系里,向右移动 10 个单位

此时,轮子在世界坐标系里的实际位置是 10 + 2 = 12

但你在代码里看到的 wheel.position.x 还是 2,因为它记录的是相对于父节点(汽车)的位置

这就像你在高铁上走动:

  • 你相对于车厢的位置是「第 5 排座位」(局部坐标)
  • 但你相对于地球的位置,是「第 5 排座位 + 高铁的位置」(世界坐标)

规则 2:父节点的变换会「传递」给所有子节点

这是场景图最强大的地方。

当你旋转、缩放、移动一个父节点时,它的所有子节点、孙节点、曾孙节点……都会跟着变。

const robot = new THREE.Group();
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
const leftArm = new THREE.Mesh(armGeometry, armMaterial);
const rightArm = new THREE.Mesh(armGeometry, armMaterial);

robot.add(body);
body.add(leftArm);
body.add(rightArm);

// 旋转机器人,整个机器人(包括身体和手臂)都会转
robot.rotation.y = Math.PI / 4;

// 旋转身体,手臂会跟着转,但机器人的腿不会动
body.rotation.x = Math.PI / 6;

这就像你转身:

  • 你的头、手、脚都会跟着转(子节点跟随父节点)
  • 但你手上拿的手机屏幕方向不会变(子节点保持自己的局部旋转)

规则 3:Scene 是所有节点的「根节点」

在 Three.js 里,Scene 就是那个最顶层的「祖宗节点」。

所有你想渲染出来的东西,都必须直接或间接地添加到 Scene 里。

const scene = new THREE.Scene();

// 方式 1:直接添加到场景
scene.add(cube);

// 方式 2:添加到某个组,再把组添加到场景
const group = new THREE.Group();
group.add(cube);
scene.add(group);

Scene 就像一个舞台:

  • 只有站在舞台上的演员(或演员团队)才能被观众(相机)看到
  • 你在后台准备的道具(没 add 到 scene 的物体),观众看不见

···

真实场景:用场景图管理一辆汽车

假设你要做一个可交互的汽车模型:

  • 汽车可以前进、后退、转弯
  • 4 个轮子要跟着车身动
  • 轮子转弯时要旋转
  • 车门可以单独打开

没有场景图的噩梦写法:

// 每次移动汽车,你要手动更新 5 个物体的位置
function moveCar(distance) {
  carBody.position.z += distance;
  wheel1.position.z += distance;
  wheel2.position.z += distance;
  wheel3.position.z += distance;
  wheel4.position.z += distance;
  door.position.z += distance;
}

// 转弯时,你要手动计算每个轮子的新位置
function turnCar(angle) {
  // 这里要写一堆三角函数...
  // 而且很容易算错
}

用场景图的优雅写法:

// 1. 创建层级结构
const car = new THREE.Group(); // 汽车根节点
const body = new THREE.Mesh(bodyGeometry, bodyMaterial); // 车身
const door = new THREE.Mesh(doorGeometry, doorMaterial); // 车门

const wheel1 = new THREE.Mesh(wheelGeometry, wheelMaterial);
const wheel2 = new THREE.Mesh(wheelGeometry, wheelMaterial);
const wheel3 = new THREE.Mesh(wheelGeometry, wheelMaterial);
const wheel4 = new THREE.Mesh(wheelGeometry, wheelMaterial);

// 2. 建立父子关系
car.add(body);
body.add(door); // 车门是车身的子节点
body.add(wheel1);
body.add(wheel2);
body.add(wheel3);
body.add(wheel4);

scene.add(car); // 整辆车添加到场景

// 3. 设置轮子的局部位置(相对于车身)
wheel1.position.set(-1, -0.5, 1.5);  // 左前轮
wheel2.position.set(1, -0.5, 1.5);   // 右前轮
wheel3.position.set(-1, -0.5, -1.5); // 左后轮
wheel4.position.set(1, -0.5, -1.5);  // 右后轮

// 4. 移动汽车,只需要操作根节点
function moveCar(distance) {
  car.position.z += distance; // 一行代码,整辆车都动了
}

// 5. 转弯,也只需要操作根节点
function turnCar(angle) {
  car.rotation.y += angle; // 一行代码,整辆车都转了
}

// 6. 打开车门,只操作车门节点
function openDoor() {
  door.rotation.y = Math.PI / 3; // 车门绕自己的轴旋转
}

// 7. 轮子转动,只操作轮子节点
function rotateWheels(speed) {
  wheel1.rotation.x += speed;
  wheel2.rotation.x += speed;
  wheel3.rotation.x += speed;
  wheel4.rotation.x += speed;
}

场景图让你的代码从「管理 100 个物体的绝对位置」,变成「管理 10 个父子关系」。

代码量少了 90%,bug 也少了 90%。

···

进阶技巧:Group 是你最好的朋友

Three.js 提供了一个专门用来组织场景图的工具:THREE.Group()

它就是一个「空节点」,自己不渲染任何东西,但可以作为其他物体的容器。

什么时候用 Group?

  1. 逻辑分组:把相关的物体放在一起

    const furniture = new THREE.Group();
    furniture.add(table);
    furniture.add(chair);
    furniture.add(lamp);
    
    // 一次性移动所有家具
    furniture.position.x = 5;
    
  2. 动画控制:需要整体旋转或移动时

    const solarSystem = new THREE.Group();
    solarSystem.add(sun);
    solarSystem.add(earth);
    solarSystem.add(mars);
    
    // 整个太阳系旋转
    solarSystem.rotation.y += 0.01;
    
  3. 坐标系转换:需要改变物体的旋转中心时

    // 默认情况下,物体绕自己的中心旋转
    // 如果你想让它绕另一个点旋转,可以用 Group
    
    const pivot = new THREE.Group();
    pivot.add(cube);
    cube.position.x = 5; // 立方体偏离 pivot 中心
    
    pivot.rotation.y += 0.01; // 立方体绕 pivot 中心旋转(公转)
    cube.rotation.y += 0.02;  // 立方体绕自己中心旋转(自转)
    

Group 就像乐高的底板:

  • 你可以在底板上搭建复杂的结构
  • 然后拿起整个底板移动,所有东西都跟着动
  • 底板本身不占空间,只是一个「组织工具」

···

常见坑点:为什么我的物体位置不对?

坑点 1:忘记父节点的变换会累积

const parent = new THREE.Group();
parent.scale.set(2, 2, 2); // 父节点放大 2 倍

const child = new THREE.Mesh(geometry, material);
child.scale.set(2, 2, 2); // 子节点也放大 2 倍
parent.add(child);

// 结果:子节点实际被放大了 2 × 2 = 4 倍!

解决方法:

  • 要么只在父节点设置缩放
  • 要么在子节点用 1 / parent.scale.x 来抵消

坑点 2:直接修改 world position 不生效

const child = new THREE.Mesh(geometry, material);
parent.add(child);

// ❌ 错误:直接设置世界坐标不会生效
child.position.set(10, 0, 0); // 这是局部坐标!

// ✅ 正确:如果要设置世界坐标,需要先转换
const worldPos = new THREE.Vector3(10, 0, 0);
child.parent.worldToLocal(worldPos);
child.position.copy(worldPos);

坑点 3:移除节点时忘记清理引用

// ❌ 错误:只从场景移除,但父子关系还在
scene.remove(child);

// ✅ 正确:从父节点移除
parent.remove(child);

// ✅ 更好:彻底清理
parent.remove(child);
child.geometry.dispose();
child.material.dispose();

核心代码与完整示例:      my-three-app

总结

如果你喜欢本教程,记得点赞+收藏!关注我获取更多Three.js开发干货

❌