Skip to content

第 3 章 · 文档切分与向量化:RAG 的"地基工程"

🎯 本章目标:理解切分和向量化为什么是 RAG 的"地基",掌握选型决策框架,能解释"为什么我的 RAG 召回不准"。

💡 业内黄金法则RAG 系统 80% 的效果,由切分和 Embedding 决定。再好的 LLM 也救不了垃圾的检索。


目录

  1. 文档切分(Chunking)深度解析
  2. 五种主流切分策略详解
  3. 切分参数怎么调
  4. Embedding 模型原理
  5. 主流 Embedding 模型对比
  6. 向量数据库选型
  7. 实战避坑指南
  8. 思考题

1. 文档切分 Chunking 深度解析

1.1 为什么切分是地基?

一个真实的反面案例

公司知识库:50 份产品手册,平均每份 100 页。
工程师小王:用默认 1000 字符切分。

用户问:「产品 A 的退货政策是什么?」
召回 Top-3:
  Chunk 1(相似度 0.85): 「产品 A 的退货政策详见附录 C,需要满足以下条件...」
                          ↑ 注意!这里 "退货政策" 出现了,但实际内容在附录 C 没切到这块
  Chunk 2(相似度 0.78): 「产品 B 的售后服务包括...」  ← 不相关
  Chunk 3(相似度 0.72): 「公司物流合作伙伴...」      ← 不相关

LLM 回答:「产品 A 的退货政策详见附录 C,请查阅相关章节。」  ← 等于没答

问题根源:切分时把"指引"和"实际内容"切开了,召回到的全是无效片段。

1.2 切分的核心矛盾

💡 切分的本质是 "在语义完整性"和"检索精度"之间找平衡

1.3 好切分的三个标准

标准解释反例
语义完整一个 Chunk 内容讲清楚一件事一个事实跨两个 Chunk
信息密度高Chunk 内大部分内容和某个语义点强相关一个 Chunk 同时混了 5 个主题
大小适中既能装下完整语义,又不至于稀释相关性1 句话 / 10000 字

2. 五种主流切分策略详解

2.1 策略一:固定长度切分(Fixed-size Chunking)

思路

每 N 个字符 / Token 切一刀,简单粗暴。

例子

原文(300 字):「猫咪是一种常见的宠物动物,它们通常活泼好动……
                  关于猫咪的饮食,主要分为干粮和湿粮两种……
                  当猫咪生病时,常见症状包括……」

按 100 字符切:
  Chunk 1: 「猫咪是一种常见的宠物动物,它们通常活泼好动……关于猫咪」
  Chunk 2: 「的饮食,主要分为干粮和湿粮两种……当猫咪生病时」
  Chunk 3: 「,常见症状包括……」

优缺点

优点缺点
✅ 实现极简❌ 经常把句子切断
✅ 速度快❌ 不考虑语义
✅ 长度可控❌ 效果差

适合场景

⚠️ 只适合 Demo 和原型验证。生产环境不推荐

2.2 策略二:递归字符切分(Recursive Character Splitter)⭐ 最常用

思路

按优先级分隔符递归尝试切分:

优先级:段落分隔(\n\n) → 行(\n) → 句号(。)→ 逗号(,)→ 字符

如果按 \n\n 切完,某个 Chunk 还是太大,就用 \n 继续切,依此类推。

例子

原文:「
猫咪发烧的常见原因:
1. 病毒感染
2. 细菌感染
3. 中暑

猫咪退烧的处理方法:
1. 保持环境凉爽
2. 提供充足饮水
3. 必要时送医


目标 Chunk 大小 100 字符:

第 1 步:按 \n\n 切分
  Chunk A: 「猫咪发烧的常见原因:1. 病毒感染 2. 细菌感染 3. 中暑」(45 字符)
  Chunk B: 「猫咪退烧的处理方法:1. 保持环境凉爽 2. 提供充足饮水 3. 必要时送医」(60 字符)

第 2 步:所有 Chunk 都小于 100 字符,无需进一步切分 ✓

关键优势:尽量保留 段落 → 句子 → 短语 的语义边界。

LangChain 的实现思路

python
RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=50,
    separators=["\n\n", "\n", "。", "!", "?", ",", " ", ""]
)

适合场景

🌟 绝大多数通用场景的首选,无脑选这个不会错。

2.3 策略三:按文档结构切分(Markdown / HTML Splitter)

