☰
RAG答错别只怪模型,文档解析、清洗与切块才是关键
2026/10/7 6:37:09 网站建设 项目流程

做了这么多年RAG落地项目,我早就习惯了一个现象:系统答错问题,第一反应就是换模型。有的团队在模型上花了不少预算,又是调prompt又是试微调,结果准确率还是上不去。我的实际体验是,RAG系统答错,有相当大比例的问题根本不在模型推理阶段,而是文档在进入系统之前就已经“坏”了。这个“坏”不是文件损坏,而是原始文档从形态到内容,在解析、清洗、切块、向量化这一整条前置链路里,信息已经丢失、错乱,或者压根没被正确提取出来。

RAG的全称是检索增强生成,但检索的前提是“备料”。你给模型喂的语料如果不干净、切得不合理、格式解析不到位,那模型就算再强,也只能在残缺文本上凭空猜测。我见过一个真实案例:某企业把产品手册丢进知识库,系统每次回答参数都会错一位小数,排查了半天,发现是PDF里的表格在解析时串了列。这种问题,换什么模型都救不回来。

所以这篇文章不聊模型选型,不聊向量数据库怎么搭,就聚焦“文档进入系统之前”这段路:文件解析、数据清洗、表格图片处理、文本切块、元数据标注,以及当RAG答错时怎么一步步定位是不是这些前置环节出了问题。无论你是准备在Mac上自己搭一个本地RAG知识库,还是正在做一个正经的RAG项目,这些内容都能直接拿过来用。

1. 先别急着甩锅给模型:RAG链路里“看不见”的第一公里

1.1 RAG不是“模型问答”,而是“数据管道+模型”的组合系统

很多人对RAG的理解是“给模型挂一个知识库,然后它就能回答问题”。但这个理解容易让人忽略一整条链路:原始文档 → 解析抽取文本 → 清洗去噪 → 文本切块 → 向量化嵌入 → 索引存储 → 检索召回 → 拼接上下文 → 模型生成答案。

在这条链路里,模型只占最后一小段。前面一大段都是“文档进入系统之前”的活儿。如果打个比方,把RAG比作开饭店,模型是大厨,文档预处理就是买菜、洗菜、切菜。菜没洗干净、切得乱七八糟,大厨再厉害也炒不出好菜。检索结果本身是脏的、偏的,生成阶段自然跟着错。

我在实际项目里发现,大多数回答质量问题,追根溯源都能追到某个前置环节。比如PDF里的文字本身是图片,没做OCR就直接向量化了,结果向量库里存的全是乱码;再比如文档里有不少水印、页眉页脚、目录页码,这些噪声被一起切进chunk,检索出来的top-k结果全是无关信息。这些问题扔给模型去“理解”,模型当然一头雾水。

1.2 文档进入系统之前,到底包含哪些环节

把“文档进入系统之前”拆开看,至少包含以下几步:

  • 格式识别与解析:把PDF、Word、HTML、PPT、扫描件等转成纯文本或结构化文本。这一步最容易出错,PDF看似简单,内部却可能包含文字图层、图片图层、表格边界、复杂排版。
  • 内容清洗:去掉页眉页脚、页码、水印、表格边框符号、重复标题、超链接、脚本标签等噪声。
  • 特殊内容处理:表格要不要转成Markdown?图片要不要OCR或生成文本摘要?跨页断句怎么合并?
  • 切块与关联:按什么逻辑切成小块,切完是否保留上下文衔接,是否补充标题或摘要作为元数据。
  • 向量化入库前检查:向量化之前抽样检查切好的文本块,确认没有乱码、截断、丢字。

很多团队把这几步压缩成“读一下PDF然后直接embedding”,这就是问题源头。后面几章,我会把每个环节逐一拆开讲,包括能直接抄的参数经验和踩坑记录。

2. 文档进系统前最容易翻车的三个地方

2.1 解析环节:PDF不是“文字文件”,而是“排版文件”

PDF是一个排版格式,不是一个文本格式。一句话在PDF里可能被打散成多个文本框,也可能因为字体编码问题变成乱码,更常见的是扫描件——整页就是一坨图片。所以我处理PDF的第一原则是:先搞清楚这份PDF是文本型还是扫描型。

  • 文本型PDF:直接用pymupdf(fitz)、pdfplumber、camelot提取文字与表格。
  • 扫描型PDF:必须先做OCR,我常用PaddleOCR或Tesseract,中文场景PaddleOCR效果更好。

实操经验:文件到手后,先随机抽几页看看提取结果。如果提取出的文本里出现大量缺字、乱码、英文夹杂,多半是字体嵌入或编码问题,别硬调提取库,换个方案比如OCR重排反而更快。

