从 Tokenizer 到 Embedding 再到 RAG —— 大模型是怎么”理解”并”查找”信息的?

前言

你有没有想过这些问题:

  • 大模型明明只认识数字,为什么它能理解”猫”和”狗”是相似的?
  • RAG 说”从知识库检索相关内容”,它是怎么知道哪些内容跟我的问题相似的?
  • Tokenizer 把文本切成 Token,Embedding 把 Token 变成向量——这两步到底有什么关系?

这篇文章会从最底层开始,把编码 → 向量化 → 相似搜索 → RAG 检索这条完整的链路讲清楚。

看完你会明白:为什么 Tokenizer 的分词质量直接影响 RAG 的检索准确性。


目录

  1. 编码的三层境界
  2. 第一层:Tokenizer —— 把文本切成块
  3. 第二层:Embedding —— 把 Token 变成向量
  4. 第三层:语义空间 —— 向量为什么能表示”相似”
  5. RAG 的完整检索流程
  6. Tokenizer 如何影响 RAG 的检索准确性
  7. 实践:一个 RAG 系统的编码链路
  8. 总结

1. 编码的三层境界

整个编码过程可以理解为三个层层递进的阶段:

1
2
3
4
5
6
7
8
9
10
自然语言                          大模型/向量数据库
"今天天气真好" [0.23, -0.45, ...]
│ ↑
│ ┌──────────┐ ┌──────────┐ │
├──│Tokenizer │───→│Embedding │───────┘
│ │ 分词编码 │ │ 向量化 │
│ └──────────┘ └──────────┘

["今天", "天气", "真好"]
[1024, 3056, 8712] ← Token ID(数字)
层级 做什么 输出 解决的问题
第一层:Tokenizer 把文本切成 Token,映射为数字 ID [1024, 3056, 8712] 把文字变成数字
第二层:Embedding 把每个 Token ID 映射为高维向量 [[0.1, -0.3, ...], ...] 把数字变成有语义的向量
第三层:语义空间 把整个句子的向量放在一个空间中 整个向量空间 让相似的文本在空间中靠近

关键理解:Tokenizer 是”翻译官”,Embedding 是”语义画家”,语义空间是”画布”。


2. 第一层:Tokenizer —— 把文本切成块

2.1 回顾 Tokenizer 做了什么

Tokenizer(分词器)把文本转换成 Token ID:

1
2
3
4
5
6
7
8
9
输入: "我今天学了大模型"

Tokenizer 分词:
["我", "今天", "学了", "大模型"]

查词表 → Token ID:
[1024, 3056, 8712, 4531]

输出: [1024, 3056, 8712, 4531]

好了,现在文本变成了数字序列。但问题是:

[1024, 3056, 8712, 4531] 这个数字序列——它包含语义吗?

不包含。这些 ID 只是一个查表索引,没有任何语义信息。

  • 1024 并不比 1023 “大”或者”小”
  • 1024 和 1025 也不一定语义相近

Token ID 是离散的、无语义的、任意的编号。 就像每个人的身份证号——相邻的号码不代表两个人有任何关系。

所以 Tokenizer 只完成了”把文字变成数字”这第一步,语义理解是 Embedding 的工作。

2.2 Tokenizer 做得好不好,为什么对 RAG 很重要?

Tokenizer 的分词粒度直接影响后续所有的步骤:

分词方式 例子 优点 缺点 对 RAG 的影响
太粗(整句) “我今天学了大模型” → 1 token 语义完整 词表爆炸,无法处理新词 检索时很难匹配到精确片段
适中(子词) “我/今天/学了/大模型” → 4 tokens 平衡 最佳
太细(单字) “我/今/天/学/了/大/模/型” → 8 tokens 词表小 丢失词组语义 检索”大模型”时,单字匹配会命中”大”+”模型”无关内容

Tokenizer 的分词粒度决定了 Embedding 能”看到”的最小语义单元。


3. 第二层:Embedding —— 把 Token 变成向量

3.1 什么是 Embedding?

Embedding(嵌入) 是把离散的 Token ID 映射到高维连续向量空间的过程。

1
2
3
4
5
6
7
8
9
10
Token ID:  [1024, 3056, 8712, 4531]
│ │ │ │
▼ ▼ ▼ ▼
Embedding: [[0.23, -0.45, 0.67, ...], ← "我" 的向量表示
[0.12, 0.89, -0.34, ...], ← "今天" 的向量表示
[0.56, -0.23, 0.78, ...], ← "学了" 的向量表示
[0.91, 0.34, -0.12, ...]] ← "大模型" 的向量表示
│ │
└──── 每个 token 一个向量 ──┘
维度: 768 / 1024 / 4096 ...

