LLM 的底层语言:从分词到向量化,搞懂 AI 是怎么"读"文字的

LLM 的底层语言:从分词到向量化,搞懂 AI 是怎么"读"文字的

v029 搞懂了 Agent 的四块基石------LLM 是大脑,Tools 是手脚,Reasoning 是思考过程,Context 是记忆。

v030 搞懂了 Loop------不是写 Prompt,而是设计循环。Completion → Check → 退出或重试。

v031 搞懂了工程化------用 LangGraph 写 Agent,用 Supervisor 模式让多个 Agent 协作。

但这四天有一个共同的盲区:我们一直在"用" LLM,却从没问过它到底是怎么"读"文字的。

你发一段中文给 DeepSeek,它回复一段流畅的中文。但 DeepSeek 是一个神经网络------神经网络只能处理数字,看不懂中文、英文、任何文字。那它是怎么做到的?

arduino 复制代码
v031 ──→ v032 今天
工程化    回到底层
怎么写代码  AI 到底怎么"读"文字

今天的内容分两块,一块是算法思维的启蒙,一块是 AI 基础的认知:

  1. 递归与分治------一种把大问题拆成小问题的思维方式,快排是它的经典案例
  2. Token 与 Embedding------LLM 处理文字的两个核心步骤:先切词,再向量化

这两块看似无关,但底层逻辑是一样的:都是在拆解问题。 算法把排序问题拆成子数组,LLM 把一段文字拆成 token,再把 token 转成向量。拆得越细,处理得越好。

一、先聊算法:递归与分治

在进入 AI 之前,先建立一种思维方式------递归

递归是什么

递归的本质很简单:函数调用自己。

javascript 复制代码
// 求 1+2+3+...+n 的和
// 迭代的方式
function sum(n) {
  let total = 0
  for (let i = 0; i <= n; i++) {
    total += i
  }
  return total
}

迭代是"一步一步往前走"------从 1 加到 n,循环累加。这是大多数人最直觉的写法。

但换一种思路:

scss 复制代码
sum(5) = 5 + sum(4)
sum(4) = 4 + sum(3)
sum(3) = 3 + sum(2)
sum(2) = 2 + sum(1)
sum(1) = 1

要算 sum(5),先算 sum(4);要算 sum(4),先算 sum(3)......直到 sum(1),我们知道答案是 1。 然后一层一层返回去。

这就是递归------把一个大问题,拆成一个更小的同类问题,直到小到可以直接解决。

scss 复制代码
递归的两个要素:

  1. 递归公式:sum(n) = n + sum(n-1)
  2. 终止条件:sum(1) = 1

  缺任何一个,递归要么无限循环,要么无法启动

递归的实际应用:数组扁平化

递归不只是用来算数学题的。看一个实际场景------把嵌套数组展平

javascript 复制代码
const arr = [1, [2, [3, 4, 5]]]
// 目标:[1, 2, 3, 4, 5]

这个数组有多层嵌套,你不知道它嵌套了多少层。用迭代处理会非常麻烦------你需要循环、判断、再循环、再判断......

用递归,思路清晰得多:

javascript 复制代码
function flatten(arr) {
  let res = []
  for (let i = 0; i < arr.length; i++) {
    if (Array.isArray(arr[i])) {
      // 如果是数组,递归展平它
      res = res.concat(flatten(arr[i]))
    } else {
      // 如果是元素,直接放入结果
      res.push(arr[i])
    }
  }
  return res
}
scss 复制代码
flatten([1, [2, [3, 4, 5]]])

  遍历到 1 → 不是数组 → push(1)
  遍历到 [2, [3, 4, 5]] → 是数组 → 递归 flatten([2, [3, 4, 5]])
    遍历到 2 → push(2)
    遍历到 [3, 4, 5] → 是数组 → 递归 flatten([3, 4, 5])
      遍历到 3 → push(3)
      遍历到 4 → push(4)
      遍历到 5 → push(5)
    返回 [3, 4, 5]
  返回 [2, 3, 4, 5]
