RAG答错别急着换模型:文档预处理才是效果分水岭
1. 为什么模型背了锅:RAG 答错的真实归因
RAG 答错的时候,绝大多数人的第一反应是换模型。从 7B 换到 14B,从 14B 换到 70B,再不行就上闭源 API,账单翻了几倍,效果可能只提升了几个百分点。我见过太多团队在这个循环里反复横跳,最后把问题归结为“模型能力不够”。
但如果你把整条链路拆开看,一个 RAG 系统的回答质量,模型只占其中一环。检索到的内容对不对、文档解析出来的文本干不干净、切分后的 chunk 有没有把语义切断、embedding 有没有把关键信息编码进去——这些环节里任何一个出问题,后面换再大的模型也救不回来。用一句话概括就是:垃圾进,垃圾出。
这个道理听起来简单,但实际操作中,文档进入系统之前的那段流程,恰恰是最容易被忽视的。大家把精力花在调 prompt、换模型、加 rerank 上,却很少有人回头看看:那份 PDF 解析出来之后,表格是不是变成了乱码?那份 Word 文档里的标题层级是不是全丢了?那份扫描件 OCR 之后,错字率是不是高得离谱?
我做过一个粗略的统计,在我经手的 RAG 项目里,回答质量不达预期的案例中,超过六成的问题根源在文档预处理阶段,而不是模型本身。这个比例可能因场景而异,但方向是明确的:文档进入系统之前的那段路,才是 RAG 效果的分水岭。
这篇文章就是围绕这个判断展开的。我会把文档从原始文件到进入向量库的完整链路拆开,逐段讲清楚每个环节在做什么、容易在哪里出问题、怎么排查、怎么修。适合正在做 RAG 项目但效果卡住的同学,也适合刚开始搭知识库、想少走弯路的开发者。不需要你精通 NLP,但需要对 RAG 的基本流程有个概念。
2. 文档进入系统之前的完整链路拆解
2.1 从原始文件到向量库的五个阶段
一份文档从硬盘上的文件变成向量库里的一条记录,中间要经过五个阶段。很多人只关注最后两个阶段,前面三个基本是“能跑就行”,问题就出在这里。
第一阶段是格式解析。PDF、Word、Excel、PPT、HTML、Markdown、扫描件,每种格式的解析难度完全不同。PDF 是最麻烦的,因为它本质上是一个排版格式,不是内容格式。文字在 PDF 里是按坐标定位的,解析器需要推断阅读顺序。表格更麻烦,PDF 里的表格没有结构信息,解析器只能靠线条和间距去猜。
第二阶段是内容清洗。解析出来的原始文本里会有大量噪音:页眉页脚、页码、水印、乱码、多余的空格和换行、重复的段落。这些东西如果不清理,会直接进入后续环节,污染检索结果。
第三阶段是结构化处理。把线性文本还原成有层级的内容,识别标题、正文、列表、表格、代码块。这一步决定了后续切分时能不能保持语义完整。
第四阶段是切分。把长文档切成适合 embedding 的小块。切分策略直接影响检索命中率,切大了语义模糊,切小了上下文丢失。
第五阶段是向量化与入库。用 embedding 模型把文本块转成向量,存入向量数据库,同时保留元数据用于过滤和溯源。
这五个阶段里,前三个阶段是“文档进入系统之前”的部分,也是本文的重点。后两个阶段虽然也重要,但前提是前面的输入是干净的。
2.2 为什么预处理阶段最容易被跳过
预处理阶段被跳过,原因很现实:它不出现在 demo 里。
你搭一个 RAG demo,拿几篇干净的 Markdown 文档,解析、切分、embedding、检索、生成,跑通了,效果还不错。于是你觉得这套流程没问题,开始往里面灌真实数据。结果 PDF 解析出来一堆乱码,表格变成了数字堆,扫描件 OCR 错字连篇。这时候你第一反应是“模型不行”,而不是“文档没处理好”。
另一个原因是预处理没有标准答案。切分切多大?清洗洗到什么程度?结构化做到什么粒度?这些问题没有统一的最优解,取决于你的文档类型和查询场景。没有标准答案的事情,大家就倾向于用默认值,而默认值往往是最粗糙的。
还有一个原因是预处理的效果很难量化。检索命中率可以算,回答准确率可以评,但“文档解析质量”怎么衡量?没有现成的指标,大家就不去衡量,不去衡量就不去优化。
2.3 一个真实的翻车案例
我接手过一个企业知识库项目,文档主要是产品手册和售后工单,格式以 PDF 和 Word 为主。团队用的是当时比较主流的方案:PDF 用某个开源解析库,Word 用 python-docx,切分用固定长度 512 字符,embedding 用某个通用模型。
上线之后,用户反馈最多的问题是“答非所问”。比如问“XX 型号的保修期是多久”,系统检索到的内容里确实有“保修期”三个字,但回答出来的却是另一个型号的保修政策。团队一开始以为是模型幻觉,换了两个模型,问题依旧。
我让他们把检索到的原始 chunk 打出来看,问题一目了然:PDF 解析出来的文本里,表格全部变成了按行排列的纯文本,型号和保修期之间的对应关系完全丢失。检索时命中了“保修期”这个词,但周围的上下文里混着好几个型号,模型只能猜。这不是模型的问题,是文档解析阶段把结构化信息压平了。
后来我们重新做了表格解析,把每个型号和对应的保修期绑定成一个独立的 chunk,问题就解决了。模型没换,prompt 没改,只是把文档进入系统之前的那段路修好了。
3. 格式解析:不同文档类型的坑与解法
3.1 PDF 解析的三种路线与选型逻辑
PDF 解析是文档预处理里最复杂的一环。市面上的方案大致分三类,各有各的适用场景。
第一类是基于规则的传统解析库。这类工具通过分析 PDF 内部的文字对象、字体信息、坐标位置来推断阅读顺序和段落结构。优点是速度快、资源消耗低、对纯文本 PDF 效果好。缺点是遇到复杂排版(多栏、表格、图文混排)就容易出错,对扫描件完全无能为力。
第二类是基于 OCR 的解析方案。先把 PDF 每页转成图片,再用 OCR 识别文字。优点是通吃所有 PDF,包括扫描件。缺点是速度慢、错字率取决于 OCR 质量、表格结构还原困难。而且 OCR 出来的文本没有字体信息,标题和正文区分不了。
第三类是近年来出现的基于深度学习的文档理解模型。这类模型能同时处理文字、版面、表格结构,输出带层级和表格结构的结构化内容。优点是效果好,尤其是表格和复杂版面。缺点是需要 GPU、推理速度慢、部署成本高。
选型逻辑很简单:先看你的文档里有没有扫描件,再看有没有复杂表格,最后看预算和延迟要求。纯文本 PDF 用第一类就够了,有扫描件就必须上 OCR,表格多且结构重要就考虑第三类。不要一上来就追求最贵的方案,也不要为了省钱用完全不适合的方案。
3.2 Word 与 Excel 的结构化解析要点
Word 文档看起来比 PDF 好处理,因为它的底层是结构化的 XML。但很多人用 python-docx 只取paragraph.text,把样式信息全丢了。标题、正文、列表、表格,全部变成一样的纯文本,后续切分时完全不知道哪里是章节边界。
正确的做法是解析时保留样式信息。paragraph.style.name能告诉你这是 Heading 1 还是 Normal,表格可以用document.tables单独提取。标题信息在切分时非常有用,你可以按标题层级来切,保证每个 chunk 属于同一个章节。
Excel 的坑又不一样。Excel 是二维表格,但很多业务表格是“人类可读”的,不是“机器可读”的。比如合并单元格、多级表头、注释行混在数据里。直接按行读取,出来的东西根本没法用。
处理 Excel 的关键是先理解表格的语义结构。表头在哪一行?有没有合并单元格?哪些列是维度,哪些列是指标?这些信息决定了你怎么把一行数据转成一段自然语言描述。比如一行“型号 A | 保修期 12 个月 | 保修范围 主板”,应该转成“型号 A 的保修期为 12 个月,保修范围为主板”,而不是把三个单元格用空格拼起来。
3.3 扫描件与图片文档的 OCR 处理策略
扫描件的处理核心是 OCR,但 OCR 只是第一步。识别出来的文本没有段落结构,你需要根据行间距、缩进、字体大小(如果 OCR 能提供)来推断段落边界。
OCR 的错字率是个大问题。通用 OCR 对印刷体效果不错,但遇到表格、公式、特殊符号就容易出错。如果文档里有大量专业术语,可以考虑用领域词典做后处理纠错。另一个技巧是保留 OCR 的置信度信息,低置信度的词在后续检索时可以降权或标记出来。
还有一个容易被忽视的点:扫描件的图片质量直接影响 OCR 效果。如果原始扫描件分辨率低、有倾斜、有噪点,先做图像预处理(去噪、纠偏、二值化)再送 OCR,效果会好很多。这一步多花的时间,比后面换模型省下的时间划算得多。
3.4 解析质量的自检清单
解析完之后,不要直接进入下一步。花十分钟做个体检,能避免后面几小时的排查。
| 检查项 | 检查方法 | 合格标准 |
|---|---|---|
| 文本完整性 | 随机抽 5 页,对比原文和解析结果 | 无大段缺失,无乱码 |
| 阅读顺序 | 检查多栏文档的解析结果 | 文字顺序符合人类阅读习惯 |
| 表格结构 | 找一个有表格的页面 | 表格行列对应关系正确 |
| 标题识别 | 检查章节标题是否被标记 | 标题与正文可区分 |
| 特殊字符 | 搜索公式、符号、单位 | 无异常转义或丢失 |
| 页眉页脚 | 检查是否混入正文 | 已清理或可过滤 |
这个清单看起来简单,但能筛掉大部分低级问题。我见过太多项目,解析出来的文本里混着页码和页眉,检索时经常命中这些噪音,模型被误导得莫名其妙。
4. 内容清洗与结构化:把噪音挡在门外
4.1 清洗规则的设计与优先级
清洗不是越狠越好。洗得太狠,可能把有用的信息也洗掉了;洗得太轻,噪音会污染检索。关键是要分清哪些是确定的噪音,哪些是可能有用但需要处理的内容。
确定的噪音包括:页眉页脚、页码、水印文字、重复的版权声明、解析产生的乱码字符。这些可以直接删。
可能有用但需要处理的内容包括:脚注、参考文献、图表标题、代码块。这些不能直接删,但也不能和正文混在一起。脚注可以单独存成元数据,图表标题可以附加到对应的表格或图片描述上。
清洗规则要有优先级。先处理确定的噪音,再处理格式问题(多余空格、换行、全角半角),最后处理语义层面的问题(重复段落、无关内容)。顺序反了的话,后面的规则可能会被前面的噪音干扰。
4.2 标题层级还原与章节切分
标题层级是切分的地图。如果解析阶段保留了标题信息,这一步就简单了:按标题层级构建一棵树,每个叶子节点就是一个自然的切分单元。
如果解析阶段没保留标题信息,就需要从文本特征去推断。常见的启发式规则包括:短行且独立成行、以数字编号开头、字体加粗或放大、前后有空行。这些规则不完美,但比盲目按长度切分好得多。
标题还原的粒度要适中。太粗,一个章节几千字,切出来的 chunk 太大;太细,每个小节都切成独立 chunk,上下文丢失。我的经验是切到二级或三级标题比较合适,具体取决于文档的层级深度和每个章节的平均长度。
4.3 表格与图片的结构化表示
表格是 RAG 里最容易丢信息的地方。纯文本化之后,行列对应关系没了,模型只能靠猜。正确的做法是把表格转成结构化表示,常见的有三种:
第一种是Markdown 表格。适合小表格,行列清晰,embedding 模型对 Markdown 格式的表格有一定理解能力。缺点是列多了之后,每行的文本会很长,embedding 效果下降。
第二种是键值对展开。把每一行转成“列名:值”的形式,比如“型号:A,保修期:12 个月,保修范围:主板”。这种方式对 embedding 友好,检索时容易命中。缺点是行数多了之后,chunk 数量会膨胀。
第三种是自然语言描述。把表格内容转成一段话,比如“型号 A 的保修期为 12 个月,保修范围为主板;型号 B 的保修期为 24 个月,保修范围为主板和屏幕”。这种方式最接近人类的表达习惯,检索效果通常最好,但生成描述需要额外的处理逻辑。
图片的处理类似。如果图片里有文字,先 OCR;如果是图表,考虑用多模态模型生成描述;如果是装饰性图片,直接跳过。图片描述要和周围的正文关联起来,不能孤立存在。
4.4 元数据提取:让检索多一个维度
元数据是检索时的过滤器。同样的查询,加上元数据过滤,命中率能提升一大截。
常见的元数据包括:文档来源、文档类型、创建时间、所属部门、产品型号、版本号。这些信息在解析阶段就能提取,存到向量库的 metadata 字段里。检索时可以根据查询意图做过滤,比如用户问“最新版的保修政策”,就可以按版本号或时间过滤。
元数据提取的关键是和业务场景对齐。不是所有能提取的字段都有用,要选那些用户查询时可能用来筛选的维度。提取太多,维护成本高;提取太少,过滤能力弱。我的建议是先提取三到五个核心字段,上线后根据实际查询日志再调整。
5. 切分策略:决定检索命中率的关键一步
5.1 固定长度切分的适用边界
固定长度切分是最简单的策略:按字符数或 token 数切,每段 512 或 1024,段间留一点重叠。很多人用它是因为它简单,不需要理解文档结构。
但固定长度切分有个致命问题:它会在语义边界之外切断文本。一个完整的论述被切成两半,前半段在 chunk A,后半段在 chunk B。检索时命中了 chunk A,但缺少后半段的上下文,模型回答时就会断章取义。
固定长度切分不是不能用,而是有适用边界。它适合那些结构松散、没有明显章节划分的文档,比如聊天记录、日志、短评。对于有明确结构的文档(手册、论文、合同),固定长度切分是在浪费文档本身的结构信息。
5.2 语义切分的实现思路
语义切分的核心思想是:在语义边界处切,而不是在字符数边界处切。
最简单的语义切分是按段落切。段落本身就是作者划分的语义单元,按段落切天然保持了语义完整。缺点是段落长度不均匀,有的段落只有一句话,有的段落上千字。
进阶的做法是按标题层级切。前面说的标题还原就是为这个服务的。每个叶子节点是一个语义单元,切出来的 chunk 自带章节上下文。
再进阶的做法是用 embedding 相似度来检测语义边界。计算相邻句子的 embedding 相似度,相似度骤降的地方就是语义边界。这种方法效果好,但计算成本高,适合对质量要求极高的场景。
实际操作中,我通常用标题层级 + 段落 + 长度上限的组合策略:优先按标题切,标题下按段落切,段落超过长度上限时再按句子切。这样既保持了语义完整,又控制了 chunk 大小。
5.3 重叠窗口的设置与计算
重叠窗口是为了解决“边界信息丢失”的问题。如果 chunk A 和 chunk B 之间没有重叠,恰好切在关键信息中间,检索时可能两边都命中不全。加上重叠之后,关键信息至少在一个 chunk 里是完整的。
重叠大小怎么定?经验值是 chunk 大小的 10% 到 20%。chunk 是 512 token,重叠就设 50 到 100 token。太小起不到作用,太大浪费存储和检索资源。
但重叠不是万能的。如果切分点本身就不合理(比如切在表格中间),加再多重叠也救不回来。重叠是补救措施,不是解决方案。先把切分点选好,再用重叠兜底。
5.4 切分效果的快速验证方法
切分完之后,怎么知道切得好不好?我常用一个简单的方法:随机抽 10 个 chunk,读一遍,问自己三个问题。
第一个问题:这个 chunk 单独拿出来,能不能看懂它在说什么?如果看不懂,说明上下文丢失太多。
第二个问题:这个 chunk 里有没有完整的语义单元?如果一句话被切成两半,说明切分点选得不好。
第三个问题:这个 chunk 里有没有无关内容?如果混入了页眉页脚或其他章节的内容,说明清洗或切分有问题。
这三个问题不需要任何工具,十分钟就能做完。如果 10 个 chunk 里有 3 个以上有问题,说明切分策略需要调整。
6. 常见问题与排查技巧实录
6.1 检索命中但回答错误的排查路径
“检索到了相关内容,但回答还是错的”,这是最常见的症状。排查路径如下:
第一步,把检索到的 chunk 原文打出来,逐字读一遍。很多时候问题一眼就能看出来:chunk 里有多个相似实体,模型分不清;或者关键信息被截断了,模型只能猜。
第二步,检查 chunk 的元数据。如果元数据里有版本号、时间、来源,看看检索到的 chunk 是不是过期版本或错误来源。
第三步,检查查询和 chunk 的匹配方式。如果是关键词匹配,看看是不是只匹配到了表面词,没有匹配到语义。如果是向量匹配,看看 embedding 模型是否适合当前领域。
第四步,如果前三步都没问题,再考虑模型和 prompt。这时候换模型或调 prompt 才是有意义的。
6.2 表格数据检索失效的修复方案
表格数据检索失效,通常是因为表格被压平成了纯文本,行列对应关系丢失。修复方案分三步:
第一步,重新解析表格,保留行列结构。如果原解析器不支持,换一个支持表格结构提取的工具。
第二步,把表格转成适合检索的格式。小表格用 Markdown,大表格用键值对展开或自然语言描述。
第三步,给表格 chunk 加上元数据,比如表名、列名、所属章节。检索时可以用这些元数据做过滤或重排。
我做过一个对比测试:同一份产品参数表,压平处理后的检索命中率是 40% 左右,结构化处理后的命中率能到 85% 以上。差距非常明显。
6.3 多栏 PDF 阅读顺序错乱的解决
多栏 PDF 的阅读顺序错乱,是因为解析器按坐标排序时,把左右两栏的文字混在了一起。解决方法有两种:
一种是换用支持版面分析的解析工具。这类工具能识别分栏结构,按栏分别提取文字。
另一种是在解析后做后处理。根据文字的 x 坐标聚类,把同一栏的文字归到一起,再按 y 坐标排序。这个方法需要一些调参,但对特定版式的文档效果不错。
如果文档版式固定,后处理方案的成本更低。如果版式多样,还是换工具更省心。
6.4 常见问题速查表
| 症状 | 可能原因 | 排查方法 | 修复方案 |
|---|---|---|---|
| 检索命中但回答错误 | chunk 内有多个相似实体 | 打印 chunk 原文 | 切分时按实体拆分 |
| 表格数据检索不到 | 表格被压平 | 检查 chunk 是否含表格结构 | 结构化表格表示 |
| 多栏 PDF 顺序错乱 | 解析器按坐标排序 | 检查解析后文本顺序 | 换工具或后处理 |
| 扫描件错字多 | OCR 质量差 | 抽查 OCR 结果 | 图像预处理 + 领域词典 |
| 检索结果重复 | 重叠窗口过大 | 检查 chunk 重叠比例 | 降低重叠到 10% |
| 长文档检索不全 | 切分粒度过粗 | 检查 chunk 平均长度 | 按标题层级细切 |
| 元数据过滤失效 | 元数据未提取或格式不对 | 检查向量库 metadata | 重新提取并入库 |
6.5 几个反直觉的实操心得
第一个心得:清洗过度比清洗不足更危险。我见过一个项目,清洗规则写得太激进,把所有的数字和单位都当噪音删了,结果产品参数全没了。清洗规则要保守,宁可留一点噪音,不要误删有用信息。
第二个心得:切分粒度不是越细越好。切得太细,每个 chunk 只有一两句话,检索时命中率高但上下文不足,模型回答时缺乏依据。切得太粗,检索时命中率低但上下文丰富。平衡点取决于你的查询类型:事实型查询适合细切,分析型查询适合粗切。
第三个心得:embedding 模型的选择要和切分策略匹配。有些 embedding 模型对长文本效果好,有些对短文本效果好。切分粒度变了,embedding 模型可能也要跟着换。不要一套配置用到底。
第四个心得:文档预处理的效果要用检索指标来验证,不要凭感觉。解析完觉得“看起来还行”,切分完觉得“应该没问题”,这些都是主观判断。用一组标准查询测一下命中率,用数据说话。
7. 把功夫花在进入系统之前
回到开头那个判断:RAG 答错的时候,先别急着换模型。模型是整条链路的最后一环,它只能基于检索到的内容来回答。如果检索到的内容本身就是错的、残缺的、混乱的,再强的模型也变不出正确答案。
文档进入系统之前的那段路,包括格式解析、内容清洗、结构化处理、切分策略,每一步都在决定最终进入向量库的内容质量。这段路走好了,后面的事情会顺很多;这段路走歪了,后面怎么调都是事倍功半。
我在实际项目中的体会是,把 60% 的优化精力花在文档预处理上,剩下 40% 花在检索和生成上,整体效果比反过来分配要好得多。预处理是地基,地基不稳,上面盖什么都是危房。
最后分享一个小技巧:每次调整预处理策略后,不要只看整体效果,要分文档类型看效果。PDF 的命中率提升了,但 Word 的下降了,说明你的策略对某类文档过拟合了。分类型监控,才能找到真正通用的策略。