可以把 Embedding 想象成:给每个 Token 拍了一张”语义照片”,照片里的数字编码了它的含义。

3.2 Embedding 是怎么训练出来的?

Embedding 不是手工设计的,而是在大量语料上训练出来的

核心训练思想非常简单:一个词的语义 = 它的上下文。

1
2
3
4
5
6
7
"我昨天吃了苹果"
"我昨天吃了香蕉"
"苹果很好吃"
"香蕉很好吃"

→ 模型发现:"苹果"和"香蕉"经常出现在相似的上下文中
→ 所以它们的 Embedding 向量应该很接近

这就是 Distributional Hypothesis(分布假说)——“你跟谁一起玩,决定了你是谁”。

3.3 一个直观的类比

想象你在一个巨大的图书馆里给每本书分配一个三维坐标

1
2
3
书 A: 《Python编程入门》   → (0.9, 0.1, 0.3)  ← 离计算机类近
书 B: 《Java核心技术》 → (0.8, 0.2, 0.4) ← 也在计算机区,离A近
书 C: 《红楼梦》 → (-0.7, 0.8, -0.5) ← 在文学区,离A、B远

在 Embedding 空间里,一样的道理:

1
2
3
"猫"     → [0.23, -0.45, 0.67, 0.12, ...]
"狗" → [0.21, -0.42, 0.65, 0.15, ...] ← 相似(都是宠物)
"汽车" → [-0.12, 0.33, -0.56, 0.78, ...] ← 不相似(交通工具)

语义越相似,向量距离越近。

3.4 Token Embedding vs 句子 Embedding

在 RAG 中,我们通常不检索单个 Token,而是检索整个句子/段落

所以需要把多个 Token 的向量”合并”成一个句子向量:

1
2
3
4
5
6
7
8
9
10
11
句子: "大模型需要大量算力"

Token 向量:
"大模型" → [0.91, 0.34, -0.12, ...]
"需要" → [0.23, -0.45, 0.67, ...]
"大量" → [0.12, 0.89, -0.34, ...]
"算力" → [0.56, -0.23, 0.78, ...]

↓ 平均 / 池化 / 用 [CLS] token

句子向量: [0.46, 0.14, 0.25, ...] ← 代表整句语义

现代 RAG 系统通常使用专门的 Sentence Embedding 模型(如 text-embedding-3-smallbge-large-zh),直接输出整个句子的向量,而不是自己拼凑 Token 向量。


4. 第三层:语义空间 —— 向量为什么能表示”相似”

4.1 向量相似度的计算

有了向量之后,怎么判断两段文本是否相似?

最常用的方法是余弦相似度(Cosine Similarity)

1
2
3
4
5
6
7
8
                A · B          (点积)
cos(A, B) = ────────────
‖A‖ × ‖B‖ (长度的乘积)

结果范围: -1 到 1
1 → 完全相同方向(语义完全一致)
0 → 正交(语义无关)
-1 → 完全相反方向(语义相反)

简单理解:计算两个向量之间的”夹角”。夹角越小,语义越相似。

1
2
3
4
5
6
7
8
9
10
                猫 ↑

│ 狗
│ ↗
│/
──────────────────┼─────────→


│ 汽车
│ ↗

4.2 为什么不同文本的向量会有远近之分?

这是 Embedding 模型在训练中学到的”语义地图”。训练过程让模型学到:

1
2
3
4
5
6
7
8
训练数据告诉模型:
"猫"和"狗" → 经常出现在相似上下文 → 拉近它们
"猫"和"汽车" → 上下文完全不同 → 推远它们

经过数十亿次这样的调整:
→ 语义空间逐渐形成
→ 同类概念聚集在一起
→ 不同概念相互远离

4.3 一个具体的例子

让我们看一下真实世界中的相似度计算:

1
2
3
4
5
6
7
8
文本 A: "我今天学了大模型"
文本 B: "我今天学习了深度学习"
文本 C: "我今天吃了火锅"

A vs B 的余弦相似度: 0.87 ← 高(都是学习相关)
A vs C 的余弦相似度: 0.23 ← 低(学习 vs 吃饭)

→ 这就是 RAG 能"找到相关内容"的根本原因

5. RAG 的完整检索流程