返回 [1, 2, 3, 4, 5]

递归的精髓:你不需要想清楚每一层的细节,只需要想清楚两件事------"当前层做什么"和"什么时候停"。 当前层:遍历数组,遇到元素就收集,遇到数组就递归。停止条件:数组遍历完了。

二、快速排序:分治策略的经典

理解了递归,就能理解分治(Divide and Conquer)------递归是代码的实现方式,分治是算法的设计策略。

为什么需要快排

先看三种基础排序:

scss 复制代码
冒泡排序:两两相邻比较,大的往后冒泡
选择排序:每次选最小的,放到前面
插入排序:从当前位置往前找,插入到正确位置

三种都是 O(n²)------数据量翻倍,耗时翻四倍

O(n²) 在数据量小的时候无所谓,但数据量一大就扛不住了。 100 万条数据,O(n²) 需要 1 万亿次操作------现代计算机也得跑好几秒。

快速排序是 O(n log n)------同样的 100 万条数据,只需要约 2000 万次操作。快了 5 万倍。

快排的核心思想

复制代码
核心思想:选一个基准值(pivot),把数组分成两部分

  比 pivot 小的 → 放左边
  比 pivot 大的 → 放右边
  
  然后对左边和右边分别递归执行同样的操作
  直到每个子数组只剩一个元素

举个例子:

ini 复制代码
原始数组: [2, 4, 1, 0, 3, 5]

选第一个元素 2 作为 pivot

第一轮分区:
  比 2 小的: [1, 0]
  pivot:    [2]
  比 2 大的: [4, 3, 5]

递归处理左边 [1, 0]:
  选 1 作为 pivot
  比 1 小的: [0]
  pivot:    [1]
  比 1 大的: []
  → 左边排好了: [0, 1]

递归处理右边 [4, 3, 5]:
  选 4 作为 pivot
  比 4 小的: [3]
  pivot:    [4]
  比 4 大的: [5]
  → 右边排好了: [3, 4, 5]

合并: [0, 1] + [2] + [3, 4, 5] = [0, 1, 2, 3, 4, 5]

分而治之------这就是分治策略。 分:把大数组拆成小数组。治:每个小数组排序。合:拼回大数组。

partition 函数:快排的核心

快排的关键在 partition 函数------它负责"分区",把数组分成"比 pivot 小"和"比 pivot 大"两部分。

javascript 复制代码
function partition(nums, left, right) {
  let i = left, j = right
  while (i < j) {
    // 从右往左找,找到第一个比 pivot 小的
    while (i < j && nums[j] >= nums[left]) {
      j--
    }
    // 从左往右找,找到第一个比 pivot 大的
    while (i < j && nums[i] <= nums[left]) {
      i++
    }
    // 交换这两个元素
    [nums[i], nums[j]] = [nums[j], nums[i]]
  }
  // 把 pivot 放到正确的位置
  [nums[left], nums[i]] = [nums[i], nums[left]]
  return i
}
css 复制代码
双指针的工作过程:

  [2, 4, 1, 0, 3, 5]
   ↑pivot     j→  ←i

  i 从左往右找比 pivot 大的
  j 从右往左找比 pivot 小的
  找到后交换

  最终 i 和 j 相遇的位置,就是 pivot 的正确位置

这个函数的精髓是"原地交换"------不需要开辟新数组,直接在原数组上操作。 这就是为什么快排的空间复杂度是 O(log n)(递归栈),而不是 O(n)(新数组)。

快排的递归实现

javascript 复制代码
function quickSort(nums, left, right) {
  if (left >= right) return        // 终止条件:子数组只有 0 或 1 个元素

  let pivot = partition(nums, left, right)  // 分区
  quickSort(nums, left, pivot - 1)          // 递归排左边
  quickSort(nums, pivot + 1, right)         // 递归排右边
}

三行核心代码。 partition 做"治",两个 quickSort 做"分"。递归自动完成了"分 → 治 → 合"的全部过程。

