文档切分策略
掌握文档切分的核心技术,选择合适的切分策略优化RAG效果
🎯 核心概念
为什么需要文档切分?
RAG系统的第一步就是将长文档切分成小块,这样做的原因:
- Token 限制:嵌入模型、重排器和生成模型各有输入上限,通常不能按生成模型的窗口来切块
- 检索精度:小块更容易匹配用户查询的具体信息
- 计算效率:控制单次输入成本;块越多,索引体积与召回开销也可能越大
- 语义聚焦:避免一个chunk包含过多无关主题
切分的核心挑战
关键平衡
粒度平衡:太大检索不准,太小丢失上下文
语义完整:避免在句子中间切断,保持逻辑连贯
重叠处理:边界信息的保留与冗余的权衡
🔬 文档智能预处理(切分前的关键步骤)
核心洞察
数据准备决定证据能否被正确读取,模型能力决定证据能否被正确使用。 数字原生文档通常先提取文本与结构;扫描件才需要 OCR。解析错误会把错误证据传到后续所有环节。
文档智能六阶段流水线
| 阶段 | 技术 | 作用 | 关键工具 |
|---|---|---|---|
| 1. 布局分析 | LayoutLM、DocLayout-YOLO | 识别标题、段落、表格、图片等结构元素 | LayoutLMv3、GNN |
| 2. OCR识别 | 光学字符识别 | 将图像中的文字转为可编辑文本 | PaddleOCR、Tesseract |
| 3. 表格识别 | 表格结构识别 | 从视觉网格重建结构化数据 | TableTransformer |
| 4. 公式识别 | 数学/化学公式解析 | 将公式图像转为LaTeX等格式 | Pix2Tex、Mathpix |
| 5. 图像分析 | VLM视觉语言模型 | 为图表生成文本描述 | GPT-4V、LLaVA |
| 6. 阅读顺序 | 逻辑流检测 | 确定多栏文档的正确阅读顺序 | 规则/ML混合 |
为什么布局分析至关重要?
下例展示组件职责,布局、OCR、表格和阅读顺序解析器由具体实现注入,不能直接当作可运行 SDK。每个输出元素应保留页码和边界框,便于回到原件核验。
# 反面案例:双栏PDF直接提取文本
# 结果:两栏句子被胡乱拼接,毫无逻辑
# 正确做法:先进行布局分析
class DocumentIntelligencePipeline:
"""文档智能处理流水线"""
def process(self, document):
# 1. 布局分析:识别结构元素
layout = self.layout_analyzer.detect(document)
# 输出:标题、段落、表格、图片的边界框和类型
# 2. 按元素类型分别处理
for element in layout.elements:
if element.type == 'table':
content = self.table_parser.extract(element)
elif element.type == 'formula':
content = self.formula_recognizer.parse(element)
elif element.type == 'image':
content = self.vlm.describe(element)
else:
content = self.ocr.extract(element)
element.content = content
# 3. 按阅读顺序重组
ordered_content = self.reading_order_detector.sort(layout)
return ordered_contentLayoutLM:布局感知的核心技术
LayoutLM通过多模态嵌入同时建模文本和布局信息:
- 文本嵌入:类BERT嵌入捕捉语义
- 1D位置嵌入:单词在序列中的顺序
- 2D位置嵌入:每个token的边界框坐标(x₀,y₀,x₁,y₁) — 突破性特征
- 图像嵌入:字体样式、颜色等视觉特征
关键规则:视觉上相邻的词语通常在语义上相关
📋 15种分块策略详解
基于《解锁RAG效能:15种分块策略秘籍》的实战总结
1️⃣ 逐行分块法(Line-by-Line Chunking)
核心原理:以换行符为分割点,每行独立成块
适用场景:
- 聊天记录、对话转录文本
- 问答式数据(客服对话、访谈实录)
- 结构化的列表数据
实战示例:
def line_chunking(text):
return text.strip().split('\n')
# 示例
dialogue = """Alice: 嗨Bob,今天下午3点有空通话吗?
Bob: 可以,想讨论项目进展吗?
Alice: 是的,还要聊聊客户会议的事
Bob: 没问题!3点见"""
chunks = line_chunking(dialogue)
# 结果:4个独立的对话块优势:保留对话天然独立性,支持细粒度检索
注意:过短的行可能导致上下文缺失
2️⃣ 固定尺寸分块法(Fixed-Size Chunking)
核心原理:按固定字符数/Token数分割,无视语义边界
适用场景:
- 杂乱无章的非结构化文本
- OCR识别结果、网页爬取数据
- 老旧扫描文档的数字化内容
实战示例:
def fixed_size_chunking(text, chunk_size=500, overlap=50):
if chunk_size <= 0 or not 0 <= overlap < chunk_size:
raise ValueError("要求 chunk_size > 0 且 0 <= overlap < chunk_size")
chunks = []
start = 0
while start < len(text):
end = min(start + chunk_size, len(text))
chunk = text[start:end]
chunks.append(chunk)
if end == len(text):
break # 尾块必须终止,避免 overlap 让循环永不结束
start = end - overlap
return chunks优势:实现数据块标准化,便于统一处理
风险:可能割裂完整语义单元,建议仅在万不得已时使用
3️⃣ 滑动窗口分块法(Sliding Window Chunking)
核心原理:固定窗口大小,按步长滑动切分,保留重叠区域
适用场景:
- 连续性强的技术文档
- 小说、散文等文学作品
- 需要保持上下文连贯的长文本
关键参数:
window_size:窗口大小(如512 tokens)step_size:滑动步长(如400 tokens,重叠112 tokens)
4️⃣ 句子级分块法(Sentence-Based Chunking)
核心原理:以句号、问号、感叹号等句子边界为切分点
优势:保持语义完整性,适合问答系统
实现:结合NLP库(如spaCy、NLTK)进行句子分割
5️⃣ 段落分块法(Paragraph Chunking)
核心原理:以段落(双换行符\n\n)为切分单位
适用场景:
- 结构化文档(论文、报告)
- 博客文章、新闻稿
- 技术文档
6️⃣ 递归分块法(Recursive Chunking)
核心原理:从大单元开始,逐层分割过大的分块,直至符合尺寸要求
算法流程:
- 先按段落分割
- 段落过大 → 按句子分割
- 句子仍过大 → 按固定长度分割
实战示例:
def recursive_chunking(text, max_size=500):
# 第1层:段落分割
paragraphs = text.split('\n\n')
chunks = []
for para in paragraphs:
if len(para) <= max_size:
chunks.append(para)
else:
# 第2层:句子分割
sentences = para.split('。')
for sent in sentences:
if len(sent) <= max_size:
chunks.append(sent)
else:
# 第3层:固定长度分割
chunks.extend(fixed_size_chunking(sent, max_size, overlap=0))
return chunks7️⃣ 表格分块法(Table Chunking)
核心原理:将表格作为独立分块(整体或逐行分割)
适用场景:
- 财务报告、销售数据
- 科学论文的实验结果
- 包含结构化数据的混合文档
处理策略:
- 整表保留:小表格整体作为一个chunk
- 逐行分割:大表格按行切分,保留表头
- 语义分组:按业务逻辑将相关行组合
8️⃣ 章节/标题分块法(Section-Based Chunking)
核心原理:按文档的层级结构(H1、H2、H3标题)进行切分
优势:保留文档逻辑结构,便于理解上下文关系
9️⃣ 语义分块法(Semantic Chunking)
核心原理:基于语义相似度,将相关内容聚合为一个chunk
实现方式:
- 计算句子间的embedding相似度
- 设定相似度阈值进行聚类
- 动态调整chunk边界
计算开销:句子级向量化与边界判断增加成本,不保证优于结构化递归切分。语义连续也不意味着证据完整,尤其要保留否定、条件与表格说明。
🔟 Token分块法(Token-Based Chunking)
核心原理:按模型的Token数量进行精确切分
重要性:直接对应模型输入限制,是最精确的切分方式
import tiktoken
def token_chunking(text, model="gpt-3.5-turbo", max_tokens=500):
encoding = tiktoken.encoding_for_model(model)
tokens = encoding.encode(text)
chunks = []
for i in range(0, len(tokens), max_tokens):
chunk_tokens = tokens[i:i + max_tokens]
chunk_text = encoding.decode(chunk_tokens)
chunks.append(chunk_text)
return chunks11️⃣ 父子分块(Parent–Child Chunking)
小块负责匹配,命中后取回所属父段落或章节作为证据。它适合问题简短、回答需要上下文的场景;父块回填必须去重并受 token 预算约束,否则几个小块可能反复带回同一大段。
12️⃣ 句子窗口(Sentence Window)
以句子建立检索粒度,命中后扩展相邻若干句。扩展时不能跨租户、文档版本或不相关章节;检查窗口是否包含代词指代和例外条件,而不只是固定取前后 N 句。
13️⃣ 代码结构分块(AST-aware Chunking)
按函数、类或语法树节点切分,保留文件路径、符号名、签名、注释和必要导入。超长函数需要继续切分并记录所属符号。调用关系问题还可能需要邻接符号或图检索,单个函数块不足以回答。
14️⃣ 问答对分块(QA-pair Chunking)
FAQ 将问题与回答作为同一单元,并保存业务条件、时间与来源。多轮客服对话按问题解决过程分组,不能让“可以”“不支持”等回答脱离前文;合成问题只做检索辅助,不替换原始事实。
15️⃣ 上下文补充分块(Context-enriched Chunking)
为片段加入标题路径、文档摘要或模型生成的局部背景,提高独立检索时的可理解性。生成补充信息要与原文分字段保存并核验;引用仍指向原文,避免把补充摘要中的推测升级为事实。
⚙️ 分块策略选择指南
按文档类型选择
| 文档类型 | 推荐策略 | 理由 |
|---|---|---|
| 对话记录 | 逐行分块 | 保持问答独立性 |
| 技术文档 | 章节分块 + 递归 | 结合结构与大小控制 |
| 学术论文 | 段落分块 | 逻辑单元完整 |
| 小说散文 | 滑动窗口 | 保持情节连贯 |
| 表格数据 | 表格专用 | 保持结构化信息 |
| OCR文本 | 固定尺寸 | 结构混乱时的兜底方案 |
按应用场景选择
| 应用场景 | 策略组合 | 参数建议 |
|---|---|---|
| 问答系统 | 句子级 + Token控制 | 每chunk 1-3个完整句子 |
| 文档摘要 | 段落级 + 重叠窗口 | chunk_size=800, overlap=100 |
| 代码检索 | 函数级 + 递归 | 按函数/类边界切分 |
| 多语言文档 | 语义分块 | 考虑语言特征 |
🛠️ 实战最佳实践
LangChain 实现
安装 langchain-text-splitters。下例的默认长度函数是 len,所以 1000/200 是字符数,不是 token 数;token 限额需要指定计数器或使用对应 tokenizer。见 LangChain 递归切分官方文档。
from langchain_text_splitters import RecursiveCharacterTextSplitter
# 中文优化的递归分块
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
separators=["\n\n", "\n", "。", "!", "?", ";", " ", ""],
keep_separator=True # 保留分隔符
)
document = "# 年假规定\n\n员工满足连续工作条件后,按规定申请年假。"
chunks = splitter.split_text(document)混合策略实现
下面是接口示意,section_based_chunking 需由 Markdown/HTML 等结构解析器实现;不是仅按字符串匹配标题就能处理所有文档。
def hybrid_chunking(text, doc_type="general"):
if doc_type == "dialogue":
return line_chunking(text)
elif doc_type == "structured":
return section_based_chunking(text)
else:
# 默认递归策略
return recursive_chunking(text, max_size=800)元数据保留
def chunking_with_metadata(text, source_file, page_num):
chunks = recursive_chunking(text)
metadata_chunks = []
for i, chunk in enumerate(chunks):
metadata_chunks.append({
"content": chunk,
"source": source_file,
"page": page_num,
"chunk_id": f"{source_file}_{page_num}_{i}",
"chunk_size": len(chunk)
})
return metadata_chunks⚠️ 常见问题与解决
问题1:上下文丢失
现象:切分后的chunk缺乏完整信息
解决:
- 在相同上下文 token 预算下比较不同 overlap;不默认采用 20–30%,先检查重复证据占比
- 使用父子分段:小chunk检索,大chunk生成
- 保留关键元数据(标题、章节信息)
问题2:检索不精准
现象:返回的chunk与查询不匹配
解决:
- 将标准证据映射回原文,确认是块太长引入噪声,还是块太短切断条件,再调整大小
- 优化分隔符选择
- 结合重排序(Rerank)
问题3:计算开销大
现象:语义切分速度慢
解决:
- 预计算文档embedding
- 使用缓存机制
- 混合策略:重要文档用语义分块,其他用递归分块
切分方案怎么验收
为每个问题标出原文中的最小证据区间,再观察切分后是否仍能完整覆盖。固定嵌入模型、候选 K、重排器及最终上下文 token 预算,对比切分方案;只固定 K 会让大块方案获得更多可见信息,比较并不公平。
至少记录:证据完整率、Recall@K、引用定位正确率、上下文重复率、索引大小、解析失败率与入库耗时。重点抽查跨页表格、页眉页脚、脚注、代码块和“除非/不适用”等限定语。可检索块需要稳定文档 ID、版本、页码/字符区间、标题路径和权限标签;单纯顺序编号不足以支持更新追溯。
Token 切分示例仅展示计数思路:按 token 数硬切可能拆开字符对应的字节序列,也可能割断完整句子。生产中优先沿原文边界分段,再用 tokenizer 检查上限;截断、解码与重新编码后的长度都要验证。
🔗 相关阅读
- RAG范式演进 - 了解RAG技术发展脉络
- Embedding技术详解 - 理解向量化原理
- 检索策略优化 - 切分后的检索技巧
- RAG评估方法 - 评估切分效果
相关文章:
外部资源: