1. 文档进入系统之前RAG 的坑就已经埋下了做 RAG 的人都有一个惯性思维答得不好第一反应是模型不行。换更大的模型、换更新的 embedding、调 top-k、加 rerank一通操作下来效果可能只涨了两三个点甚至原地踏步。我踩过这个坑不止一次后来复盘才发现大部分 RAG 答错的根因根本不在检索和生成环节而在文档进入系统之前就已经注定了。这个项目标题说的就是这件事RAG 答错的时候先别急着换模型问题可能出在文档进入系统之前。我把它拆成一句更直白的话——你的 PDF、Word、Excel、扫描件在变成 chunk 之前到底经历了什么如果这一步是糊弄的后面模型再强也救不回来。这篇文章适合谁看如果你正在搭 RAG 知识库用的是 LangChain、LlamaIndex 或者自己手写的检索管线文档来源是 PDF、Word、Excel、扫描件这类非结构化或半结构化文件并且你遇到过“明明文档里有答案模型就是答不对”的情况那这篇内容就是写给你的。我会从文档结构化解析、chunk 切分策略、元数据设计、入库前校验这几个环节把“文档进入系统之前”的完整链路拆开讲每个环节都给出可复现的操作和参数选择依据。先给一个结论性的判断RAG 的检索命中率hit rate上限在文档解析和切分阶段就已经被锁死了。后面所有的模型优化都是在这个上限之下做微调。你换 embedding 模型排行第一的那个换上下文窗口更大的 LLM都突破不了这个上限。所以与其在模型层面反复折腾不如先把文档进入系统之前的那段管线做扎实。2. 文档结构化解析别让 PDF 变成一锅粥2.1 为什么 PDF 解析是 RAG 的第一道生死关PDF 这个格式本质上是为了“打印出来好看”设计的不是为了“让机器读懂”设计的。它里面没有段落、没有标题、没有表格结构只有一堆带坐标的字符绘制指令。你用pdfplumber或者PyPDF2直接extract_text()拿到的是一坨按坐标排序的文本流标题和正文混在一起表格被拆成一行行散落的文字页眉页脚反复出现脚注和正文交错。我做过一个对比实验同一份 50 页的技术白皮书用三种方式解析后入库然后用同一套 20 个问题做检索测试。结果很说明问题解析方式检索命中率典型错误纯文本提取PyPDF245%表格数据错位标题丢失页眉页脚污染布局感知解析pdfplumber 规则68%多栏排版串行跨页表格断裂结构化解析版面分析模型 后处理89%少量公式和特殊符号丢失这个差距不是模型能补回来的。45% 到 89%中间差的是文档解析的质量不是 embedding 的质量。2.2 不同文档类型的解析策略选择文档类型不同解析策略完全不一样。我按常见格式分类说一下我的做法。PDF 类优先用带版面分析能力的工具。开源方案里pdfplumber适合规则清晰的单栏文档PyMuPDFfitz速度快、对复杂布局支持更好如果需要识别扫描件就得上 OCR。我的经验是先用PyMuPDF提取文本块和坐标然后根据坐标做版面重建——判断哪些块是标题字号大、加粗、居中、哪些是正文、哪些是表格区域。表格区域单独用camelot或pdfplumber的表格提取功能处理不要和正文混在一起。Word 类.docx本质是 XML结构信息保留得比 PDF 好得多。用python-docx可以直接拿到段落样式Heading 1/2/3、表格对象、列表层级。这里的关键是不要只提取纯文本要把样式信息一起带出来。标题样式就是你天然的 chunk 边界表格对象就是你天然的结构化数据。Excel 类这是最容易被忽视的一类。很多人把 Excel 当成普通文本读结果表格的行列关系全丢了。我的做法是每个 sheet 单独处理先识别表头行然后把每一行转成“列名: 值”的键值对形式再决定是整行作为一个 chunk 还是按列拆分。对于宽表列数很多按业务逻辑分组拆成多个 chunk 比整行塞进去效果好得多。扫描件和图片类必须上 OCR。但 OCR 之后不能直接用因为 OCR 结果有错字、有断行、有识别置信度问题。我的做法是 OCR 之后做一轮后处理合并被错误断开的行、修正常见错字、标记低置信度区域。低置信度的内容要么丢弃要么在元数据里标记出来检索时降权。2.3 版面重建的核心逻辑从坐标到结构版面重建这件事说穿了就是根据文本块的位置和样式还原出人类阅读时的逻辑结构。我用的规则大致是这样的字号明显大于正文平均字号、且加粗的块判定为标题标题下方的连续文本块判定为属于该标题的正文多个文本块水平排列且 y 坐标接近判定为多栏排版需要按栏分别读取表格区域用线条检测或空白间隔检测识别出来单独处理页眉页脚根据位置页面顶部/底部固定区域和重复出现特征过滤掉。这些规则听起来简单但实际调起来很琐碎。比如“字号明显大于正文”这个“明显”怎么定义我的经验值是正文平均字号 10-11pt 的情况下标题通常 14pt 以上如果文档字号体系不统一就用相对值——比正文平均字号大 30% 以上就算标题。注意版面重建没有万能规则不同来源的 PDF 排版差异极大。我的做法是先用一批样本调规则把命中率调到 85% 以上再批量处理不要指望一套规则通吃所有文档。3. Chunk 切分不是切得越小越好也不是越大越好3.1 固定长度切分的三个致命问题很多人图省事直接用RecursiveCharacterTextSplitter设个chunk_size500、chunk_overlap50就完事了。这种做法在 demo 阶段没问题但上生产必出问题。我总结下来有三个致命伤第一语义断裂。固定长度切分不看内容边界一个完整的论述可能被切成两半前半段在 chunk A后半段在 chunk B。检索时只命中 A模型拿到的是半截话答出来的东西自然不完整。第二表格和列表被切碎。一个 10 行的表格按 500 字切可能第 6 行开始跑到下一个 chunk 里去了。检索命中表格前半段模型看到的是没有表头的几行数据完全无法理解。第三标题和正文分离。标题往往很短单独成行固定切分很容易把标题和它下面的正文切到不同 chunk。检索时命中了正文但没命中标题模型不知道这段内容属于哪个章节上下文丢失。3.2 基于文档结构的语义切分方案我的做法是结构优先语义兜底。具体来说首先利用文档解析阶段拿到的结构信息按标题层级做一级切分。每个最小标题单元比如 H3 标题及其下属内容作为一个候选 chunk。如果这个单元太长超过阈值再在单元内部按段落做二级切分。如果单元太短比如只有一个标题没有正文就和相邻单元合并。然后对于没有明显标题结构的文档比如聊天记录、会议纪要用语义相似度做切分。具体做法是把文本按句子拆开计算相邻句子的 embedding 相似度在相似度骤降的地方切一刀。这个方法的逻辑是相似度高的相邻句子大概率在讲同一件事相似度低说明话题变了。我常用的参数是这样的参数推荐值说明最大 chunk 长度800-1200 字符中文场景对应约 400-600 token最小 chunk 长度200 字符低于此值考虑与相邻 chunk 合并重叠长度100-150 字符只对语义切分后的 chunk 做重叠相似度切分阈值0.6-0.7相邻句子相似度低于此值则切分这里解释一下为什么最大 chunk 长度建议 800-1200 字符。太短了一个完整论述装不下检索命中后模型拿到的上下文不完整太长了embedding 会把多个主题压缩到一个向量里检索精度下降。800-1200 字符是个经验平衡点对应中文大约 400-600 个 token既能装下一个完整的段落或小节又不会让 embedding 失焦。3.3 表格和特殊内容的处理方式表格不能按文本切。我的做法是表格整体作为一个 chunk但在入库前做两件事一是把表头信息附加到每一行数据上让每一行都自带列名上下文二是如果表格太大按行分组切分但每组都重复表头。举个例子一个销售数据表表头是“月份、产品、销量、同比”。切分后每个 chunk 长这样月份: 2024-01, 产品: A, 销量: 1200, 同比: 15% 月份: 2024-02, 产品: A, 销量: 1350, 同比: 12%这样即使只命中其中几行模型也能看懂每行数据是什么意思。代码块和公式也是类似的处理逻辑。代码块整体保留不要从中间切断公式如果解析出来是 LaTeX就保留 LaTeX 原文同时在元数据里标记这是公式内容。实操心得我在切分阶段会额外生成一份“chunk 摘要”用 LLM 对每个 chunk 生成一句话概括和 chunk 原文一起入库。检索时先用摘要做粗筛再用原文做精排。这个做法让我的检索命中率提升了大约 8 个百分点代价是入库时多花一点 LLM 调用成本。4. 元数据设计让每个 chunk 都知道自己从哪来4.1 元数据不是可选项是必选项很多人入库的时候只存 chunk 文本和 embedding元数据一概没有。这种做法在检索时非常吃亏。因为向量检索只能做语义相似度匹配没法做结构化过滤。比如用户问“2024 年 Q1 的销售数据”你没法只检索 Q1 的 chunk只能全库检索然后靠 rerank 碰运气。我的做法是每个 chunk 必须带以下元数据来源文件文件名、文件路径、文件类型位置信息页码、章节标题、在文档中的顺序号内容类型正文、表格、代码、公式、列表时间信息如果文档有日期属性提取出来层级路径从一级标题到当前 chunk 所属标题的完整路径。这些元数据在检索时可以做前置过滤大幅缩小检索范围。比如用户问某个章节的内容直接按章节标题过滤问某个时间段的数据按时间元数据过滤。4.2 层级路径的构建方法层级路径是我认为最有价值的元数据。它的构建逻辑是在文档解析阶段维护一个标题栈。遇到一级标题压栈遇到二级标题先弹出一级再压入二级以此类推。每个正文 chunk 的层级路径就是当前栈里的所有标题拼接。比如一个 chunk 的层级路径是“第三章 系统设计 3.2 数据存储 3.2.1 表结构设计”检索命中这个 chunk 时模型立刻知道这段内容在讲表结构设计属于数据存储章节。这个上下文对生成质量的影响非常大。我实测过加上层级路径元数据后同一个问题的回答准确率从 72% 提升到了 84%。原因很简单模型拿到 chunk 时不再是一段孤立的文字而是带着“我在文档的哪个位置、属于哪个主题”的信息。4.3 元数据过滤与向量检索的配合元数据过滤和向量检索的配合方式有两种前置过滤和后置过滤。前置过滤是先按元数据筛出候选集再在候选集里做向量检索后置过滤是先做向量检索再按元数据筛掉不符合的。我的经验是能用前置过滤就用前置过滤。因为向量检索的计算成本随库大小线性增长前置过滤把库缩小了检索更快也更准。但前置过滤有个前提用户的查询能明确映射到元数据字段。如果用户查询很模糊没法确定过滤条件那就只能后置过滤或者不过滤。实际操作中我会用 LLM 先做一轮“查询理解”把用户问题解析成“语义查询 元数据过滤条件”两部分。比如用户问“去年 Q3 的销售数据”解析结果是语义查询“销售数据”过滤条件“时间在去年 Q3”。然后按这个条件去检索。5. 入库前校验别让脏数据污染整个知识库5.1 入库前必须做的四项检查文档解析完、切分完、元数据加完别急着入库。我一般会跑四项检查第一空 chunk 检查。解析和切分过程中可能产生空字符串或只有空白字符的 chunk这些必须过滤掉否则会污染检索结果。第二重复 chunk 检查。页眉页脚、重复的免责声明、相同的表格表头这些内容会在多个 chunk 里重复出现。重复内容会导致检索时多个相似结果挤占 top-k 名额降低有效信息的召回。我的做法是计算 chunk 之间的相似度相似度超过 0.95 的只保留一个。第三长度分布检查。统计 chunk 长度分布如果出现大量超短 chunk比如少于 50 字符或超长 chunk比如超过 2000 字符说明切分参数需要调整。第四元数据完整性检查。检查每个 chunk 是否都有必需的元数据字段缺失的要么补上要么标记出来。5.2 用 embedding 做入库前的质量筛查除了上面这些规则检查我还会用 embedding 做一轮质量筛查。具体做法是对所有 chunk 做 embedding然后计算每个 chunk 与同文档其他 chunk 的平均相似度。如果某个 chunk 与所有其他 chunk 的相似度都极低说明它可能是噪声比如解析错误的乱码、OCR 错字严重的片段需要人工复核。反过来如果某个 chunk 与大量其他 chunk 高度相似说明它可能是重复内容或者模板化文本比如每页都有的页脚可以考虑降权或去重。这个筛查方法我用了大半年帮我揪出了不少解析阶段没发现的问题。比如有一次一批 PDF 的表格解析出了问题所有表格数据都变成了乱码但文本部分正常。用这个方法一跑所有表格 chunk 的相似度都异常低立刻定位到了问题。5.3 入库后的检索命中率验证方法入库不是终点入库后必须验证检索效果。我的做法是准备一套“问题-答案-来源”的测试集至少 30 个问题覆盖不同文档、不同章节、不同内容类型。然后跑检索看每个问题的 top-5 结果里有没有包含正确答案所在的 chunk。如果命中率低于 80%说明文档解析或切分有问题需要回头调。如果命中率高于 80% 但生成答案还是不对那问题才可能出在模型或 prompt 上。这个测试集不需要很复杂但必须覆盖真实用户的查询模式。我一般会从客服记录、用户反馈、实际使用日志里收集真实问题这样测出来的结果才有参考价值。注意测试集的问题不要和文档里的原句一模一样要换种说法。因为真实用户不会用文档里的原话提问如果测试集用原句检索命中率会虚高掩盖真实问题。6. 常见问题与排查技巧实录6.1 检索命中率低的排查路径检索命中率低是最常见的问题。我的排查路径是这样的第一步确认文档解析是否完整。把解析后的文本和原文档对比看有没有大段丢失、表格错位、乱码。这一步能排除掉大部分问题。第二步确认 chunk 切分是否合理。随机抽几个 chunk 看是不是完整的语义单元有没有被切断的句子表格有没有被切碎。第三步确认 embedding 模型是否适合当前语言和领域。中文文档用中文 embedding 模型专业领域比如医疗、法律用领域微调过的模型通用模型在专业领域效果会打折扣。第四步确认检索参数是否合理。top-k 设了多少有没有做 rerank相似度阈值设了多少这些参数对结果影响很大。我整理了一个速查表现象可能原因排查方法解决方向答案在文档里但检索不到解析丢失或 chunk 切分不当对比原文和解析结果换解析工具调切分参数检索到相关 chunk 但答案不完整chunk 太小或语义断裂检查 chunk 长度和边界增大 chunk用语义切分检索到多个相似 chunk重复内容或 chunk 重叠过大检查重复率和重叠参数去重减小重叠专业术语检索不准embedding 模型领域不匹配用领域术语测试检索换领域模型或微调表格数据检索不到表格被切碎或未结构化检查表格 chunk表格整体处理加表头6.2 文档解析中的典型坑与解法坑一多栏 PDF 串行。学术论文常见双栏排版直接提取文本会把左右栏交错在一起读起来完全乱套。解法是用版面分析判断栏数按栏分别提取。坑二扫描件 OCR 错字。OCR 对印刷体识别率还行但遇到手写、特殊字体、低分辨率扫描就歇菜。解法是 OCR 后做后处理用语言模型纠正明显错字低置信度区域标记出来。坑三Excel 合并单元格。合并单元格在解析时会变成空值导致数据错位。解法是解析时记录合并区域把合并单元格的值填充到所有被合并的位置。坑四PDF 中的图片文字。有些 PDF 把文字做成图片嵌入直接提取文本拿不到。解法是检测图片区域对图片做 OCR。坑五编码问题。中文 PDF 有时会遇到编码错误提取出来是乱码。解法是检测编码尝试用不同编码解析或者用 OCR 兜底。6.3 切分参数调优的实操经验切分参数没有万能值必须根据文档特点调。我的调优步骤是先固定一个初始值比如 chunk_size1000, overlap100跑一批测试问题记录命中率。然后每次只调一个参数观察命中率变化。chunk_size 从 500 到 1500 扫一遍找到命中率最高的区间。overlap 从 0 到 200 扫一遍找到最佳值。我调过的文档里技术文档最佳 chunk_size 通常在 800-1200法律合同在 500-800因为条款短且独立聊天记录在 300-500因为单条消息短。overlap 一般在 chunk_size 的 10%-15% 之间。还有一个技巧对不同内容类型用不同的切分参数。正文用一种参数表格用另一种代码用另一种。在元数据里标记内容类型检索时按类型分别处理。7. 把文档管线做扎实比换模型划算得多回到开头那个判断RAG 答错的时候先别急着换模型。我自己的经历是把文档解析从 PyPDF2 换成版面分析方案检索命中率涨了 20 多个点把固定切分换成结构感知切分又涨了 10 多个点加上元数据过滤和层级路径再涨 10 个点左右。这些加起来比换任何模型带来的提升都大而且成本低得多——解析和切分是一次性投入模型调用是持续成本。我现在做 RAG 项目的顺序是先把文档管线跑通用测试集验证检索命中率到 85% 以上然后再考虑模型层面的优化。如果检索命中率上不去模型换再多也是白搭。最后分享一个我常用的检查方法随便挑一个用户问题手动在原始文档里找到答案所在的位置然后看这个位置的文本经过解析和切分后变成了什么 chunk这个 chunk 能不能被检索到。如果这一步就断了那问题一定在文档进入系统之前。这个检查方法简单粗暴但每次都能帮我快速定位问题所在。