【AI应用开发】 RAG篇(三):文档切分与多路检索
前言
前两篇我们讲清了 RAG 的整体架构和向量检索原理。但这远不足以让系统"好用"------一条最容易被忽视却最为致命的链路是:文档切不好,检索等于白做。
本文深入拆解 RAG 中两个承上启下的关键环节:文档切分(Chunking) 和 多路检索(Multi-Path Retrieval)。六种切分策略各有什么优劣?K 值怎么选?什么场景该上多路检索?读完本文你会对"怎么切、怎么搜"建立完整的工程直觉。
目录
- 文档处理与切分策略
- 1.1 为什么需要切分
- 1.2 六种切分策略详解
- 1.3 切分策略选择指南
- 1.4 Overlap:重叠窗口的艺术
- 1.5 文档解析的挑战
- [检索:RAG 的灵魂环节](#检索:RAG 的灵魂环节)
- 2.1 检索的基本流程
- 2.2 多路检索:不止向量一种方法
- 2.3 K 值的选择艺术
- 2.4 相似度阈值过滤
- 2.5 检索质量的量化
- 2.6 Re-rank:检索的最后一道防线
1. 文档处理与切分策略
1.1 为什么需要切分
文档切分(Chunking)是 RAG 系统中最容易被低估但影响最大的环节之一。切得好不好,直接决定检索质量。三个核心原因迫使我们必须做切分:
| 原因 | 说明 | 后果(如果不做) |
|---|---|---|
| Embedding 模型的 Token 限制 | 大部分模型一次只能处理 512~8192 个 token | 长文档无法直接向量化,超出部分被截断 |
| 检索精度 | 大段文本语义被稀释,检索时匹配到不相关内容 | 用户问"退款流程",返回的却是整篇用户协议 |
| LLM 上下文窗口限制 | 最终要拼接到 Prompt 中给 LLM 看 | Chunk 太大占用过多 token,留给答案的空间变少 |
核心权衡:Chunk 太大 → 语义被稀释,检索不精准;Chunk 太小 → 上下文丢失,关键信息被截断。切分本质上是在"语义聚焦度"和"上下文完整度"之间找最优平衡点。
1.2 六种切分策略详解

① 固定长度切分(Fixed-size Chunking)
按固定字符数或 token 数切分,如每 500 字符一块。
原文:"大模型技术近年来发展迅猛。从GPT到Claude,从文心一言到通义千问,国产大模型也百花齐放..."
固定长度切分(chunk_size=30):
┌────────────────────────────────┐
│ "大模型技术近年来发展迅猛。从GPT到Claude," │ ← 在句子中间切断
├────────────────────────────────┤
│ "从文心一言到通义千问,国产大模型也百花齐放..." │ ← 不完整上下文
└────────────────────────────────┘
- 优点:最简单,无需任何 NLP 工具,计算开销为零
- 缺点:可能在句子/词语中间切断,破坏语义完整性
- 适合:纯英文、格式规整的文档
- Python 示例:
python
def fixed_size_split(text: str, chunk_size: int = 500, overlap: int = 50):
chunks = []
start = 0
while start < len(text):
end = start + chunk_size
chunks.append(text[start:end])
start = end - overlap
return chunks
② 按句子切分(Sentence-based Chunking)
用标点符号(。!?\n)识别句子边界,按 N 句一组切分。
- 优点:保持句子完整,语义连贯性好,中文场景推荐起步方案
- 缺点:句子长度差异大,chunk 大小不均匀------可能一个 chunk 只有 50 字,另一个 500 字
- 适合 :大多数中文场景的起步方案
- Python 示例:
python
import re
def sentence_split(text: str, sentences_per_chunk: int = 5):
sentences = re.split(r'(?<=[。!?\n])', text)
sentences = [s.strip() for s in sentences if s.strip()]
chunks = []
for i in range(0, len(sentences), sentences_per_chunk):
chunk = ''.join(sentences[i:i + sentences_per_chunk])
chunks.append(chunk)
return chunks
③ 按段落切分(Paragraph-based Chunking)
以空行或缩进为标志按段落切分。
- 优点:段落是天然的语义单元,信息完整;每个 chunk 通常围绕一个主题
- 缺点:段落长度可能极不均衡(一句话段落 vs 一整页段落),过长段落需二次切分
- 适合:结构清晰的文档(技术文档、论文、API 文档)
④ 语义切分(Semantic Chunking)⭐
核心思路:用 Embedding 模型计算相邻段落的语义相似度,在**"相似度骤降"**的位置切分------这个位置通常就是话题转换的自然边界。
文档段落序列的语义相似度曲线:
段落1 ── 段落2 ── 段落3 ──│── 段落4 ── 段落5 ──│── 段落6
0.85 0.82 0.31 ↑ 0.88 0.29 ↑
切分点! 切分点!
(话题转换) (话题再转换)
- 优点:切分边界最自然,话题内聚性强,是质量最高的切分方式
- 缺点:计算开销大(每对相邻句子都要调用一次 Embedding)
- 适合:长文档、内容丰富多样的场景、对检索质量要求极高的场景
- Python 实现思路:
python
def semantic_split(document: str, similarity_threshold: float = 0.6):
sentences = split_into_sentences(document)
embeddings = [embed(s) for s in sentences]
chunks = []
current_chunk = [sentences[0]]
for i in range(1, len(sentences)):
similarity = cosine_similarity(embeddings[i-1], embeddings[i])
if similarity < similarity_threshold:
# 语义骤降 → 切分
chunks.append(''.join(current_chunk))
current_chunk = [sentences[i]]
else:
current_chunk.append(sentences[i])
chunks.append(''.join(current_chunk))
return chunks
⑤ 递归字符切分(Recursive Character Text Splitter)
LangChain 的默认方案,也是 最推荐的通用切分方案。按优先级顺序尝试切分符:
切分优先级:"\n\n" → "\n" → " " → ""
段落级 句子级 词语级 字符级
先尝试用双换行切(段落级),如果 chunk 还是太大 → 再用单换行切(句子级),以此类推,递归下降直到满足大小要求。
- 优点:灵活,兼顾文档自然结构和大小控制,"开箱即用"效果不错
- 缺点 :
chunk_size和chunk_overlap参数需要根据文档类型调优 - 适合 :通用场景、格式不确定的文档 ------ 也是大多数人的默认选择
python
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # 每个 chunk 最大字符数
chunk_overlap=50, # 相邻 chunk 重叠字符数
separators=["\n\n", "\n", "。", "!", "?", " ", ""],
length_function=len,
)
chunks = splitter.split_text(document)
⑥ 结构化文档切分(Structure-aware Chunking)
针对 Markdown、HTML 等有层级结构的文档,按标题/标签层级切分,保留结构信息。
Markdown 文档结构感知切分:
# 第一章 ← H1
## 1.1 概述 ← H2
...内容...
## 1.2 详细说明 ← H2
...内容...
# 第二章 ← H1
...
切分结果(每个 chunk 携带层级路径):
chunk_1: {h1: "第一章", h2: "1.1 概述", content: "..."}
chunk_2: {h1: "第一章", h2: "1.2 详细说明", content: "..."}
chunk_3: {h1: "第二章", content: "..."}
- 优点:最大程度保留文档层级关系,检索时可用结构信息增强相关性
- 缺点:需要感知文档格式,不同格式需要不同的解析器
- 适合:Markdown 文档、HTML 页面、API 文档、知识库网站
1.3 切分策略选择指南
| 文档类型 | 推荐策略 | Chunk Size | Overlap | 注意事项 |
|---|---|---|---|---|
| 纯文本(英文) | 固定长度 | 500-1000 tokens | 10-20% | 英文单词为基本单位 |
| 纯文本(中文) | 按句子 / 递归字符 | 300-800 字 | 1-3 句 | 中文字数 ≠ token 数 |
| 技术文档/API 文档 | 结构化切分 | 500-1500 字 | 少量 | 保留标题层级作为元数据 |
| 法律/合同 | 按条款 + 句子 | 200-500 字 | 2-4 句 | 精确度优先,宁可多切 |
| FAQ | 按条目(一问一答) | 不切 | 0 | 每条问答独立索引 |
| 论文/长文章 | 语义切分 | 1000-2000 字 | 10-15% | Embedding 成本高但效果好 |
| 多文档知识库 | 递归字符切分 | 500-800 字 | 10% | 通用首选 |
1.4 Overlap:重叠窗口的艺术