快排为什么不稳定

less 复制代码
稳定性:相等元素的相对顺序是否会被打乱

  原始: [2a, 2b, 1]  (2a 和 2b 值相同,但 a 在 b 前面)
  
  快排后: [1, 2b, 2a]  (b 跑到 a 前面了)
  
  → 不稳定

快排在分区过程中,相等元素可能被交换到不同侧,导致相对位置颠倒。 这在大多数场景下无所谓,但在需要稳定排序的场景(比如按多个字段排序)下,需要选择归并排序等稳定算法。

三、Token:LLM 的最小语言单位

聊完算法,进入今天的重头戏------LLM 到底怎么处理文字。

为什么 LLM 不能直接读文字

arduino 复制代码
计算机的底层:

  CPU 执行的是指令 → 指令操作的是二进制数
  一切都是 0 和 1

神经网络的底层:

  输入层接收的是向量(一串数字)
  中间层做的是矩阵乘法(数字运算)
  输出层给出的是概率分布(数字)

  从头到尾,没有"文字"这个东西

神经网络只能处理数字------这是由计算机的底层运行机制和模型训练的效率决定的。 不是 LLM 的设计选择,而是物理约束。

那 LLM 怎么处理中文、英文?答案是:先把文字转成数字。

Token:文字的数字身份

arduino 复制代码
文本 → 分词器(Tokenizer)→ Token ID → 模型处理

"Hello"    → [15339]
"世界"     → [73112, 99842]
"人工智能" → [20668, 100653]

Token 是 LLM 计价和工作的最小单位。 一个英文字符大约 0.3 个 token,一个中文字符大约 0.6 个 token。你每次调用 LLM API,输入的 prompt 和输出的回复,都是按 token 计费的。

arduino 复制代码
为什么中文比英文"贵"?

  英文: "Hello World" → 2 tokens
  中文: "你好世界"  → 4 tokens(大约)
  
  训练数据以英文为主 → 英文词表更大 → 英文编码更紧凑
  中文在词表中占比小 → 需要更多 token 来表达同样的语义

分词的原理:BPE

LLM 用的分词算法叫 BPE(Byte Pair Encoding,字节对编码)。核心思想很简单:

arduino 复制代码
1. 从单个字符开始
2. 统计哪些字符经常一起出现
3. 把经常出现的字符对合并成一个新 token
4. 重复步骤 2-3,直到词表大小达到上限

例如:
  "lower" 最初被拆成 ['l', 'o', 'w', 'e', 'r']
  统计发现 'lo' 经常一起出现 → 合并成 'lo'
  统计发现 'low' 经常一起出现 → 合并成 'low'
  最终 'lower' 可能变成 ['low', 'er'] 两个 token

BPE 的好处:高频词会被合并成一个 token(效率高),低频词会被拆成多个 token(不会丢失信息)。 这是一种自动平衡------常见的表达紧凑,罕见的表达冗长。

动手实操:js-tiktoken

javascript 复制代码
import { getEncoding } from 'js-tiktoken'

// 使用 GPT 官方的 token 编码器
const enc = getEncoding('cl100k_base')

const text = "Hello tiktoken! 你好, 世界"

// 编码:文本 → Token IDs
const tokens = enc.encode(text)
console.log("Token IDs:", tokens)
// [9906, 261, 16718, 4797, 0, 57668, 11, 99842]

// 解码:Token IDs → 文本
const decodedText = enc.decode(tokens)
console.log("Decoded Text:", decodedText)
// "Hello tiktoken! 你好, 世界"
ini 复制代码
cl100k_base 是什么?

  cl  = code language(代码语言)
  100k = 100K 词表大小
  base = 基础版本

  这是 GPT-3.5/4 使用的编码器
  它定义了"哪些字符组合算一个 token"的规则

编码和解码是可逆的。 encode 把文字切成 token ID 列表,decode 把 token ID 列表还原成文字。中间的 token ID 列表,就是 LLM 真正"看到"的东西。