思路

利用文档自身的结构(Markdown 标题、HTML 标签)切分。

Markdown 例子

markdown
# 宠物医疗手册

## 第一章 猫咪疾病

### 1.1 发烧
猫咪发烧的常见原因...

### 1.2 腹泻
猫咪腹泻的常见原因...

## 第二章 狗狗疾病

### 2.1 皮肤病
狗狗皮肤病的常见类型...

按 H2/H3 标题切分,每个 Chunk 还能带上层级路径:

Chunk 1:
  metadata: { h1: "宠物医疗手册", h2: "第一章 猫咪疾病", h3: "1.1 发烧" }
  content: "猫咪发烧的常见原因..."

Chunk 2:
  metadata: { h1: "宠物医疗手册", h2: "第一章 猫咪疾病", h3: "1.2 腹泻" }
  content: "猫咪腹泻的常见原因..."

优点

优点解释
✅ 完美保留结构章节、小节边界清晰
✅ Metadata 丰富层级路径可用于过滤检索
✅ 召回准一个小节通常就是一个完整知识点

适合场景

  • 技术文档、API 文档
  • 法律法规(章/节/条)
  • 教科书
  • Wiki 类内容

2.4 策略四:语义切分(Semantic Chunking)

思路

不按字符数切,而是按语义相似度自动判断在哪里切

💡 类比:你读一本小说,作者在 "场景切换" 时会自然换段。语义切分就是让算法自动识别这种 "语义切换点"。

算法步骤

例子

句子序列:
S1: "今天天气很好"            (相似度 → S2: 0.92)
S2: "适合户外活动"            (相似度 → S3: 0.88)
S3: "公园里很多人在散步"      (相似度 → S4: 0.30 ← 突然下降!)
S4: "Python 是一门解释型语言" (相似度 → S5: 0.85)
S5: "适合初学者上手"

→ 在 S3 和 S4 之间切一刀(语义跳跃点)

Chunk 1: S1 + S2 + S3 (讲天气和户外)
Chunk 2: S4 + S5     (讲编程)

优缺点

优点缺点
✅ 语义边界精准❌ 计算成本高(每句都要 Embedding)
✅ 适合长连续文本❌ 实现复杂
✅ 不依赖文档格式❌ 阈值难调

适合场景

  • 小说、长篇连续文本
  • 没有明显结构的口语化转写
  • 对召回质量极致追求的场景

2.5 策略五:基于 Token 的切分

思路

不按字符切,按大模型的 Token 数切。

为什么?

  • 中文一个汉字通常对应 1-2 个 Token
  • 英文一个单词通常 1-4 个 Token
  • LLM 收费按 Token 算,按 Token 切能精确控制成本和长度

工具

python
import tiktoken

encoder = tiktoken.encoding_for_model("gpt-4")
tokens = encoder.encode("你的文本")

# 按 500 Token 切
chunks = []
for i in range(0, len(tokens), 500):
    chunk_tokens = tokens[i:i+500]
    chunks.append(encoder.decode(chunk_tokens))

适合场景

  • 严格控制 LLM 输入长度的场景
  • 需要精确预估 API 成本

2.6 五种策略对比速查表

策略实现难度召回质量速度推荐度适合
固定长度⭐⭐⭐⭐⭐⭐⭐⭐⭐Demo
递归字符⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐通用首选
文档结构⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐结构化文档
语义切分⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐极致追求
基于 Token⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐严控长度

2.7 进阶组合策略

实际生产中往往组合多种策略

组合方案 A:父子文档(Parent-Child)

父 Chunk: 完整章节(2000 字,提供完整上下文)
子 Chunk: 细粒度切分(300 字,用于精准检索)

检索时:
  1. 用子 Chunk 检索(精准)
  2. 召回后用对应的父 Chunk 喂给 LLM(上下文完整)

💡 效果:兼顾"检索精度"和"上下文完整性",是企业级 RAG 的标配。

组合方案 B:摘要 + 原文

对每个章节生成一个 LLM 摘要
向量库存储:摘要的 Embedding
关联:摘要 ↔ 原文

检索时:
  1. 用摘要做向量检索(信息密度高,检索精准)
  2. 召回后取对应的原文喂给 LLM

3. 切分参数怎么调

3.1 三个关键参数