Overlap 指的是相邻 chunk 之间共享的内容。比如 chunk_size=500, overlap=100:
Chunk 1: ████████████████████████████░░░░░░ (字符 0-500)
Chunk 2: ░░░░░░░████████████████████████████ (字符 400-900)
Chunk 3: ░░░░░░░████████████████████████████ (字符 800-1300)
↑
重叠区域 = 100 字符
重叠解决的核心问题:避免关键信息恰好落在切分边界上被"腰斩"。
举个真实场景:一条完整的退款规定写在文档的第 497-520 字符处。如果不设置 overlap,这条规定被切割在 Chunk 1 的末尾(第 497-500 字符,只剩"退款"两个字)和 Chunk 2 的开头(第 501-520 字符,缺了主语)。两个 chunk 单独看都无法完整表达意思,检索时两个都匹配不上。
最佳实践:
- 通常设置 10~20% 的重叠率
- 句子级切分可设置 1-3 句的重叠(而非百分比)
- 不要过度重叠------overlap 太高会让 chunk 之间差异过小,K 个返回结果高度重复,浪费 LLM 上下文窗口
1.5 文档解析的挑战
不同格式的文档有各自的解析难点,这里给出实用的工具选择:
| 文档格式 | 难点 | 推荐工具 | 注意事项 |
|---|---|---|---|
| PDF(文本型) | 多栏布局、页眉页脚干扰 | PyPDF2, pdfplumber | pdfplumber 对表格提取更强 |
| PDF(扫描型) | 需要 OCR 识别 | Tesseract + pdf2image | 识别准确率影响最终检索效果 |
| Word (.docx) | 样式、表格、图片 | python-docx | 相对简单,生态成熟 |
| Markdown | 代码块、表格需保持格式 | 原生解析 | 注意 fenced code block 不要被切散 |
| HTML | 标签噪音、嵌套结构 | BeautifulSoup + 自定义解析器 | 需保留标题层级结构 |
| 图片/扫描件 | 文字提取依赖 OCR | PaddleOCR(中文强)、Tesseract | 可结合多模态 Embedding 直接向量化图片 |
统一建议 :为每种格式编写专门的 Loader,统一输出为
{content: str, metadata: dict}结构。metadata 中保留来源文件名、页码、标题层级、文档类型等信息,这些东西在检索阶段能大幅提升排序的准确性。
2. 检索:RAG 的灵魂环节
2.1 检索的基本流程
用户问题: "公司年假怎么申请?"
│
▼
① 查询向量化:Embedding 模型 → query_vector [0.23, 0.87, ...]
│
▼
② 相似度搜索:query_vector vs 向量库中所有文档向量 → 计算余弦相似度
│
▼
③ Top-K 筛选:取相似度最高的 K 个 chunk
│ K=3: [chunk_42(0.92), chunk_17(0.87), chunk_88(0.81)]
│
▼
④ 后处理:去重 → 过滤低分 → 按相似度排序 → 拼接为上下文
│
▼
⑤ 注入 LLM Prompt:
"根据以下参考信息回答用户问题... [chunk_42内容] [chunk_17内容] [chunk_88内容] ... 用户问题:公司年假怎么申请?"
2.2 多路检索:不止向量一种方法
很多系统只用了向量检索就上线了,但实际效果往往不尽如人意。这是因为向量检索擅长"语义近似"但不擅长"精确匹配"。一个典型翻车场景:
用户问:"API v3.0 的 breaking changes 有哪些?"
向量检索返回了一堆关于"API 设计原则"和"版本管理"的文档,却偏偏漏掉了标题就叫《API v3.0 Breaking Changes》的那篇文章------因为向量压根不关心"精确字符串匹配"。
解决方案:多路检索(Multi-Path Retrieval)