token 的本质

arduino 复制代码
token 不是单词,token 不是字符,token 是------

  "模型词表中的一个条目"

  "hello"     → 1 个 token(常见词,整个词在词表中)
  "tokenization" → 可能是 3 个 token(不太常见,被拆成 token + ization + ...)
  "你好"      → 2 个 token(中文字符在词表中占比小)
  
  一个 token 对应一个 Token ID(一个整数)
  Token ID 是词表中的索引号

关键认知:token 和"词"不是一一对应的。 一个英文单词可能是一个 token,也可能是多个 token;一个中文字通常是 1-2 个 token。分词规则由 cl100k_base 等编码器决定,不是你能控制的。

四、Embedding:从符号到语义

Token ID 只是一个离散的符号------99069907 之间没有任何语义关系。但 LLM 需要理解语义------"开心"和"高兴"应该比"开心"和"汽车"更接近。

这就是 Embedding 的作用:把离散的 token ID 转换成连续的向量,让语义关系可以用数学计算。

什么是 Embedding

arduino 复制代码
Token ID: 离散符号
  9906 → "Hello"
  57668 → "你好"
  
  9906 和 57668 之间没有任何数学关系
  它们只是词表中的索引号

Embedding 向量: 连续数值
  9906 → [0.12, -0.34, 0.56, ..., 0.78]  (1024 维)
  57668 → [0.15, -0.31, 0.52, ..., 0.81]  (1024 维)
  
  两个向量之间的余弦相似度 ≈ 0.95(非常接近)
  因为 "Hello" 和 "你好" 语义相似

Embedding 把 token 从一个"编号"变成了一个"坐标"。 在这个 1024 维的向量空间中,语义相近的词会聚在一起,语义不同的词会离得很远。

1024 维向量空间

arduino 复制代码
为什么是 1024 维?

  二维空间只能表达两个维度的关系(x 轴和 y 轴)
  三维空间能表达三个维度的关系
  
  1024 维空间能表达 1024 个维度的"语义特征"
  
  这些维度不是人类能理解的"特征"(比如"情感色彩""正式程度")
  而是模型在训练过程中自动学到的抽象特征
  
  维度越高,能表达的语义越细腻
  但计算成本也越高

动手实操:Embedding 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',
})

async function getEmbedding(text) {
  const res = await client.embeddings.create({
    model: 'text-embedding-v4',
    input: text,
    dimension: 1024,  // 输出 1024 维向量
  })
  return res.data[0].embedding
}

async function run() {
  const text1 = "Andrej Karpathy LLM Tokenization 分词原理"
  const text2 = "卡帕西讲解大模型BPE字词分词"

  const vec1 = await getEmbedding(text1)
  const vec2 = await getEmbedding(text2)

  console.log(vec1.length)  // 1024
  console.log(vec1.slice(0, 5))  // [0.12, -0.34, 0.56, ...]
}
arduino 复制代码
text1 和 text2 的关系:

  text1: "Andrej Karpathy LLM Tokenization 分词原理"
  text2: "卡帕西讲解大模型BPE字词分词"

  文本匹配?完全不同------一个是英文为主,一个是中文为主
  语义匹配?非常接近------都在讲 LLM 分词
  
  这就是 Embedding 的价值:
    文本匹配找不到的相似性,Embedding 可以找到

Embedding 的应用场景

markdown 复制代码
Embedding 能做什么?

  1. 语义搜索
     用户问: "怎么学习 AI?"
     知识库中有 100 篇文章
     → 把问题和每篇文章都转成向量
     → 找余弦相似度最高的几篇
     → 返回给 LLM 作为上下文(RAG)

  2. 文本分类
     把文本转成向量 → 在向量空间中聚类
     语义相近的文本自然聚在一起

  3. 推荐系统
     用户喜欢的文章 → 转成向量
     找向量空间中最近的其他文章 → 推荐给用户

  4. 去重
     两段文字表面不同但语义相同 → 向量几乎重合 → 去重