场景推荐工具备注
文本型PDF快速提取pymupdf / PyMuPDF快,适合大量文件
PDF表格提取pdfplumber / camelot表格会保留行列结构
扫描件文字识别PaddleOCR / Tesseract中文推荐PaddleOCR
混合格式整体解析unstructured / docling能识别标题正文结构

举个例子,用pymupdf提取文本很简单:

import fitz doc = fitz.open("manual.pdf") for page in doc: text = page.get_text() if len(text.strip()) < 20: print(f"第{page.number + 1}页疑似扫描件,需走OCR")

这段代码本身不复杂,核心价值在于那个“字符数阈值”判断。一页文本少于20个字符,基本可以断定是图片页或者扫描页,就不要再继续用纯文本方案了。

在Mac上搭建RAG知识库,完全可以用本地工具完成解析。装一个docling,它能把PDF、Word、HTML统一解析成Markdown,结构化程度比直接抽纯文本好得多,后续切块也方便。不需要非得上一套重型平台。

2.2 清洗环节:网页、Word里藏着看不见的脏数据

网页转文本时,最常见的问题是把导航栏、页脚、cookie弹窗、脚本内容一起抓进来。Word文档里也有大量看不见的隐藏字符、修订记录、文本框残留。这些脏数据进入向量库,不但浪费存储,还会在检索时“以噪生噪”。

我的清洗习惯是逐项过滤:

  • 删除页眉页脚、页码标识、目录页和封面页。
  • 合并空行,去除非ASCII控制字符、零宽空格、emoji。
  • 网页HTML解析时,直接用BeautifulSoup的get_text(separator='\n'),并剔除script、style、nav、footer等标签。
  • Word转文本时,优先转成Markdown(pandoc或mammoth),保留标题层级,后续切块可以直接利用标题结构。

HTML清洗我一般这样处理:

from bs4 import BeautifulSoup def html_to_clean_text(html): soup = BeautifulSoup(html, "html.parser") for tag in soup(["script", "style", "nav", "footer", "aside"]): tag.decompose() text = soup.get_text(separator="\n") lines = [line.strip() for line in text.splitlines() if line.strip()] return "\n".join(lines)

清洗做完,再做一道“人工抽检”:随机抽5页转出来的文本,肉眼确认有没有残留噪声。这道抽检成本很低,但能省下后面向量化和排查的大量时间。

另外回应一个常被问到的问题:有没有本地的RAG文本拆解工具?我的答案是没有“万能工具”,但可以按格式组合一套pipeline。入口判断文件类型,PDF走pymupdf加OCR,Word走mammoth,HTML走readability或上面的BeautifulSoup方案,最后统一转成Markdown或纯文本。如果不想自己维护,unstructured和docling都是开箱即用的本地方案。

2.3 表格和图片:RAG知识库能不能存图片?

有人问“RAG知识库能存储图片吗”。直接说结论:RAG知识库本身不能直接存图片,向量库里存的仍然是文本向量。但图片里的信息完全可以通过下面几种方式进入知识库:

  • 文字型图片:OCR提取图片中的文字,把文字作为文本块入库。适合包含文字信息的截图、扫描件。
  • 视觉信息为主的图片:用多模态模型生成图片描述,把描述文本入库。适合产品图、流程图、界面截图。
  • 文档配图:更好的做法是把图片和标题、上下文说明一起保存,检索结果里返回图片路径作为参考。

表格的处理比图片更微妙。表格不能被简单“压平”成一行纯文本,否则“参数A对应值B”这种对应关系会完全丢失。我的做法是:把表格转成Markdown或HTML结构保留,切块时单独识别表格区域,让表格作为一个整体进入向量库。用llama_index里的markdown reader或unstructured的table处理都能做到这一点。如果表格很大,可以按行拆成若干子表,但每个子表前面要带表头描述。

对于RAG项目,影响检索质量最大的往往不是“存了什么”,而是“以什么结构存进去”。图片和表格尤其如此。这部分放到第四章继续展开,先看更核心的切块问题。

3. 切块策略:一个被低估的RAG瓶颈

3.1 为什么切块方式比模型更影响回答质量

RAG的检索单元是chunk,不是整个文档。模型回答问题,看的是检索回来的几个chunk拼接成的上下文。如果chunk太大,一个块里塞了太多无关内容,向量化后主题不聚焦,检索精度下降;如果chunk太小,关键信息被截断,上下文不完整,模型没法理解。

我见过有人把整份50页文档直接做成一个chunk,也见过有人把一个句子切成一个chunk,效果都不好。切块本质上是“上下文完整性和语义聚焦度之间的平衡”。