参数含义经验值
chunk_size每个 Chunk 大小(字符 / Token)300-1500 字符
chunk_overlap相邻 Chunk 重叠大小chunk_size 的 10%-20%
separators切分优先级分隔符按段落 → 行 → 句子 → 字符

3.2 chunk_size 怎么定?

决策框架

实战经验

文档类型推荐 chunk_size原因
FAQ 问答对200-300每个 Q-A 就是一个原子单位
技术文档500-800一个小节一个 Chunk
法律法规300-500一条法律就是一个独立逻辑
论文/书籍800-1500上下文跨度大
客服对话日志200-400一组对话一个 Chunk
代码按函数切函数是天然单元

3.3 chunk_overlap 怎么定?

为什么需要重叠?

不重叠:
  Chunk 1: "猫咪发烧的判断标准是体温超过 39.2 度,处理方法包括"
  Chunk 2: "保持环境凉爽、提供充足饮水、必要时送医。"

  问题:"怎么处理猫咪发烧?"
  检索到 Chunk 2,但 LLM 不知道是在讨论什么,可能答错。

有重叠:
  Chunk 1: "猫咪发烧的判断标准是体温超过 39.2 度,处理方法包括"
  Chunk 2: "猫咪发烧的处理方法包括保持环境凉爽、提供充足饮水、必要时送医。"
            ↑ 上文重叠了

取值建议

chunk_size推荐 overlap比例
30030-6010%-20%
50050-10010%-20%
1000100-20010%-20%

⚠️ 不要超过 30%,否则同一信息被存多份,浪费向量库,召回也会冗余。

3.4 怎么验证切分质量?

方法一:人工抽检

随机抽 20 个 Chunk,看:

  • ✅ 内容是否完整?
  • ✅ 是否在句中被切断?
  • ✅ Chunk 是否聚焦一个主题?

方法二:召回测试

准备 10-20 个真实问题,跑检索,看:

  • ✅ 是否能召回到 "应该被召回" 的 Chunk?
  • ✅ 召回的 Chunk 相似度分数分布是否合理?

方法三:端到端评估

用 RAGAS 等工具评估 Context Recall、Context Precision 指标(详见第 6 章)。


4. Embedding 模型原理

4.1 什么是 Embedding?

直观理解

Embedding 模型 = "给文本算 DNA" 的机器。

输入:一段文本 输出:一个固定长度的向量(一串数字),代表文本的 "语义指纹"。

"猫咪发烧"       → [0.12, -0.45, 0.78, 0.23, ..., 0.55]  (1536 维)
"小猫体温升高"   → [0.13, -0.43, 0.80, 0.21, ..., 0.54]  ← 几乎相同
"狗狗腹泻"       → [-0.20, 0.18, 0.42, -0.31, ..., 0.10]
"今天股市大跌"   → [-0.55, 0.21, -0.30, 0.65, ..., -0.42]

4.2 为什么向量能代表语义?

训练原理(极简版)

Embedding 模型通过 "对比学习" 训练:

训练数据:
  正样本对(语义相近的句子对):
    ("猫咪发烧", "小猫体温升高")
    ("RAG 是什么", "检索增强生成的概念")

  负样本对(语义无关的句子对):
    ("猫咪发烧", "今天股市大跌")
    ("RAG 是什么", "如何做红烧肉")

训练目标:
  → 让正样本对的向量距离尽可能近
  → 让负样本对的向量距离尽可能远

经过亿级语料的训练,模型学会了 "把语义相近的文本映射到向量空间的相近位置"。

4.3 向量空间的神奇属性

语义相似性

python
similarity("猫", "狗") = 0.85     # 都是宠物,相近
similarity("猫", "汽车") = 0.20   # 不相关

语义代数(经典例子)

vector("国王") - vector("男人") + vector("女人") ≈ vector("女王")
vector("巴黎") - vector("法国") + vector("日本") ≈ vector("东京")

这说明向量空间隐含了抽象的语义关系

4.4 Embedding 的维度

维度典型模型含义
384bge-small小模型,速度快
768bge-base中等
1024bge-large、BGE-M3主流
1536text-embedding-3-smallOpenAI 默认
3072text-embedding-3-largeOpenAI 高精度

💡 维度越高,理论表达能力越强,但存储和计算成本也越高。 经验上 768-1024 维是性价比之选。


5. 主流 Embedding 模型对比

5.1 选型核心维度