路径一:向量检索(Dense Retrieval)
- 原理:Embedding 语义匹配,找"意思相近"的内容
- 优势:能理解同义词、改写、跨语言语义
- 弱势:对专有名词、精确编号(工单号、API 版本号)、缩写不敏感
路径二:关键词检索(Sparse Retrieval --- BM25)
- 原理:基于词频统计,找"词长得像"的内容
- 优势:精确匹配专有名词、编号、代码片段极强
- 弱势:不理解同义词,"年假"匹配不到"带薪休假"
路径三(可选):基于元数据的过滤
- 原理:利用文档元数据做预过滤(时间、来源、分类标签)
- 优势:缩小检索范围,排除大量无关文档
- 使用方式:先按日期/分类过滤 → 再在子集上做向量+关键词混合检索
多路融合策略
三种主流融合方式:
① 结果合并(简单粗暴):
向量结果 Top-K1 ∪ 关键词结果 Top-K2 → 去重 → 交 Re-ranker 统一排序
② 加权融合(Reciprocal Rank Fusion, RRF):
为每条结果的倒数排名加权求和:
RRF_score = Σ 1/(k + rank_i),k 通常取 60
相比简单合并,RRF 能更好地平衡两路结果
③ 分层检索(先过滤再向量):
元数据过滤 → BM25 初筛 → 向量精排(适合超大规模、预分类场景)
何时上多路检索?
| 场景特征 | 单路向量即可 | 建议上多路 |
|---|---|---|
| 查询类型 | 自然语言问句为主 | 混合自然语言 + 精确查找 + 代码/编号 |
| 文档内容 | 叙述性、概念性 | 包含大量专有名词、编号、代码 |
| 检索失败模式 | 无 | 已知有"搜不到"的 case |
2.3 K 值的选择艺术
K 值(检索返回的文档数)直接影响答案质量。选太小漏信息,选太大塞噪音。

