文档分割提高RAG的常用技巧
文档分割的核心作用
- 检索效率与精度:
- 效率:恰当的分割能大幅减少索引体积和计算开销,提升检索速度(经验上可优化30-50%)。
- 精度:避免过大片段包含无关噪声,或过小片段丢失关键上下文,使检索结果更相关、更聚焦。
- 上下文完整性:
- 良好的分割确保每个片段尽可能包含一个完整的语义单元(如一个论点、一个事件描述、一个QA对),避免信息被生硬切断导致的语义碎片化,为后续生成提供坚实基础。
- 生成内容质量:
- 实验数据和实践经验表明,优化后的分割策略能显著提升LLM生成答案的准确性、相关性和连贯性(准确率提升40-60%的案例并不罕见),因为它确保了输入LLM的上下文片段本身是信息完整且目标明确的。
常用文档分割技巧
1. 固定长度分割法
适用场景:标准化文档(如普通文本文档、无明显强结构的网页内容)、追求简单快速实现的场景,或作为其他方法的预处理步骤。
原理:严格按照字符数或Token数进行切割。
示例(LangChain):
from langchain.text_splitter import CharacterTextSplitter
text_splitter = CharacterTextSplitter(
chunk_size=1000, # 每个片段最大字符数
chunk_overlap=200, # 相邻片段重叠字符数
separator="\n", # 优先在换行符处分割,其次按字符切
length_function=len # 计算长度的方法(此处按字符数)
)
chunks = text_splitter.split_text(your_text)
参数优化建议与考量:
chunk_size
(核心):- 通用范围:500-1500字符(或对应Token数)是常见起点。
- 文档类型:
- 技术文档/论文:800-1500字符(需保留公式、代码上下文)
- 新闻/博客/社交媒体:400-800字符
- 对话记录/邮件:300-600字符(保持对话轮次完整)
- LLM上下文窗口限制:必须远小于LLM的最大上下文窗口(需预留空间给用户问题、系统指令和生成结果)。
chunk_overlap
(关键):- 目的:防止关键信息(如恰好位于分割点的术语、结论)丢失,提高上下文连续性。
- 建议:通常设置为
chunk_size
的10%-25%。信息密度高或结构松散文档可适当增加。
separator
(提升语义):- 设置合理的分割符优先级(如
["\n\n", "\n", " ", ""]
),优先在段落、句子、单词边界处切割,避免在单词或数字中间切断。
- 设置合理的分割符优先级(如
length_function
:根据需求选择按字符数 (len
) 或 Token 数(如tiktoken
库计算)分割。LLM处理基于Token,按Token分割通常更精确匹配其处理能力。
优点:实现简单,计算高效。
缺点:容易破坏自然语义结构(切断句子、段落),可能导致检索片段包含不完整信息。
2. 结构化文档分割法
适用场景:具有明显层级结构的文档(如 Markdown, HTML, LaTeX, PDF with Headings, Word with Styles)。
原理:利用文档固有的结构标记(标题、章节、列表等)作为分割点,保持逻辑单元的完整性。
示例(LangChain - Markdown):
from langchain.text_splitter import MarkdownHeaderTextSplitter
headers_to_split_on = [
("#", "Header 1"), # 一级标题
("##", "Header 2"), # 二级标题
("###", "Header 3"),# 三级标题
]
markdown_splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=headers_to_split_on,
strip_headers=False # 可选:是否在输出中保留标题文本
)
chunks = markdown_splitter.split_text(your_markdown_text)
关键点:
- 结构识别:分割器需要能解析文档的特定结构(Markdown标题、HTML标签等)。
- 保留元信息:分割时通常会将父级标题信息(如
Header 1 > Header 2 > Header 3
)作为元数据添加到片段中,极大增强检索和生成时的上下文理解。 - 粒度控制:通过选择分割的标题级别(如只按
##
切)可以控制片段的粒度。
优点:最大程度保持语义和逻辑单元的完整性,片段自带结构信息,检索和生成质量通常很高。
缺点:依赖文档的结构化程度和质量,对非结构化或结构混乱的文档效果不佳。
3. 递归分割法(推荐常用)
适用场景:通用性强,尤其适用于混合内容或结构不清晰但仍有部分分隔符(如段落、句子)的文档。是固定长度分割的智能升级版。
原理:采用分层分割策略。优先使用较大的分隔符(如双换行\n\n
)将文本分割成大块。如果大块仍然超过chunk_size
,则使用下一级分隔符(如单换行\n
)继续分割。依此类推,直到分割成满足chunk_size
要求的片段。这尽可能地在较大语义边界处切割。
示例(LangChain):
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
separators=["\n\n", "\n", ". ", "? ", "! ", " ", ""] # 分割符优先级列表
)
chunks = text_splitter.split_text(your_text)
参数说明:
separators
:最关键参数。定义分割符的优先级列表。分割器会顺序尝试列表中的分隔符。例如,先尝试用"\n\n"
分,分出来的块如果还太大,再用"\n"
分这个块,还不够小再用". "
分,以此类推,直到满足chunk_size
或没有分隔符可用(最后按字符切)。chunk_size
,chunk_overlap
:作用同固定长度分割法。
优点:
- 在无法利用强结构时,比纯固定长度分割更能尊重自然语义边界(段落、句子)。
- 通用性好,适用于多种文档类型。
- 是 LangChain 等库中最常用且推荐的默认文本分割器。
缺点:对于完全没有明显分隔符(如长段落无换行)的文本,最终还是会退化成按字符/Token切分。
分割前需要思考的地方
- 理解文档特性是第一要务:
- 分析文档类型(技术手册、新闻、对话、代码)、结构强度(Markdown/HTML vs 纯文本)、信息密度、典型长度。
- 结构化文档优先尝试结构化分割法。
- 通用文本或混合内容优先使用递归分割法。
- 固定长度法作为兜底或预处理。
- 案例:处理医学研究论文(PDF)时,发现其有清晰的章节结构(摘要/方法/结果)。
- 操作:
- 使用PDF解析器提取标题层级
- 选择结构化分割法,按
## 方法
、## 结果
等二级标题切分 - 每个片段自动附加"章节标题"元数据
- 错误示范:若用固定长度分割,可能将"实验组疗效数据"表格拦腰切断
- 保持语义单元完整:
- 这是最高原则。避免在句子中间、重要论点中途、关键实体(人名、地名、术语)中间、表格行间、代码块中间切断。利用分隔符和重叠尽可能规避。
- 案例:分割用户客服对话日志(文本格式)
- 操作:
- 设置递归分割器参数:
separators=["\nUser:", "\nAgent:"]
- 确保每个片段包含完整对话轮次(如用户提问+客服回复)
- 设置递归分割器参数:
- 错误示范:若用
chunk_size=300
固定分割,可能使回复"您的订单号是AB-X"被拆成[“您的订单”,“号是AB-X”]
- 重叠(Overlap)不是万能的,但不可或缺:
- 合理设置
chunk_overlap
能有效缓解分割点信息丢失问题,提高上下文连贯性。 - 但过大的重叠会显著增加索引大小和检索冗余,需平衡。
- 合理设置
- 案例:法律合同分割(条款间存在引用关系)
- 操作:
- 设置
chunk_size=1200
,chunk_overlap=300
(25%) - 当分割点出现在"见第3.2条"时,overlap确保下个片段包含3.2条开头
- 设置
- 平衡实践:对新闻摘要等低密度文本,overlap降为10%避免冗余
- 考虑LLM的上下文窗口(Context Window):
- 分割后的片段长度(加上用户问题、指令等)必须严格适配你使用的LLM的上下文窗口限制。预留足够空间给生成。
- 案例:使用GPT-4-128K处理财报分析(平均每份PDF 50页)
- 操作:
- 计算:用户问题(200token) + 系统指令(100token) + 生成空间(500token)
- 确定
chunk_size=800token
(预留足够buffer)
- 错误示范:若设置
chunk_size=1500token
,当需要同时插入2个片段时,总长度超模型限制
- 考虑检索效率与索引大小:
- 片段过小会导致索引条目过多,增加检索计算量和存储成本。
- 片段过大会降低检索精度(包含无关信息)并消耗更多LLM Token。
- 在语义完整性和检索效率间寻求平衡点。
- 案例:电商百万级商品描述库
- 操作:
- 测试发现:
chunk_size=600
时召回率85%,索引大小120GB - 优化为
chunk_size=800
后召回率82%,索引降至80GB(接受3%召回下降换取40%存储节省)
- 测试发现:
- 平衡点:在召回率下降≤5%范围内最大化chunk_size
- 实验、评估与迭代:
- 没有放之四海而皆准的最优参数! 必须针对你的具体数据和你的RAG Pipeline进行实验。
- 评估指标:检索召回率(Recall)@K、检索结果相关性、生成答案的准确性(Factual Accuracy)、流畅性、人工评估。
- 调整
chunk_size
,overlap
,separators
,观察效果变化。A/B测试不同策略非常有效。
- 案例:科技博客RAG系统优化
- 操作:
- A组:
chunk_size=500, overlap=0
- B组:
chunk_size=800, overlap=100
- 评估:B组在"解释技术原理"类问题准确率提升37%
- 迭代:针对"代码示例"问题,额外增加按代码块分割策略
- A组:
- 元数据是黄金:
- 在分割时尽可能保留和附加元数据(来源文件名、章节标题、页码、时间戳等)。这些信息对检索排序和LLM理解片段来源至关重要。结构化分割法通常天然支持此功能。
- 案例:企业制度文档库版本管理
- 操作:
- 分割时附加元数据:
{"doc_id": "HR-2024", "section": "休假制度", "effective_date": "2024-01-01"}
- 当用户问"最新病假政策"时,检索器优先返回带
effective_date≥2024
片段
- 分割时附加元数据:
- 效果:避免返回已过期的2023年政策片段
- 处理长上下文依赖:
- 对于需要跨越多个片段才能理解的复杂问题(如文档摘要、多步骤推理),分割策略本身可能不够。需要结合其他技术(如父文档检索、句子窗口检索、摘要链等)。
- 案例:研报中的跨页数据对比(如"图3"在P10,"分析结论"在P12)
- 操作:
- 第一层:用结构化分割法按章节切分
- 第二层:对"财务分析"章节采用父文档检索
# 子片段(用于检索)
chunks = split_text(chunk_size=400)
# 父片段(用于生成)
parent_chunks = split_text(chunk_size=2000)
- 当检索到子片段"见图3趋势"时,自动关联所属的父片段(完整分析章节)