维度关注点
语言中文/英文/多语
领域通用/垂直(医疗/法律/代码)
长度最大支持多长输入?
维度影响存储和计算
价格免费开源 / 按 Token 付费
部署API / 自部署
性能MTEB 榜单排名

5.2 推荐模型清单

中文场景

模型维度部署方式特点
BGE-Large-zh-v1.51024开源/自部署中文 SOTA,免费
BGE-M31024开源/自部署中英文+长文本(8192 Token)
Qwen3-Embedding1024API/开源阿里出品,中文优秀
M3E-large1024开源/自部署早期中文经典

英文/多语场景

模型维度部署方式特点
text-embedding-3-small1536OpenAI API性价比之王
text-embedding-3-large3072OpenAI API效果好
Cohere embed-v31024Cohere API检索优化
Jina Embeddings v31024API/开源多语支持好
Voyage AI1024API垂直领域强

5.3 怎么选?决策表

你的情况推荐
中文为主,预算有限BGE-Large-zh-v1.5(开源自部署)
中英混合,文档很长BGE-M3(支持 8192 Token)
全英文,省心想用 APItext-embedding-3-small
追求 SOTA,预算充足text-embedding-3-large + Cohere Rerank
数据敏感不能上云BGE 系列开源版自部署
代码搜索text-embedding-3 / Voyage Code 系列

5.4 评估 Embedding 模型的标准:MTEB

MTEB(Massive Text Embedding Benchmark) 是 Embedding 模型的"高考"。

中文版叫 C-MTEB,查中文模型排名看这个。

官方榜单:MTEB Leaderboard

5.5 一个 Embedding 实操例子

python
from sentence_transformers import SentenceTransformer

model = SentenceTransformer("BAAI/bge-large-zh-v1.5")

sentences = [
    "猫咪发烧怎么办",
    "小猫体温升高的处理",
    "今天股市大跌",
]

embeddings = model.encode(sentences, normalize_embeddings=True)

import numpy as np
sim_1_2 = np.dot(embeddings[0], embeddings[1])
sim_1_3 = np.dot(embeddings[0], embeddings[2])

print(f"猫咪发烧 vs 小猫体温升高: {sim_1_2:.3f}")
print(f"猫咪发烧 vs 股市大跌: {sim_1_3:.3f}")

6. 向量数据库选型

6.1 主流向量数据库全景

6.2 选型决策表

你的情况推荐
上千万到亿级向量,要分布式Milvus
千万级以下,重性能轻运维Qdrant
已经在用 PostgreSQLpgvector(不要再多引一个组件)
已经在用 ElasticsearchES 向量字段(混合检索方便)
本地开发 DemoChroma
完全不想运维Pinecone / Zilliz Cloud
需要"语义+关键词"混合检索原生支持Weaviate / ES

6.3 关键能力对比

能力MilvusQdrantWeaviateChromapgvectorES
分布式依赖 PG
元数据过滤✅✅✅✅✅✅
混合检索✅✅✅✅
多向量字段
学习曲线⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐

✅✅ = 原生强支持,✅ = 支持,⭕ = 部分支持,❌ = 不支持


7. 实战避坑指南

7.1 切分阶段的常见坑

坑 1:表格被切碎

原表格:
| 产品   | 价格 | 库存 |
|--------|------|------|
| 产品 A | 100  | 50   |
| 产品 B | 200  | 30   |

按字符切后:
  Chunk 1: "| 产品   | 价格 | 库存 || 产品 A | 100  |"
  Chunk 2: "50   || 产品 B | 200  | 30 |"
  → 表格语义彻底破碎

解决方案

  • 表格单独识别和处理(用 unstructured 提取表格)
  • 把每行转成 "产品名: A, 价格: 100, 库存: 50" 这种独立 Chunk

坑 2:代码块被切断

原代码:
def foo():
    return bar()

按字符切:
  Chunk 1: "def foo():"
  Chunk 2: "    return bar()"
  → 单看 Chunk 2 不知道在讲什么

解决方案

  • LangChainRecursiveCharacterTextSplitter.from_language(Language.PYTHON)
  • 按函数/类作为最小切分单元

坑 3:标题和正文被切开

Chunk 1: "## 第二章 退货政策"
Chunk 2: "退货需要在 7 天内提出,并保持商品完好..."

→ 用户搜"退货政策"匹到 Chunk 1,但 Chunk 1 只有标题没有内容