热词里有人提到“滑动窗口”这个概念,其实切块时的overlap就是一种滑动窗口思想。相邻两个chunk之间保留一部分重叠文本,避免关键句恰好被切在边界上。这个设计对检索召回率的提升非常明显,尤其当原文中存在大量长句时。

3.2 常见切块方案对比与参数选择

先说参数起点:

  • 中文场景,chunk size建议从300到500字起试,别一上来就上1000。
  • overlap建议为chunk size的10%到20%,既能保持衔接,又不会造成太多重复。
  • 英文文本按token算更合适,chunk size约200到400 token。

切块方案按内容结构调整:

  • 固定字符切分:简单粗暴,按字符数切,适合日志、聊天记录这类无结构文本。
  • 递归字符切分:先按段落,再按句子,再按字符,尽量让每个块落在自然边界上。LangChain的RecursiveCharacterTextSplitter就是干这个的。
  • 结构感知切分:利用Markdown标题、PDF章节、HTML标题切块,每个块天然有章节归属,检索质量明显更高。
  • 语义切分:用embedding计算句子相似度来聚类切块,效果更好但耗时长,适合精调阶段。

我在项目里最常用的是“结构感知切分加固定块大小兜底”:先按Markdown标题把文档切成章节,再对超长章节递归切分。切完之后给每个块补上章节路径作为元数据,这样检索时就算命中的是第三层小标题下的内容,也能知道它属于哪个大章节。

3.3 一个可复用的切块实操示例

以Mac本地环境为例,用Python写一个简单但有效的切块逻辑:

import re def split_by_markdown_headers(text: str) -> list: """按 Markdown 标题切分,返回 (章节路径, 文本) 列表""" lines = text.splitlines() chunks = [] current_path = [] buffer = [] def flush(): nonlocal buffer if buffer: chunks.append(("/".join(current_path), "\n".join(buffer).strip())) buffer = [] for line in lines: m = re.match(r"^(#{1,6})\s+(.*)", line) if m: level = len(m.group(1)) title = m.group(2).strip() flush() current_path = current_path[:level] while len(current_path) < level: current_path.append("") current_path[level - 1] = title else: buffer.append(line) flush() return chunks

这个函数把文档按Markdown标题拆成带层级路径的块。实际使用中,再用一个字符边界逻辑把超长块二次切分,并加上overlap:

def split_long_chunk(text, chunk_size=400, overlap=80): if len(text) <= chunk_size: return [text] chunks = [] start = 0 while start < len(text): end = start + chunk_size chunks.append(text[start:end]) if end >= len(text): break start = end - overlap return chunks

overlap取多少?我一般先定chunk_size的15%,然后抽几条边界句子人工看:如果发现很多句子被从中间截断,就加大overlap;如果发现检索结果里重复内容太多,就减小。调参没有银弹,只能基于抽样观察。顺便说一句,如果用的是LlamaIndex或LangChain,这些切块器都内置了,但手写一遍能让你更清楚参数到底在控制什么。

4. 结构化与非结构化:知识库类型如何决定RAG的上限

4.1 知识库、RAG知识库、结构化知识库各是什么

有人问“kg知识库、rag知识库和结构知识库如何区分以及各自的应用场景”,这里用一个表格说清楚:

类型存储形态检索方式擅长的内容与应用场景
普通知识库/文件库原始文档、网页、附件关键词/全文检索文件归档与浏览
RAG知识库(向量知识库)文档切片后的embedding向量向量相似度检索非结构化文档问答、客服助手
KG知识库(知识图谱)实体、关系三元组图查询/关系推理多跳问答、关系查询、一致性要求高的业务
结构化知识库(数据库表)表格数据行SQL/接口查询订单、物料、指标等明细数据

简单说,如果回答“产品A有哪些参数”,RAG知识库就够了;如果回答“产品A和产品B共同参与的流程有哪些中间节点”,那是知识图谱的场景。很多团队一开始就用RAG硬扛所有问题,扛不动了就去换模型,其实该换的是知识表示方式。

RAG知识库也有自己的边界:它擅长语义相似度召回,不擅长精确计算与逻辑推理。比如“上个季度总费用是多少”这种聚合性问题,用RAG从文本里拼凑,十有八九会出错;这时候应该走结构化数据库查询。

4.2 给切块结果补上“元数据”,让检索更准

文档进入系统之前,真正拉开差距的是元数据。同样是两个chunk,一个有元数据“来源:产品手册/空调系列/能效参数”,一个只有裸文本,检索质量和下游展示效果完全不同。

我建议每个chunk至少包含这些元数据:

  • 来源路径(文件路径或URL)
  • 一级/二级章节标题
  • 文档类型(说明书、FAQ、合同、代码文档等)
  • 语言
  • 更新时间或版本号
  • 如果是表格来源,注明table类型

