【AI应用开发】 RAG篇(三):文档切分与多路检索

【AI应用开发】 RAG篇(三):文档切分与多路检索


前言

前两篇我们讲清了 RAG 的整体架构和向量检索原理。但这远不足以让系统"好用"------一条最容易被忽视却最为致命的链路是:文档切不好,检索等于白做

本文深入拆解 RAG 中两个承上启下的关键环节:文档切分(Chunking)多路检索(Multi-Path Retrieval)。六种切分策略各有什么优劣?K 值怎么选?什么场景该上多路检索?读完本文你会对"怎么切、怎么搜"建立完整的工程直觉。


目录

  1. 文档处理与切分策略
    • 1.1 为什么需要切分
    • 1.2 六种切分策略详解
    • 1.3 切分策略选择指南
    • 1.4 Overlap:重叠窗口的艺术
    • 1.5 文档解析的挑战
  2. [检索: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_sizechunk_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 充分利用检索到的上下文?混合检索怎么做?查询改写有什么技巧?

相关推荐
通问AI1 小时前
2026年AI短剧技术现状:全链路AIGC生产已达95%,但用户完播率不足15%
人工智能·aigc
_Jimmy_1 小时前
Agent引用数据库知识过时的增量同步方案
人工智能·python·langchain
qq_454245032 小时前
Systemprompt 体系全览:形式化公理驱动的分层系统设计
人工智能
大龄码农有梦想2 小时前
传统的 BPMN 工作流审批和 AI 工作流有什么区别?
人工智能·流程引擎·工作流·ai agent·ai工作流·审批流·智能体平台
DO_Community2 小时前
Claude Opus 5 现已上线 DigitalOcean AI 推理云
人工智能·llm·agent·claude
幸福指北2 小时前
🚀 开源了,一个人 + AI 肝出一个 AI 终端 | AShell 技术分享
运维·人工智能·ai·终端
可以飞的话2 小时前
一、机器学习概述
人工智能·机器学习
sunneo2 小时前
磐石2.0发布,科学建模新突破
人工智能
煎饼学大模型2 小时前
Agent 的“大脑-手“解耦架构:当推理层和工具执行层各自独立演进
数据库·人工智能·oracle·架构·agent
南京码讯光电技术有限公司3 小时前
2026年4G/5G工业CPE推荐:从极端场景看硬核选型
人工智能