Embedding 是 RAG 的基础。 v031 讲的 Context Engineering,核心就是"让 AI 看到它需要看到的信息"。Embedding 让你能在海量知识中,快速找到和当前问题最相关的几条------这就是"检索增强生成"的"检索"部分。

五、完整的文本处理管线

把 Token 和 Embedding 串起来,就是 LLM 处理文字的完整管线:

ini 复制代码
用户输入: "怎么学习大模型?"
            │
            ▼
      ┌─────────────┐
      │  Tokenizer   │  分词器
      │  (cl100k_base)│
      └──────┬──────┘
             │
             ▼
  Token IDs: [73112, 99842, 20668, 100653, 30]
             │
             ▼
      ┌─────────────┐
      │  Embedding   │  向量化
      │  (1024 维)   │
      └──────┬──────┘
             │
             ▼
  Vectors: [[0.12, -0.34, ...], [0.15, -0.31, ...], ...]
             │
             ▼
      ┌─────────────┐
      │  Transformer │  模型处理
      │  (Attention) │
      └──────┬──────┘
             │
             ▼
  输出 Token IDs: [20668, 100653, 113577, ...]
             │
             ▼
      ┌─────────────┐
      │   Decoder    │  解码器
      └──────┬──────┘
             │
             ▼
  输出文本: "学习大模型可以从以下几个方面入手..."
csharp 复制代码
每一步的作用:

  Tokenizer(分词器)
    文本 → Token IDs
    把文字切成模型能处理的最小单位
    "怎么学习大模型" → [73112, 99842, 20668, 100653, 30]

  Embedding(向量化)
    Token IDs → 向量
    把离散符号转成连续数值,保留语义关系
    [73112, ...] → [[0.12, -0.34, ...], ...]

  Transformer(模型处理)
    向量 → 向量
    通过 Attention 机制理解 token 之间的关系
    "学习"和"大模型"之间的语义关联

  Decoder(解码器)
    输出 Token IDs → 文本
    把模型输出的数字还原成可读的文字

输入的是 prompt 文本,输出的是 completion 文本。但模型处理的全程都是数字。 Token 和 Embedding 是文字世界和数字世界之间的桥梁。

一个容易混淆的点

yaml 复制代码
Token ≠ Embedding

  Token: 文本被切割后的离散符号 ID
    "Hello" → 9906(一个整数)
    它是词表中的索引号,没有语义

  Embedding: Token 被向量化后的连续数值
    9906 → [0.12, -0.34, 0.56, ...](1024 个浮点数)
    它是语义的数学表达,有距离和方向

  Token 是"身份证号",Embedding 是"三维坐标"
  知道身份证号,你只能查到这个人叫什么
  知道坐标,你能算出他离谁近、离谁远

六、算法思维与 AI 基础的底层连接

今天的内容看似是两个独立的主题------算法(递归/快排)和 AI(Token/Embedding)。但它们的底层逻辑是相通的。

都是"拆解"

ini 复制代码
递归/分治:
  大问题 → 拆成小问题 → 解决小问题 → 合并结果

  快排: [2,4,1,0,3,5] → [1,0] [2] [4,3,5] → 排序 → 合并
  扁平化: [1,[2,[3,4,5]]] → 递归展平每一层 → 合并

Tokenization:
  长文本 → 拆成 token → 模型逐个处理 → 生成新 token → 拼回文本

  "怎么学习大模型" → [73112, 99842, 20668, 100653, 30]
  → 模型处理 → [20668, 100653, 113577, ...] → "学习大模型可以从..."

Embedding:
  一个高维语义 → 拆成 1024 个维度 → 每个维度一个数值

  "开心" → [0.8, 0.2, -0.1, ..., 0.5](1024 个特征)

递归把大问题拆成小问题,Tokenization 把长文本拆成短 token,Embedding 把复杂语义拆成多维数值。 拆解是处理复杂性的通用策略。

都是"抽象"