在Mac本地搭建RAG知识库时,用LlamaIndex的MetadataAwareSplitter,或者手动在embedding前给元数据字段赋值都能做到。检索时这些元数据可以作为过滤条件,也可以拼进上下文里,让模型知道文本块来自哪里。比如检索命中了能效参数那一节,把“来源:产品手册/空调系列/能效参数”拼到chunk前面,模型回答时就不会乱发挥。

4.3 针对不同内容类型(文档、表格、长文)的预处理方案

我给的通用方案是“先分类,再处理”:

  • 自然语言文档(Word、Markdown、HTML正文):清洗后按章节切块,保持结构。
  • 表格/数据文件(Excel、PDF表格、CSV):转成Markdown或JSON,行数多的拆成子表,加表头描述。
  • 图片/扫描件:先OCR成文本,再按文本流程处理;必要时用多模态模型补充视觉描述。
  • 长文(技术手册、制度文件):用结构感知切块,并在块内保留上下文提示,比如“上文提及的产品型号为ABC-2000”。
  • 代码片段(技术文档里的示例代码):不要用普通字符切分,保留代码块完整性,按函数或代码块切。

还有一个容易忽略的点:切块之后要检查“块内是否自洽”。也就是说,单独看这个chunk,读者能不能大致看懂它在讲什么。如果不自洽,回到全局补一句上下文引导语进去。这个做法对提升检索命中后模型回答的连贯性非常有帮助。特别是跨页断句这种问题,两页之间段落被拆开,切块时要把上一页的结尾和下一页的开头拼完整,别让句子断在半路。

5. RAG答错时的排查路线图:从数据倒推到模型

5.1 三步定位法:先查文档,再查切块,最后查生成

当用户反馈“答错了”,我的排查顺序是倒着链路走,但先查数据。

第一步,复现问题并固定问法。拿到一条错误回答,先记录问题原文,再看模型答的是哪一段话、检索命中了哪些chunk。很多RAG框架都支持打开检索明细,没有的话就自己打印检索结果。

第二步,检查检索命中的chunk。重点看两个指标:命中内容是不是真的与问题相关,命中的chunk是不是足够完整。如果命中内容驴唇不对马嘴,问题大概率在文档处理或向量化,而不是模型。用向量相似度检索工具单独查一下“和这个query最相似的内容是什么”,能快速定位是embedding模型的问题还是文档侧的问题。

第三步,检查chunk内容本身。如果检索结果相关,但模型回答依然错误,把命中的chunk拼起来自己读一遍。如果发现chunk里有乱码、截断、串列、噪声,说明解析清洗环节有漏洞。这一步看起来笨,却是最高效的定位方式。

5.2 常见问题速查表与排查工具

整理一张速查表,方便对照:

现象大概率环节检查建议
回答内容张冠李戴表格解析串列检查原始表格结构是否保留
回答漏关键参数文本抽取丢字抽检PDF提取文本
命中的chunk与问题无关切块过大/embedding模型不适配检查chunk size和相似度分数
多个问题答案都提到同一段噪声清洗不彻底查看chunk原文有没有页眉页脚水印
回答总是“找不到答案”文档未入库或切块不能自洽查向量库是否存在、检索时是否被过滤
某些格式文件完全不回答解析失败直接跑一次解析脚本看输出

排查工具上,我常用的有:LangSmith追踪RAG链路中检索与生成明细;LlamaIndex的QueryEngineTracer;手动的话,Chroma或Qdrant自带检索预览,打开一眼就知道召回的是什么。别等到线上出了错才去翻日志,把可观测性提前埋好,能省大量时间。

5.3 分享一下我踩过的坑

最后说几个真实踩坑记录。

第一个坑:扫描版PDF没OCR就入库。结果知识库对这份文档的检索结果全是乱码。这个排查不难,难在很多人根本不知道文件是扫描件。我现在的流程是入库前统一做一次文本密度检查,一页提取出来的字符数小于阈值就自动标记为疑似扫描件,走OCR通道。

第二个坑:表格转纯文本后串列。某次做设备参数问答,系统把“功率:2000W”答成“功率:2000V”,原因就是PDF表格解析时同一行文本把两列合并了。后来改成先把表格区域识别出来,转成Markdown再切块,问题才消失。

第三个坑:盲目加大chunk size。一开始以为块越大模型信息越多,就把chunk size提到1000字以上,结果检索命中率明显下降,有的块融合了三个不同主题。后来改用结构感知切块,块大小降到400字左右,准确率反而上来了。

这些坑的共同点,都是模型之外的文档链路问题。所以当你觉得模型不够聪明的时候,先别急着换模型,去查一查文档进入系统之前发生了什么。很多时候,答案就藏在那些被忽略的预处理细节里。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询