解决方案

  • 在 Chunk 内 复制上下文标题(如 [第二章 退货政策] 退货需要在 7 天内...
  • 或者用文档结构切分(Markdown Splitter)

7.2 Embedding 阶段的常见坑

坑 1:入库和查询用了不同模型

入库:BGE-Large-zh-v1.5
查询:text-embedding-3-small

→ 两个完全不同的向量空间,相似度毫无意义
→ 召回乱七八糟

解决方案

  • 严格固定一个模型
  • 在向量库里加 metadata 记录用的什么模型

坑 2:归一化与否不一致

入库时:向量归一化(normalize_embeddings=True)
查询时:向量未归一化

→ 余弦相似度计算偏差

解决方案

  • 入库和查询保持一致的归一化策略
  • 推荐:都归一化(多数向量库默认期望归一化向量)

坑 3:中文用了英文模型

入库:text-embedding-ada-002(早期版本对中文不友好)
查询:中文问题
→ 中文召回质量差

解决方案

  • 中文场景优先选 BGE / Qwen-Embedding / text-embedding-3
  • 至少跑 MTEB 中文榜单看排名

7.3 向量库阶段的常见坑

坑 1:没建索引

向量数据库默认有索引但参数可能不优化,亿级数据下检索能慢到秒级。

解决方案

  • Milvus 用 HNSW 或 IVF_FLAT
  • 调好 nlistnprobeef_constructionef_search 等参数

坑 2:元数据没存够

入库时只存了 vector 和 text,没存 source、page、date、category

→ 后期想 "只检索 2024 年的文档",做不到
→ 想给用户展示来源出处,做不到

解决方案

  • 入库时尽可能多存 metadata
  • 至少包含:source(文件名)、page(页码)、section(章节)、update_timecategory

8. 思考题

  1. 为什么不直接把整本 1000 页的书 Embedding 成一个向量?

    参考答案
    • 信息稀释:1000 页有上千个主题,压成一个向量等于全是"平均语义",啥都搜不到。
    • 召回粒度:你想找的可能就是其中一段,整本书的向量没法定位到那一段。
    • Token 限制:Embedding 模型有最大输入长度(通常 512-8192 Token)。
    • 解决方案:先切分再 Embedding,每个 Chunk 一个向量。
  2. chunk_overlap 设成 0 会怎样?设成 chunk_size 的 50% 又会怎样?

    参考答案
    • 设为 0:相邻 Chunk 无重叠,跨边界的语义信息可能丢失,召回连续性差。
    • 设为 50%:信息冗余严重,向量库膨胀 2 倍,召回时同一信息被多次检索到,浪费 Context。
    • 最佳实践:10%-20% 是甜蜜点。
  3. 同样一个问题,BGE-Large-zh 召回的是 Chunk A,text-embedding-3 召回的是 Chunk B,哪个对?

    参考答案
    • 没有 "对错",只有 "哪个更适合你的场景"。
    • 评估方法:
      1. 准备一个人工标注的评估集(问题 + 标准答案应该来自哪几个 Chunk)
      2. 用两个模型分别跑,计算 Recall@K(标准 Chunk 是否在 Top-K 召回里)
      3. 选 Recall 高的
    • 中文场景一般 BGE 系列更强,但具体看数据集。
  4. 如果用户的问题是 "退货" 这种很短的查询,向量检索效果通常不好,为什么?怎么解决?

    参考答案

    原因:

    • 短查询语义信息有限,Embedding 难以精准捕捉用户意图
    • 短查询和长 Chunk 长度差异大,向量空间分布有偏
    • 多义性强("退货" 可能涉及流程、政策、客诉等多个角度)

    解决方案:

    • 查询改写:用 LLM 把短查询扩展成完整问题("退货" → "如何申请退货?退货流程是怎样的?")
    • 混合检索:加 BM25 关键词检索,短查询关键词匹配反而更准
    • HyDE:先让 LLM 生成一个假设性回答,用回答做检索(详见第 4 章)

📌 本章小结

你应该已经知道
为什么切分质量决定 RAG 上限
五种切分策略各自适用场景
递归字符切分是通用首选
chunk_size 和 overlap 的经验取值
Embedding 模型的原理(对比学习)
中文 / 英文 / 多语场景的模型选型
主流向量数据库的选型决策
入库和查询必须用同一个 Embedding 模型

🚀 下一步

地基打好了,下一章看怎么把检索做到又快又准第 4 章 · 检索优化技术