文档切分,才是RAG落地中最容易翻车的环节
做RAG(检索增强生成)的朋友应该都有这种体会:模型选型、嵌入(Embedding)方案、向量库搭建,这些环节都有成熟的技术方案和大量公开资料可以参考。但真正到了把业务文档喂给系统的那一步,切分策略往往直接决定了检索命中率的上限。切得太粗,检索粒度不够,召回内容混杂;切得太细,语义碎片化,上下文丢失严重。可以说,文档切分是整个RAG流水线里最"吃经验"的一环。
这篇文章聚焦三种最常见的文档格式——PDF、Word、TXT,结合我实际跑过的项目经验,聊聊不同格式的切分差异、工具选型和参数配置思路。这套方案在我参与的多个知识库项目中验证过,能覆盖绝大多数办公场景下的文档处理需求,适合正在搭建知识库问答系统、做企业文档检索,或者刚接触RAG想少踩坑的开发者参考。
1. 不同格式的核心差异,决定了切分策略的分野
1.1 统一切分方案为什么行不通
很多第一次做RAG的朋友喜欢偷懒:不管什么格式的文档,全部转成纯文本,然后用同一个切分器处理。这种做法在Demo阶段问题不大,但一旦进入真实业务场景,检索质量会断崖式下跌。
原因在于,PDF、Word、TXT三种格式的"信息结构"完全不同。TXT是纯文本流,只有段落和换行,没有任何额外的结构信息;Word在段落之上还有标题层级、目录、表格、页眉页脚等富文本结构;PDF最复杂,不仅要区分文本型PDF和扫描件,还要处理多栏排版、页眉页码、表格区域等视觉布局信息。
如果全部用统一的纯文本切分方案,Word的标题结构被拉平,PDF的多栏内容被错误拼接,扫描件干脆什么都抽不出来。这就像用同一把钥匙开三把不同的锁——也许能碰巧打开一把,但大部分场景下只会把锁芯搞坏。
1.2 格式感知的切分思路
正确的思路是:先做格式感知的解析(Format-Aware Parsing),再做基于结构的分块(Structure-Aware Chunking)。也就是说,第一步把每种格式特有的结构信息提取出来,第二步利用这些结构信息做切分决策,而不是盲目按照固定长度硬切。
举个例子。同是PDF格式,一份是某公司年报,双栏排版,每页页眉有公司名,页脚有页码;另一份是某产品说明书,单栏横排,有明确的章节结构。这两种PDF如果都按普通的文字流切分,年报的文字会按视觉顺序错乱拼接,说明书的结构层级会丢失。正确答案是先检测布局,识别出页眉页脚和双栏区域,把内容恢复成正确的阅读顺序,再根据章节边界进行切分。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. PDF切分方案:先解决"读得对",再解决"切得好"
2.1 文本型PDF和扫描型PDF的识别与处理路线
PDF切分的第一步不是选切分参数,而是判断这份PDF属于文本型还是扫描型。文本型PDF的文字是真实可提取的字符数据,扫描型PDF本质上是图片,需要OCR才能拿到文字内容。
判断方法很简单:用PDF阅读器打开文档,尝试选中一段文字。如果能选中并复制,就是文本型;否则就是扫描型。更批量化的做法是用工具检测页面是否包含文本层——比如用PyMuPDF(fitz库)检查每个页面的page.get_text()返回值是否为空。
处理路径分两条:
文本型PDF,直接用解析库提取文本和位置信息。扫描型PDF,需要先走OCR流程。OCR方案我推荐PaddleOCR或者Tesseract,前者中文识别效果好一些,后者部署更轻量。这里有个经验:OCR之后的文本会丢失原始布局,建议OCR时开启"版面分析"选项(PaddleOCR的--layout参数),这样输出的文本能带着段落边界信息,后续切分会轻松很多。
提示:扫描型PDF的OCR阶段如果偷懒不做版面分析,整页文字会被揉成一团输出,后续切分基本靠猜,检索效果会非常差。这一步值得多花时间。
2.2 PyMuPDF提取文本与布局信息的关键操作
对于文本型PDF,我的主力工具是PyMuPDF。它解析速度快,而且能拿到文字块的坐标信息,这是实现布局感知切分的基础。
基础的提取代码长这样:
python复制import fitz # PyMuPDF
def extract_pdf_blocks(path):
doc = fitz.open(path)
blocks = []
for page in doc:
# blocks 包含文本块的位置和内容
page_blocks = page.get_text("blocks")
blocks.append(page_blocks)
return blocks
拿到每个文本块之后,可以做三件关键的事:
第一,识别页眉页脚。页眉页脚的特征是纵向位置上固定在页面的顶部或底部,并且内容在每一页重复出现。可以通过纵向坐标阈值直接过滤——比如页面高度为842点(A4纵向),页眉区域就是y坐标在0到60之间的区域,页脚是y坐标在782到842之间的区域。这里还要加一个内容去重逻辑:页眉通常包含公司名、文档名,页脚通常是页码,这些内容不需要进向量库,直接丢弃。
第二,处理多栏排版。判断一份PDF是否双栏,要看文本块的横向分布特征。简单做法是:统计每页文本块中心的x坐标分布,如果呈现出明显的左右两个聚类,就是双栏布局。处理双栏时,不要按文本块的原始顺序拼接,而应该先提取左栏所有文本,再提取右栏所有文本,最后拼接成一个完整的阅读流。
第三,处理跨页段落。很多文本型PDF在页面底部会断行,一个段落跨了两页。切分时如果按页硬切,这个段落就被劈成两半,语义就碎了。解决办法是检查段落的最后一个字符:如果末尾没有句号、问号、感叹号等终止符,说明段落未结束,需要和下一页开头的文本拼接。
2.3 基于视觉布局的切分点选择
做完布局恢复之后,PDF的切分点选择就水到渠成了。这时候可以利用PDF的字体大小和样式信息来识别标题——字号明显大于正文、加粗、或者颜色不同的文本块,大概率是章节标题,可以作为切分的天然边界。
PyMuPDF提取文本时还能拿到字体信息:
python复制def detect_headings(page):
d = page.get_text("dict")
for block in d["blocks"]:
if "lines" in block:
for line in block["lines"]:
for span in line["spans"]:
# span["size"] 是字号,span["font"] 是字体名
# span["flags"] 的低位表示是否加粗
if span["size"] > 15: # 阈值按实际文档情况调整
print("可能是标题:", span["text"])
识别出标题后,切分逻辑就清晰了:每个标题及其下属内容作为一个大块,大块内部再按长度进行细粒度切分。这种"先分章节,再切段落"的两级策略,能显著提升检索命中率。
3. Word文档切分方案:充分利用结构化的先天优势
3.1 为什么Word是最容易切分的格式
Word文档在所有格式里其实是最"友好"的——它天然是结构化的。标题有标题样式(Heading 1/2/3),段落有段落标记,表格有行列结构,目录有锚点。这些结构信息如果能充分利用,切分效果会远好于PDF和TXT。
可惜的是,很多人在做RAG时,第一步就把Word转成了纯文本,把所有结构信息丢得一干二净。这个操作等于把到手的优势白白浪费了。
3.2 python-docx解析标题层级的方法
处理Word文档,我的首选工具是python-docx。它可以直接读.docx文件内的XML结构,拿到段落样式信息。
python复制from docx import Document
def extract_structure(path):
doc = Document(path)
structure = []
current_h1 = None
current_h2 = None
for para in doc.paragraphs:
style = para.style.name
if style == "Heading 1":
current_h1 = para.text
current_h2 = None
elif style == "Heading 2":
current_h2 = para.text
elif para.text.strip(): # 非空正文段落
structure.append({
"h1": current_h1,
"h2": current_h2,
"text": para.text
})
return structure
这套逻辑的核心思想是:给每个正文段落打上"所属章节"的标签,这样后续切分时可以按章节边界聚合。另外Python-docx还能读取文档里的表格:
python复制for table in doc.tables:
for row in table.rows:
cells = [cell.text.strip() for cell in row.cells]
print(" | ".join(cells))
表格内容建议单独提取、单独处理,不要和正文混在一起切分。因为表格是二维结构,强行按行切分会导致一列里的内容信息丢失;强行按列切分又破坏行的语义。我常用的做法是把表格整体作为一个特殊块,用JSON或Markdown格式序列化后存储,这样既保留了结构,又方便后续检索。
3.3 保留结构的切分策略实战思路
Word文档的切分优先级应该是:目录结构 > 标题层级 > 段落边界 > 长度限制。
具体来说:
第一优先,识别目录。如果文档有自动生成的目录,那么目录本身就是最好的章节地图。可以直接从目录提取章节标题列表,然后到正文中按标题定位章节边界,逐章切分。
第二优先,利用标题层级。对没有目录的文档,按Heading 1/2/3的层级递归切分。Heading 1切出大章节,大章节里再按Heading 2切成小节,依此类推。每个最小的章节单元,如果内容长度还在合理范围内,就直接作为独立chunk;如果过长,再按段落边界继续切分。
第三,处理无标题的正文段落。存在一部分文档完全没有使用标题样式,所有段落都是普通文本。这时候就只能退回到长度切分方案,但要在段落边界处切开,避免在段落中间硬切。
4. TXT纯文本切分:启发式规则与递归切分的配合
4.1 TXT总是被低估了
TXT格式看起来最简单,但在实际业务里,很多重要的数据源恰恰是TXT——比如日志文件、代码片段、爬取的网页正文、聊天记录导出等。TXT的问题在于没有任何格式信息,切分只能依赖内容本身的规律。
好在人类的文本是有模式的。例如技术文档通常会有一级标题和二级标题(比如用#和##标记);代码里通常有函数定义和注释;日志里有时间戳;网页正文转TXT后往往保留了HTML标签的痕迹。这些模式都可以作为切分依据。
4.2 基于正则的启发式切分方法
我的TXT切分方案是"三级递进":
先检测内容模式,识别文档类型。例如用正则匹配,如果每行都有时间戳,就按时间戳切段:
python复制import re
def split_log_text(text):
# 按时间戳分组的日志切分示例
timestamp_pattern = re.compile(r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}')
chunks = []
current_chunk = []
for line in text.splitlines():
if timestamp_pattern.match(line):
if current_chunk:
chunks.append("\n".join(current_chunk))
current_chunk = []
current_chunk.append(line)
if current_chunk:
chunks.append("\n".join(current_chunk))
return chunks
再匹配明确的章节标题。很多技术文档的标题有明显特征:行首以#开头,或者全部大写,或者前面带有"第X章"字样。这些标题行就是天然的切分点。
最后,对没有任何模式识别的纯文本,执行递归字符切分(Recursive Character Splitting)。这种方式特别适用于把长文本按语义单元做切分,优先保留段落边界;如果没有段落边界,再按句子边界切分;最后兜底才按字符数量硬切。
4.3 长文本的滑动窗口处理
TXT文档还有一类常见场景:超长文本,比如几十万字的整本小说、数据集文本、监管法规原文。这类文档切分时的核心矛盾是——既要保证每个chunk的长度在嵌入模型的token限制之内,又不能让chunk之间的语义断层。
我采用的是滑动窗口方案。设定chunk_size为800字符(中文场景一个字符大约2个token),overlap为100字符。从文本开头,每次前进chunk_size - overlap的长度,形成一个窗口,保证相邻窗口间有重叠区域。这样做的目的是:如果一个语义完整的知识点恰好横跨了两个chunk的边界,重叠部分确保这个知识点在两个chunk里都完整出现,检索时不会因为边界截断而丢失信息。
5. 切分参数的选择逻辑:chunk_size和overlap应该怎么配
5.1 只看token数上限是不够的
很多文章会告诉你"chunk_size设为1024,overlap设为128",然后就没有然后了。但在实际操作里,参数要看两层约束。
第一层约束是嵌入模型和语言模型的token限制。比如底层模型的上下文窗口是4096,检索到的chunk还要加上用户的问题和提示词模板,一起塞进上下文。所以chunk的总token数通常控制在800-1500之间比较安全,给提示词和回答留出足够空间。
第二层约束是检索精度的"语义完整性"要求。切出来的chunk应该是一段有头有尾、语义自洽的文字。如果一个chunk正好是半句话,或者中间被硬切断一个列表,即使长度再合适,检索效果也会很糟糕。
5.2 不同格式的参数推荐
我实测下来比较稳妥的起始参数如下:
| 文档格式 | 切分主策略 | 推荐chunk_size | 推荐overlap | 追加说明 |
|---|---|---|---|---|
| PDF(文本型) | 布局感知 + 标题识别 | 1000字符 | 150字符 | 先恢复阅读顺序再去重 |
| PDF(扫描型) | OCR + 段落边界 | 800字符 | 100字符 | OCR文本质量差时调小chunk |
| Word | 结构感知 + 标题层级 | 1200字符 | 200字符 | 表格单独处理单独存 |
| TXT | 启发式规则 + 递归切分 | 800字符 | 100字符 | 先判类型再选规则 |
参数不是死的,这是我踩过很多坑之后的结论:如果你的检索结果经常出现"检索到但答案错误",优先检查是不是chunk_size调小了,语义不完整;如果经常出现"检索不到相关内容",优先检查是不是chunk_size调大了,导致每个chunk里噪音太多,语义被稀释。
5.3 Embedding粒度与切分粒度的匹配关系
还有一个经常被忽略的点:切分粒度要和Embedding模型的粒度匹配。如果用同一个Embedding模型做向量化,那么chunk的语义越聚焦,向量表征就越准确。这和embedding模型本身的特性有关——它擅长对"一段统一主题的文本"建模,而不是对"一堆混杂信息的段落"建模。
因此,切分的原则应该是:宁可多切几刀,保证每块文本的主题相对集中;也不要少切,让一个chunk里的内容包含太多无关信息。尤其是保险、法律、金融这类专业文档,一个章节内常常包含多个子议题,按子议题切比按章切效果好得多。
6. 实操中遇到的问题与排查实录
6.1 切分后索引数量异常偏少或偏多
我在项目中经常遇到这种问题。如果切分后索引数量异常偏少,先看是不是解析环节出了问题——比如PDF只提取到第一页,或者Word只读到了目录。如果是切分数量异常偏多(同量级文档切出了十倍以上的chunk数),通常是错把页眉页脚和重复导航文本也切进去了,去重没做好。
还有一个容易被忽略的问题:PDF解析后文本里可能包含大量不可见字符或特殊空白符,这些字符会导致切分器认为内容长度达标,但实际文本噪声很多。建议在提取后做一次文本清洗,去掉控制字符和多余的空白。
6.2 检索命中率低,问题多半不在向量检索而在于切分颗粒度
很多人在向量检索环节花了大量时间调参、换模型,但效果始终不理想。这时我会建议回头检查切分逻辑。一个常见的现象是:问题问的是某个具体条款,但检索出来的chunk是包含该条款的整个大章节,导致相关内容的占比太低,向量相似度被稀释了。
处理这个问题的思路是:把大章节切碎成子议题级的小块,而不是只按章节边界切。不过也有反面情况:切得太碎,检索回来的chunk缺少完整上下文,导致语言模型无法理解前因后果。平衡点需要针对具体文档反复测试,没有一次性到位的银弹。
6.3 跨页段落和表格结构带来的语义断裂问题
跨页段落断裂是PDF场景的高频问题。判断段落是否完整的方法之前提到过——看末尾符号。我还会加一个辅助判断:如果段落结尾不是终止符,且页末文本块和下一页开头文本块的行宽接近,大概率是同一段落。
表格的语义断裂则更隐蔽。一个表格被切到多个chunk里,每个chunk拿到的是表格的局部行或局部列,信息完全对上不。这里我的教训是:表格数据优先整体序列化,不要拆分。如果表格太大,超过chunk长度限制,就按行分组切,但切分时把表头附在每个分组前,保证每块都有列名上下文。
7. 切分后的元数据标注,容易被忽略但回报巨大
7.1 把来源信息写进切分结果
切分的结果不只是"一段文字",还应该附带上完整的来源元数据。如果这份元数据不采集,后续做引用溯源的时候会非常麻烦。
我在切分管线里一定会记录以下信息:
- 来源文件名和文件路径
- 所属章节标题(一级和二级)
- 页码范围或段落编号
- 切分的格式类型(PDF/Word/TXT)
- 切分时间
有了这些信息,回答完用户问题后可以进行引用溯源——直接告诉用户"这段内容来自某文档的第X章第X页"。没有元数据兜底,RAG的落地价值至少打五折。
7.2 为后续增量更新做铺垫
企业文档不是静态的。今天的知识库有100篇文档,明天可能新增20篇,后5天可能修改了30篇。如果切分后的chunk没有元数据,增量更新时只能全部删除重建,成本极高。
记录文件哈希值或更新时间,配合文档ID,就能实现"只重切变更的文档,保留未变动的chunk"。这个设计在长期运行的知识库系统里特别重要。
8. 最终可行的综合切分管线参考
把我前面讲的内容汇总成一套可以直接落地的切分管线:
解析阶段:按格式分流。PDF走PyMuPDF/OCR,Word走python-docx,TXT走模式识别。先做格式感知的解析,再做纯文本提取。
预处理阶段:去页眉页脚,恢复阅读顺序,合并跨页段落,清洗特殊字符。
初切阶段:利用结构信息做粗切。PDF和Word按标题层级切章节,TXT按内容模式切段落。
细切阶段:对超长章节或段落,按长度递归切分,记录overlap。
后处理阶段:标注元数据,做去重,过滤过短chunk(小于50字符的直接丢弃)。
我这套管线在一套包含论文PDF、企业Word制度文档、TXT技术日志的知识库上实测,检索命中率较之前纯文本方案提升了约35%。提升最明显的是PDF类文档——因为布局恢复解决了大量语义错乱问题。
文档切分这件事,没有放之四海而皆准的配置,但有了这套格式感知的思路,你在处理新文档格式、调节参数时就有了清晰的判断依据,而不是反复试错。希望这篇分享能给正在做RAG落地的你一些实质帮助。