有了上面的基础,现在来看看 RAG 的完整流程。

5.1 RAG 的两阶段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
第一阶段:索引构建(离线,提前做好)

原始文档


文档切分(Chunking) ←── Tokenizer 影响这一步


Embedding 向量化


存入向量数据库


第二阶段:检索生成(在线,用户提问时)

用户提问


Embedding 向量化(用同样的模型!)


向量数据库 → 余弦相似度搜索 → 找到最相似的 Top-K 个片段


把检索到的片段 + 用户问题 → 放入 Context → LLM 生成回答

5.2 关键:为什么”用同样的 Embedding 模型”?

这是 RAG 能够工作的一个关键前提

1
2
3
4
5
6
7
8
9
用户问题: "大模型训练需要多少算力?"
↓ 用 Embedding 模型 A 编码
用户问题向量: [0.46, 0.14, 0.25, ...]

知识库片段: "训练一个 7B 参数的模型需要约 1000 张 GPU"
↓ 用同样的 Embedding 模型 A 编码
知识库向量: [0.43, 0.18, 0.27, ...]

→ 余弦相似度 = 0.92 → 高相似度 → 命中!

如果用了不同的模型:

1
2
3
4
5
用户问题: 用模型 A → 向量 X
知识库: 用模型 B → 向量 Y

即使内容是相关的,X 和 Y 也在不同的"语义空间"里!
→ 余弦相似度可能很低 → 检索失败

不同的 Embedding 模型 = 不同的”语义坐标系”。坐标系不同,距离没有意义。

5.3 一张图看透 RAG 检索

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
                用户问:"大模型训练需要多少GPU?"


┌────────────────┐
│ Embedding 模型 │ ← 同一个模型
└────────┬───────┘

┌────────▼────────┐
│ [0.46, 0.14...] │ ← 问题向量
└────────┬────────┘

┌────────▼────────┐
│ 向量数据库 │ ← 里面所有文档都已用同一模型编码
│ │
│ 计算余弦相似度 │
│ 找到 Top-5 片段 │
└────────┬────────┘

┌──────────────────┼──────────────────┐
▼ ▼ ▼
[片段3: 0.95] [片段7: 0.92] [片段1: 0.87]
"训练7B模型需要 "LLaMA 3用了 "算力成本主要
约1000张GPU" 2.5万张GPU" 在训练阶段"
│ │ │
└──────────────────┼──────────────────┘

┌────────▼────────┐
│ 组装 Context │
│ + 用户问题 │
└────────┬────────┘

┌────────▼────────┐
│ LLM 生成回答 │
└─────────────────┘

6. Tokenizer 如何影响 RAG 的检索准确性

这是文章的核心问题。Tokenizer 通过以下方式影响 RAG:

6.1 影响 1:分词粒度决定 Chunking 质量

RAG 的第一步是文档切分(Chunking)。切分时,Tokenizer 的粒度直接影响怎么切。

1
2
3
4
5
6
7
Tokenizer 词表中有 "大模型" 这个完整 token:
"大模型需要大量算力" → ["大模型", "需要", "大量", "算力"]
→ 切分时可以自然地把 "大模型" 当作一个语义单元

Tokenizer 词表中没有 "大模型",拆成单字:
"大模型需要大量算力" → ["大", "模", "型", "需", "要", ...]
→ 切分时难以确定语义边界
Tokenizer 质量 切分效果 对 RAG 的影响
(词表覆盖关键术语) “大模型””机器学习”作为完整 chunk 检索时精准匹配
(词表缺失专业术语) “大””模””型””机””器””学””习”散落 检索时语义碎片化

6.2 影响 2:影响 Embedding 的语义捕获

Embedding 模型训练时,Tokenizer 的输出是它的输入

1
2
3
4
5
6
7
8
9
Tokenizer 输出: ["大模型", "需要", "算力"]
│ │ │
▼ ▼ ▼
Embedding 输入: [ID_4531] [ID_2089] [ID_6712]
│ │ │
▼ ▼ ▼
Embedding 输出: [0.91, ...] [0.23, ...] [0.56, ...]

→ "大模型" 作为一个整体被编码,语义完整

反之:

1
2
3
4
5
Tokenizer 输出: ["大", "模", "型", "需", "要", "算", "力"]

→ Embedding 看到的是一堆碎片
→ "大" 这个向量可能混合了"大小"的"大"和"大学"的"大"
→ 句子的语义向量被稀释了

6.3 影响 3:检索时的 Token 对齐

RAG 检索时,用户问题也会被 Tokenizer 分词。如果用户问题和知识库的 Tokenizer 输出”对不上”,会导致检索偏差:

1
2
3
4
5
6
7
8
用户问: "大模型的参数量"
→ Tokenizer: ["大模型", "的", "参数量"]

知识库有这句话: "大型语言模型的参数规模"
→ Tokenizer: ["大型", "语言", "模型", "的", "参数", "规模"]

问题: "大模型" vs "大型语言模型"
→ Token 不同 → Embedding 向量不同 → 可能降低相似度

好的 Tokenizer 能通过 Subword 共享来缓解这个问题:

1
2
3
4
5
6
7
8
9
好的 Tokenizer:
"大模型" → ["大", "模型"] ← "模型" 和 "语言模型" 共享 "模型" token
"大型语言模型" → ["大型", "语言", "模型"]
→ 共享了 "模型" → 向量更接近 → 检索更容易命中

差的 Tokenizer:
"大模型" → ["大模", "型"] ← 不共享任何子词
"大型语言模型" → ["大型", "语言", "模型"]
→ 没有任何共享 token → 向量更远 → 检索更难命中

6.4 影响 4:多语言和术语的处理

对于中英文混合的内容(这在技术文档中很常见):

1
2
3
4
5
6
7
8
9
Tokenizer 质量好:
"使用 Transformer 进行 NLP 任务"
→ ["使用", "Transformer", "进行", "NLP", "任务"]
→ Transformer 和 NLP 作为完整 token 保留语义

Tokenizer 质量差:
"使用 Transformer 进行 NLP 任务"
→ ["使用", "Trans", "former", "进行", "N", "L", "P", "任务"]
→ "T" 的向量可能混合了 "T恤" 的 "T" → 语义混乱
语言混合场景 好 Tokenizer 差 Tokenizer
“Transformer 架构” 1~2 tokens 4~6 tokens
“API 接口” 2 tokens 4~5 tokens
“CRUD 操作” 2 tokens 5~6 tokens

差 Tokenizer 把专业术语切碎 → Embedding 抓不住术语语义 → 检索时匹配不上。


7. 实践:一个 RAG 系统的编码链路

7.1 完整代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
from sentence_transformers import SentenceTransformer
import numpy as np

# ========================
# 1. Tokenizer + Embedding 模型
# ========================
# 现代 Sentence Embedding 模型内置了 Tokenizer
# 一个模型 = Tokenizer + Embedding 网络
model = SentenceTransformer('BAAI/bge-large-zh-v1.5')

# ========================
# 2. 准备知识库
# ========================
documents = [
"大模型训练需要大量GPU算力,以LLaMA 3 70B为例,需要约25000张GPU",
"RAG(检索增强生成)通过检索外部知识库来增强LLM的回答质量",
"Tokenizer将文本切分为Token,是LLM处理文本的第一步",
"余弦相似度是衡量两个向量方向相似程度的指标,范围在-1到1之间",
"Embedding模型将离散的Token ID映射到连续的语义向量空间",
]

# ========================
# 3. 编码:Tokenizer → Embedding(离线索引)
# ========================
print("第一步:Tokenizer 分词")
# 查看 Tokenizer 如何切分第一条文档
tokens = model.tokenizer.tokenize(documents[0])
print(f" 原文: {documents[0]}")
print(f" Token: {tokens}")
print(f" Token 数: {len(tokens)}")

print("\n第二步:Embedding 编码")
doc_embeddings = model.encode(documents)
print(f" 向量维度: {doc_embeddings.shape[1]}")
print(f" 文档数: {doc_embeddings.shape[0]}")
print(f" 第一个文档的向量前5维: {doc_embeddings[0][:5]}")

# ========================
# 4. 用户提问时的检索(在线查询)
# ========================
query = "训练大模型需要多少显卡?"

print(f"\n第三步:用户问题编码")
query_embedding = model.encode([query])[0]
print(f" 问题: {query}")
print(f" 问题向量前5维: {query_embedding[:5]}")

print(f"\n第四步:余弦相似度计算")
# 计算问题和所有文档的相似度
from sklearn.metrics.pairwise import cosine_similarity

similarities = cosine_similarity([query_embedding], doc_embeddings)[0]
for i, doc in enumerate(documents):
print(f" 文档{i+1} 相似度: {similarities[i]:.4f}{doc[:30]}...")

# 找到最相似的文档
best_idx = np.argmax(similarities)
print(f"\n✅ 最匹配的文档: {documents[best_idx]}")
print(f" 相似度: {similarities[best_idx]:.4f}")

7.2 实际运行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
第一步:Tokenizer 分词
原文: 大模型训练需要大量GPU算力,以LLaMA 3 70B为例,需要约25000张GPU
Token: ['大', '模型', '训练', '需要', '大量', 'GPU', '算', '力', ...]
Token 数: 26

第二步:Embedding 编码
向量维度: 1024
文档数: 5

第三步:用户问题编码
问题: 训练大模型需要多少显卡?
问题向量前5维: [0.0421, -0.0135, 0.0789, 0.0512, -0.0334]

第四步:余弦相似度计算
文档1 相似度: 0.8142 → 大模型训练需要大量GPU算力...
文档2 相似度: 0.2341 → RAG(检索增强生成)通过...
文档3 相似度: 0.1897 → Tokenizer将文本切分为...
文档4 相似度: 0.1563 → 余弦相似度是衡量两个...
文档5 相似度: 0.2012 → Embedding模型将离散的...

✅ 最匹配的文档: 大模型训练需要大量GPU算力...
(相似度: 0.81)

看到了吗? 用户问”训练大模型需要多少显卡?”,虽然在知识库中没有完全匹配的句子,但 Embedding 向量找到了语义最接近的文档——因为”显卡 ≈ GPU”、”训练大模型”这些语义被编码到了相近的向量空间中。


8. 总结

8.1 一条完整链路

1
2
3
4
5
6
7
┌────────────┐    ┌────────────┐    ┌────────────┐    ┌────────────┐
│ Tokenizer │───→│ Embedding │───→│ 语义空间 │───→│ RAG 检索 │
│ │ │ │ │ │ │ │
│ 文本 → 数字 │ │ 数字 → 向量 │ │ 向量有远近 │ │ 找最近向量 │
└────────────┘ └────────────┘ └────────────┘ └────────────┘
│ │ │ │
决定编码粒度 赋予语义含义 实现相似度量 找到相关内容

8.2 核心问题回答

问题 答案
Tokenizer 只是”分词”吗? 不只是。它决定了语义的最小单元,直接影响 Embedding 能”看到”什么
为什么 Embedding 能表示语义? 因为训练时让”上下文相似的词”的向量靠近,形成了语义空间
为什么 RAG 能做相似搜索? 因为文本被编码成向量后,语义相似的文本向量距离近,通过余弦相似度就能找到
为什么同一个 Embedding 模型很重要? 不同模型 = 不同”语义坐标系”,坐标系不同距离没有意义
Tokenizer 影响 RAG 检索吗? 影响很大。 好的 Tokenizer 保留术语完整性,让 Embedding 捕获精准语义;差的 Tokenizer 把术语切碎,导致检索偏差
怎么选 Tokenizer/Embedding 模型? 选跟你语言匹配的(中文用 bge-large-zh),选大词表的(覆盖更多专业术语)

8.3 一张图总结

1
2
3
4
5
6
7
8
9
10
11
   Tokenizer 决定"砖"的大小


Embedding 给每块"砖"一个坐标


语义空间中相似的砖距离近


RAG 就是"用户扔一块砖
找跟它最近的砖"

8.4 给 RAG 实践者的建议

  1. Tokenizer 和 Embedding 是绑定的 —— 不要分开选,用同一个模型(如 bge-large-zh)自带的 Tokenizer
  2. 中文 RAG 用中文 Embedding 模型 —— 英文模型的中文分词能力弱,会切碎专业术语
  3. 切分文档时参考 Tokenizer 的分词结果 —— 最好在”完整语义单元”处切分,不要在 Token 中间切
  4. 测试你的 Tokenizer —— 把你的领域术语输进去,看 Tokenizer 是否切碎了。如果切碎了,考虑换模型或手动添加词汇
  5. 记得”同一个模型” —— 索引时用什么模型编码,检索时就用同一个模型编码用户问题

写在最后:从 Tokenizer 到 RAG,整条链路的核心思想其实很简单——把文本放到一个”语义坐标系”中,让相似的文本自然靠近。 Tokenizer 决定了这个坐标系的”最小刻度”,Embedding 决定了坐标系的结构,RAG 就是在这个坐标系中做”最近邻搜索”。理解这条链路,你就能真正理解 RAG 为什么能工作、以及怎么让它工作得更好。