| K 值 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| 1-3 | 回答聚焦,不引入噪音 | 可能漏掉关键信息 | 知识库精准、问题简单、事实性问答 |
| 3-5 | 平衡覆盖度和聚焦度 ⭐ | - | 大多数通用场景的推荐起步值 |
| 5-10 | 覆盖全面,复杂问题友好 | 可能引入无关信息干扰 LLM | 问题复杂、需要综合多文档 |
| 10-20 | 不漏任何信息 | Token 成本高,LLM 容易被噪音误导 | 配合 Re-rank 使用,先粗筛再精选 |
最佳实践:先多捞(K=10~20),再用 Re-ranker 精选出 Top-3~5 给 LLM。这是质量与成本的帕累托最优解。
2.4 相似度阈值过滤
不是所有检索到的文档都是相关的。设置相似度阈值可以过滤低质量结果:
检索得分分布示意(余弦相似度):
████████████████████ 0.95 ← 高度相关,直接采纳
██████████████████ 0.88 ← 相关,采纳
██████████████ 0.75 ← 勉强相关,保留
───────── 阈值 0.70 ────────
████████ 0.62 ← 不太相关,过滤
█████ 0.55 ← 不相关,过滤
███ 0.41 ← 完全不相关,过滤
- 余弦相似度 0.7~0.8 是常用阈值起点
- 但注意:不同 Embedding 模型的相似度分布不同,阈值需要根据实际数据校准
- 防止"过滤过头":设置最少返回数(如至少 3 条),如果过滤后不足,自动放宽阈值
2.5 检索质量的量化
没有度量就没有优化。三个核心指标帮你量化检索效果:

① Hit Rate(命中率)
定义:正确答案是否出现在检索结果的 Top-K 中。
公式:Hit Rate@K = (正确答案出现过的查询数) / (总查询数)
示例:10 个测试查询,8 个的正确答案在 Top-5 中
→ Hit Rate@5 = 80%
- 适用:最直观的检索质量度量,适合快速判断"搜没搜到"
- 局限:不看排名------正确答案排第 1 和排第 K 都算命中,区分度不够
② MRR(Mean Reciprocal Rank)
定义:正确答案在检索结果中排名的倒数的平均值。
公式:MRR = (1/|Q|) × Σ(1/rank_i)
示例:
查询1:正确答案排第 2 → 贡献 1/2 = 0.5
查询2:正确答案排第 1 → 贡献 1/1 = 1.0
查询3:正确答案排第 5 → 贡献 1/5 = 0.2
MRR = (0.5 + 1.0 + 0.2) / 3 = 0.567
- 解读:MRR 越接近 1 越好,关注"答案是不是在前几位"
- 适用:正确答案只有一个的场景(如 FAQ 问答)
③ Recall@K
定义:检索返回的相关文档数 / 实际所有相关文档数。
公式:Recall@K = |检索到的相关文档 ∩ 所有相关文档| / |所有相关文档|
示例:系统中关于"年假"的文档有 5 篇,检索返回了其中 3 篇
→ Recall@5 = 3/5 = 60%
- 适用:需要"找全"多于"找对"的场景(如法律证据检索、学术文献检索)
- 与 Precision 配合:Recall 看找全了没,Precision 看找对没,通常二者此消彼长
2.6 Re-rank:检索的最后一道防线
即使前面做得再好,检索初筛的结果也难免有噪音。Re-rank(重排序) 是解决这个问题的关键一步。
核心思想:向量检索快但粗糙 → 用更强大的模型对 Top-K 结果做精细排序。
粗筛(向量检索) 精排(Re-ranker)
K=20 个候选 → 精选出 Top-3~5
毫秒级、近似匹配 百毫秒级、精确匹配
Embedding 模型 Cross-encoder 模型
为什么 Re-rank 更准?
| 向量检索(Bi-encoder) | Re-rank(Cross-encoder) | |
|---|---|---|
| 工作方式 | 分别编码 query 和 doc,再算相似度 | 把 query 和 doc 拼在一起编码 |
| 交互 | 无交互,query 和 doc 独立编码 | 有交互,attention 跨 query 和 doc |
| 速度 | 快(可预计算所有 doc 向量) | 慢(每个 pair 都要重新编码) |
| 精度 | 相对低 | 显著更高 |
常用 Re-ranker 模型:
- BAAI/bge-reranker-v2-m3:中文友好,开源可本地部署
- Cohere Rerank:API 服务,效果好,按量付费
- Cross-encoder/ms-marco-MiniLM-L-6-v2:轻量英文 Re-ranker
python
# Re-rank 调用示例
from FlagEmbedding import FlagReranker
reranker = FlagReranker('BAAI/bge-reranker-v2-m3')
pairs = [(query, doc) for doc in retrieved_docs]
scores = reranker.compute_score(pairs)
# 按 Re-rank 分数重新排序
reranked = sorted(zip(retrieved_docs, scores), key=lambda x: x[1], reverse=True)
top_docs = reranked[:3] # 精选 Top-3 送 LLM
本篇小结
| 知识点 | 一句话总结 |
|---|---|
| 切分的本质 | 在"语义聚焦度"和"上下文完整度"之间找平衡 |
| 六种策略 | 固定长度最简单、句子最常用、语义最精准、递归最通用、结构化最规整 |
| 递归字符切分 | LangChain 默认,通用首选,兼顾自然结构和大小控制 |
| Overlap | 防止信息在切分边界被"腰斩",推荐 10-20% |
| 为什么多路检索 | 向量管语义、BM25 管精确、元数据管范围,三路互补 |
| K 值实践 | 先多捞(K=10-20)+ Re-rank 精筛(取 Top-3~5) |
| Hit Rate | 最直观,看搜没搜到 |
| MRR | 看排名,答案是不是在前几位 |
| Re-rank | 检索最后一关,用精度换速度,Cross-encoder 远优于 Bi-encoder |
本文把文档切分和多路检索这两个 RAG 承上启下的环节讲透了。下一篇进入 Prompt 工程与 RAG 进阶优化------如何设计 Prompt 让 LLM 充分利用检索到的上下文?混合检索怎么做?查询改写有什么技巧?