arduino 复制代码
递归的抽象:
  不需要想清楚每一层的细节
  只需要定义"当前层做什么"和"什么时候停"
  剩下的交给递归自动处理

Embedding 的抽象:
  不需要人工定义"什么是语义相似"
  只需要给模型足够多的数据
  模型自动学到"哪些词意思接近"

两者的共同点:
  把复杂问题抽象成简单的规则/数学表达
  让机器自动处理细节

为什么程序员需要理解这些

markdown 复制代码
你可能不会自己写分词器
你可能不会自己训练 Embedding 模型
但你需要理解它们的原理,因为:

  1. 计费
     Token 是 LLM 的计价单位
     不懂 token,你就不知道一次对话要花多少钱
     中文比英文贵(0.6 vs 0.3 token/字),长对话越来越贵

  2. 性能优化
     上下文窗口是有限的(128K tokens)
     懂 token,你才能控制 prompt 的长度
     懂 Embedding,你才能设计高效的 RAG 系统

  3. 调试
     LLM 输出了奇怪的内容?
     可能是分词把你的关键词切碎了
     懂 token,你能快速定位问题

  4. 架构设计
     RAG、语义搜索、文本分类------都依赖 Embedding
     不懂 Embedding,你就不知道该选什么模型、多少维度

结语

今天从两个看似无关的领域------算法和 AI------学到了同一个道理:复杂性的解法是拆解。

yaml 复制代码
递归拆解的是问题:
  大问题 → 小问题 → 更小的问题 → 直到可以直接解决

快排拆解的是数组:
  大数组 → 小数组 → 更小的数组 → 直到只剩一个元素

Tokenization 拆解的是文本:
  长文本 → 短 token → 数字 ID

Embedding 拆解的是语义:
  复杂语义 → 1024 个维度 → 每个维度一个数值
vbnet 复制代码
v029 ──→ v030 ──→ v031 ──→ v032
是什么    怎么跑    怎么写    底层原理

Agent    Loop     工程化    Token/Embedding
概念      自动化    代码实现   AI 怎么读文字

知道"Agent = LLM+Tools"  →  知道"Loop = gen+check+stop"  →  知道"用 LangGraph 写出来"  →  知道"LLM 底层怎么处理文字"

从工程层下沉到基础层,不是开倒车------而是补地基。你不理解 Token,就无法优化 prompt 成本;你不理解 Embedding,就无法设计高效的 RAG 系统;你不理解递归和分治,就无法看懂 Agent 的递归调用链。

三十二天下来,从"AI 是什么"到"多智能体协作"再到底层的分词和向量化。认知不是一条直线------它更像是螺旋上升,每转一圈,底下的地基就更扎实一点。

Token 是 LLM 的字母表,Embedding 是 LLM 的语义地图。 搞懂了它们,你就真正理解了 AI 是怎么"读"文字的------不是用眼睛,而是用数学。

下篇见。

相关推荐
码哥字节5 小时前
Claude Code 准确率从 41% 升到 89%,这个 CLAUDE.md 只做了一件事
agent·ai编程·claude
Coffeeee5 小时前
帮你快速理解AI Agent之我想招个Android实习生
android·人工智能·agent
沉默王二5 小时前
Agent底层原理连问8道,从ReAct到记忆压缩,PaiCLI项目实战拆解
面试·agent·ai编程
把你拉进白名单5 小时前
8.OpenClaw源码解析——三层洋葱重试
人工智能·llm·agent
怕浪猫6 小时前
第4章 规划与推理:赋予Agent思考的能力
openai·agent·ai编程
米小虾6 小时前
多Agent系统编排详解:从架构设计到代码实现
人工智能·agent
米小虾6 小时前
多Agent系统的编排:架构、协议与企业级应用
人工智能·agent
To_OC16 小时前
搞懂 Token 和 Embedding 后,我终于明白大模型是怎么 "读" 文字的
人工智能·llm·agent
冬奇Lab18 小时前
Skill 系列(03):Skill 设计范式——5 个模式让输出从混沌到可预测
人工智能·开源·agent