1. 为什么你的 RAG 系统答非所问,问题多半不在模型
我做过不下二十个 RAG 项目,从最简单的本地知识库到带图谱的复杂检索链路都趟过一遍。每次系统上线后收到最多的反馈就是“答得不准”“模型太笨了”,然后团队第一反应往往是换一个更大的模型、调高 temperature、或者干脆上微调。但实测下来,八成以上的答错案例,根因根本不在生成模型,而在文档进入向量库之前就已经烂掉了。
这个判断不是拍脑袋。RAG 的全称是检索增强生成,它的工作链路是:文档解析 → 文本切分 → 向量化 → 存储 → 检索 → 重排 → 生成。生成模型只是最后一环,它拿到的上下文质量完全由前面几环决定。你喂给它一堆断句错乱、表格串行、标题丢失的碎片,再强的模型也只能基于垃圾上下文编答案。这就是业内常说的 garbage in, garbage out。
所以这篇文章我想聊的核心就一件事:当 RAG 答错时,先回头检查文档进入系统之前的那段流程。我会把文档结构化解析、切分策略、元数据设计、检索命中率排查这几个环节拆开讲,每个环节都给出可复现的操作方法和踩坑经验。适合正在搭 RAG 知识库的开发者、做企业文档问答的工程师,以及被“模型不行”这个结论误导过的朋友。看完你至少能判断出:你手里的 RAG 问题到底出在解析、切分、检索还是生成。
2. 文档进入系统前的整体设计思路拆解
2.1 先搞清楚 RAG 的瓶颈到底在哪一环
很多人把 RAG 当成一个黑盒,输入问题输出答案,答错了就归咎于模型。但只要你把链路拆开,用日志把每一环的中间结果打出来,问题定位会清晰得多。我习惯用一个简单的排查顺序:先看检索回来的 chunk 里有没有正确答案,再看这些 chunk 的原文是否完整,最后才看生成模型有没有用好这些 chunk。
这个顺序背后的逻辑很直接。如果检索回来的内容里压根没有正确答案,那生成模型再强也变不出来,问题在检索或切分;如果检索回来的 chunk 里有答案但被切得七零八落,问题在切分;如果 chunk 完整但模型答错,才轮到怀疑生成环节。我见过太多团队跳过前两步直接换模型,结果换了三四个模型问题依旧,白白烧了时间和算力。
提示:在 RAG 系统里加一个“检索结果可视化”的调试页面,把每次问答命中的 chunk 原文和相似度分数都展示出来。这一个动作能帮你省掉一半的无效调参。
2.2 文档结构化解析为什么是地基
文档结构化解析,指的是把 PDF、Word、Excel、PPT 这些原始文件,转换成保留语义结构的纯文本或结构化数据。这一步听起来简单,实际上是最容易被低估的环节。一份排版复杂的 PDF,用不同的解析工具出来的结果可能天差地别:有的把双栏排版读成左右交错,有的把表格拆成一行行乱码,有的把页眉页脚混进正文。
为什么这一步是地基?因为后面所有的切分、向量化、检索都建立在这份文本之上。解析阶段丢掉的标题层级、表格结构、列表关系,在后续环节是补不回来的。你不可能指望切分算法从一堆乱序文本里还原出“这是第三章第二节下面的一个表格”。所以我的原则是:解析阶段宁可慢一点、复杂一点,也要把结构信息尽量保留下来。
2.3 切分策略决定了检索的天花板
文本切分是 RAG 里最考验经验的一环。切得太碎,单个 chunk 语义不完整,检索时匹配到了但答不出;切得太大,一个 chunk 里混了好几个主题,向量表示被稀释,检索精度下降。常见的固定长度切分(比如每 500 字一刀)看似省事,实际上经常把一句话、一个表格、一个标题和它的正文拦腰截断。
我的经验是,切分要跟着文档的语义结构走,而不是跟着字符数走。标题、段落、列表项、表格这些天然的结构边界,才是理想的切分点。固定长度切分只能作为兜底手段,用在那些结构不明显的长文本上。这个思路和滑动窗口滤波模型处理信号时的逻辑有点像:窗口大小和步长要根据信号本身的特征来定,而不是拍一个固定值。
2.4 元数据设计是被最多人忽略的加分项
元数据就是附着在每个 chunk 上的附加信息,比如来源文件名、章节标题、页码、文档类型、更新时间。很多人觉得这些信息没用,反正检索靠的是向量相似度。但实测下来,元数据在过滤和重排阶段的价值极大。
举个例子,用户问“2024 年的报销标准是多少”,如果你的 chunk 上带了年份和文档类型元数据,检索时就能先按年份过滤,把 2023 年甚至更早的旧标准排除掉,命中率立刻上一个台阶。再比如,用户问某个接口的参数,你可以按文档类型过滤出“接口文档”这一类,避免命中了产品说明书里的无关内容。元数据设计得好,相当于给检索加了一层精准的筛子。
3. 核心细节解析与实操要点
3.1 文档解析:不同格式要用不同打法
文档解析没有万能工具,得按格式分别处理。我把常见格式和对应的处理思路整理成一张表,方便你对照自己的场景选型。
| 文档格式 | 解析难点 | 推荐处理思路 |
|---|---|---|
| PDF(文本型) | 双栏排版、页眉页脚、表格 | 用能识别版面的解析库,按阅读顺序抽取,过滤页眉页脚 |
| PDF(扫描型) | 本质是图片,无文本层 | 先做 OCR,再按文本型 PDF 处理,注意 OCR 纠错 |
| Word | 样式层级、批注、修订 | 按标题样式还原层级,忽略批注和修订痕迹 |
| Excel | 多 sheet、合并单元格、公式 | 按 sheet 拆分,表头单独提取,公式取计算值 |
| PPT | 图文混排、备注页 | 按页抽取,正文和备注分开,图片单独走 OCR |
| Markdown | 相对简单,但代码块要保护 | 按标题层级切分,代码块整体保留不切 |
这里重点说 PDF,因为它是企业文档里最头疼的格式。文本型 PDF 的解析,核心是还原阅读顺序。双栏排版的文档,如果按坐标从左到右读,会把两栏内容交错在一起,读出来完全不通。好的解析库会先判断版面是单栏还是多栏,再按栏的顺序抽取。表格则要识别行列结构,转成 Markdown 表格或结构化数据,而不是简单地把单元格文字拼成一行。
扫描型 PDF 更麻烦,因为它没有文本层,必须先 OCR。OCR 的准确率直接决定了后续质量,尤其是中文文档里的数字、英文缩写、专业术语,OCR 经常认错。我的做法是 OCR 之后加一道人工抽检,把高频出错的词做成替换规则,比如把常见的“0”和“O”混淆、“1”和“l”混淆做针对性纠正。
3.2 文本切分:语义边界优先,长度兜底
切分策略我总结成一句话:优先按语义边界切,长度超限再二次切分。具体操作上,我会按这个优先级找切分点:一级标题 > 二级标题 > 段落 > 句子 > 固定长度。也就是说,先按标题把文档切成大块,如果某块还是太长,再按段落切,段落还长就按句子切,最后才用固定长度硬切。
为什么要这个顺序?因为标题和段落是作者已经组织好的语义单元,顺着它们切,每个 chunk 的语义最完整。固定长度切分是最后手段,用在那些没有明显结构的长文本上,比如一整段没有换行的说明文字。
切分长度上,我一般把目标 chunk 控制在 300 到 800 字之间,具体看文档类型。技术文档、接口文档这种信息密度高的,可以短一点,300 到 500 字;叙述性的说明文档可以长一点,500 到 800 字。相邻 chunk 之间留 10% 到 20% 的重叠,避免边界处的信息被切断后两边都检索不到。
注意:切分时一定要保护代码块、表格、公式这些特殊结构。一个代码块被从中间切开,检索出来就是半截代码,模型根本没法用。我的做法是在切分前先标记这些特殊区域,切分时把它们当作不可分割的整体。
3.3 元数据字段设计:至少带上这五类信息
元数据字段不用设计得太复杂,但有几类是必须的。我通常会给每个 chunk 带上这几类信息:
- 来源标识:文件名、文件路径、文档 ID,用于追溯和去重。
- 结构位置:章节标题、页码、在文档中的顺序号,用于还原上下文。
- 文档属性:文档类型、所属业务域、版本号、更新时间,用于过滤。
- 内容类型:正文、表格、代码、列表,用于差异化处理。
- 权限标签:可见范围、密级,用于多租户或权限隔离场景。
这几类信息里,结构位置和文档属性对检索精度的提升最明显。结构位置能让你在检索到某个 chunk 后,顺藤摸瓜把它的父章节或相邻 chunk 也拉进来,补全上下文。文档属性则能在检索前做预过滤,把明显不相关的文档排除掉,减少噪声。
3.4 向量化与 embedding 模型的选择逻辑
embedding 模型的选择,核心看两点:语言和领域。中文文档优先选对中文支持好的模型,中英混合的文档要选多语言能力均衡的。领域方面,通用模型在通用语料上表现不错,但遇到法律、医疗、金融这些专业领域,术语的向量表示可能不准,这时候要么选领域微调过的模型,要么在检索时配合关键词召回做互补。
embedding 模型排行这类信息网上很多,但我不建议盲目追榜。榜单上的评测集和你的实际文档分布往往不一样,榜一模型在你的场景里未必最好。我的做法是:拿一批自己业务的真实问答对,构造一个小型评测集,把候选模型都跑一遍,看检索命中率,用数据说话。这个评测集不用大,一两百条就够,但一定要来自真实场景。
4. 实操过程与核心环节实现
4.1 从原始文档到结构化文本的完整流程
我把整个流程拆成六步,每一步都有明确的输入输出,方便你对照实现。
第一步,格式识别与分流。拿到文件后先判断格式,PDF、Word、Excel、PPT 分别走不同的解析分支。这一步用文件扩展名加文件头魔数双重判断,避免扩展名被改错导致解析失败。
第二步,版面分析与内容抽取。对 PDF 做版面分析,识别文本块、表格、图片区域,按阅读顺序抽取文本。对 Word 按样式层级抽取,保留标题级别。对 Excel 按 sheet 和表头结构抽取。
第三步,结构还原。把抽取出来的内容还原成带层级的结构化文本,标题用 Markdown 的 # 表示层级,表格转成 Markdown 表格,列表保留项目符号。这一步的产物是一份“结构化的中间文本”。
第四步,清洗与规范化。去掉页眉页脚、页码、水印文字,统一全角半角,修正 OCR 错误,合并被错误换行拆开的句子。
第五步,语义切分。按前面讲的优先级找切分点,生成 chunk,每个 chunk 带上元数据。
第六步,向量化与入库。对每个 chunk 调 embedding 接口生成向量,连同原文和元数据一起写入向量库。
这六步里,第三步和第五步是最容易出问题的。结构还原没做好,切分就失去了依据;切分没做好,后面全白搭。我建议在这两步之后都加人工抽检,随机抽几十个 chunk 看质量,发现问题及时调整。
4.2 切分参数的计算与选择过程
切分长度不是拍脑袋定的,可以结合 embedding 模型的上下文窗口和检索粒度来算。假设你用的 embedding 模型最大输入是 512 个 token,那单个 chunk 的 token 数就不能超过这个值,否则会被截断。中文里一个汉字大约对应 1 到 2 个 token,所以 512 token 大概对应 250 到 500 个汉字。这就是为什么我把中文 chunk 的目标长度定在 300 到 500 字。
重叠长度一般是 chunk 长度的 10% 到 20%。重叠的作用是防止边界信息丢失。比如一句话正好被切在两个 chunk 的边界上,有了重叠,这句话在两个 chunk 里都完整出现,检索时至少有一个能命中。重叠也不能太大,否则存储和检索成本上升,还容易召回重复内容。
我实际项目里常用的参数组合是:目标长度 400 字,最大不超过 600 字,重叠 60 字。这个组合在技术文档和说明文档上表现比较均衡。如果你的文档句子特别长,可以适当调大;如果文档碎片化严重,可以调小。
4.3 检索命中率的实测与调优记录
调优检索命中率,我习惯用一个可量化的流程。先准备 100 条真实问题,人工标注每条问题的正确答案在哪个文档、哪个章节。然后跑一遍检索,统计 top-5 命中率(正确答案出现在前 5 个结果里的比例)。这个指标比单纯看相似度分数靠谱得多。
我最近一个项目里,初始 top-5 命中率只有 62%。排查后发现两个问题:一是 PDF 表格解析错乱,导致表格里的数据检索不到;二是切分时把标题和正文切开了,标题单独成一个 chunk,正文里没有标题信息,检索时匹配不到。针对第一个问题,我换了表格识别能力更强的解析方案;针对第二个问题,我改成标题和其下第一段正文合并成一个 chunk。两处调整后,top-5 命中率提到了 89%。
这个过程中我最大的体会是:命中率上不去,先别动 embedding 模型,先查解析和切分。解析和切分的问题解决了,命中率往往能提升二三十个百分点,比换模型的效果明显得多。
4.4 一个完整的排查案例复盘
说一个我印象最深的案例。有个知识库系统,用户问“某个功能的超时时间是多少”,系统总是答错,要么答成别的参数,要么说找不到。团队一开始怀疑是模型不行,准备换模型。
我接手后先看检索结果,发现命中的 chunk 里确实有“超时时间”这几个字,但内容是“超时时间的设置请参考第三章”。真正的数值在第三章的表格里,而那个表格被解析成了乱码,根本没进向量库。问题根因是表格解析失败,跟模型一点关系都没有。
修复方案是重新处理所有含表格的文档,用能识别表格结构的解析方案,把表格转成 Markdown 后重新入库。修复后这个问题再没出现过。这个案例让我更加确信:RAG 答错,先查数据入口,再查模型。
5. 常见问题与排查技巧实录
5.1 检索不到答案的六种典型原因
我把检索不到答案的原因整理成一张速查表,遇到问题可以按这个顺序排查。
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 完全检索不到相关 chunk | 文档没入库或解析失败 | 检查文档处理日志,确认 chunk 数量 |
| 检索到但内容不完整 | 切分把关键信息切断了 | 查看命中 chunk 的原文,看是否被截断 |
| 检索到但答非所问 | 向量表示被无关内容稀释 | 检查 chunk 是否混了多个主题 |
| 相似度分数普遍偏低 | embedding 模型不匹配领域 | 换领域模型或加关键词召回 |
| 旧文档干扰新问题 | 缺少时间或版本过滤 | 补充元数据,检索时加过滤条件 |
| 表格数据检索不到 | 表格解析失败 | 检查表格是否被正确转成结构化文本 |
这张表里的每一行,我都实际遇到过。其中“表格数据检索不到”和“切分把关键信息切断”是最常见的两个,加起来能占到问题的一半以上。
5.2 解析阶段的避坑清单
解析阶段的坑,我踩过的总结成几条:
- 别信扩展名:有的文件扩展名是 .pdf,实际内容是 Word,解析前用文件头判断真实格式。
- 页眉页脚必须去:不去掉的话,每一页的页眉都会进 chunk,检索时全是噪声。
- OCR 要抽检:扫描件 OCR 后一定人工看几十页,把高频错字做成替换规则。
- 表格单独处理:表格不要和正文混在一起切,单独转成结构化格式,带上表头。
- 代码块要保护:技术文档里的代码块,切分时整体保留,不要从中间断开。
- 多栏 PDF 要按栏读:按坐标从左到右读会把两栏内容交错,必须按版面顺序读。
提示:解析完成后,随机抽 20 个 chunk 打印出来人工看一遍。这一步花不了多少时间,但能发现大部分解析问题。
5.3 切分阶段的避坑清单
切分阶段的坑同样不少:
- 标题不要单独成 chunk:标题单独切出来,正文里就没有标题信息了,检索时匹配不到。标题要和它下面的正文合并。
- 列表项不要拆散:一个列表的多个项如果被拆到不同 chunk,语义就断了。整个列表作为一个 chunk 处理。
- 重叠别太大:重叠超过 30% 会导致大量重复召回,浪费检索名额。
- 特殊结构要标记:代码、表格、公式在切分前先标记,切分时跳过这些区域。
- chunk 长度要统计:切完后统计一下长度分布,如果大量 chunk 过短或过长,说明切分策略有问题。
5.4 元数据与检索阶段的避坑清单
- 元数据要能过滤:设计元数据时想清楚哪些字段会用于检索过滤,这些字段要建索引。
- 时间字段要规范:时间格式统一,避免“2024年”“2024-01”“2024/01/01”混用导致过滤失效。
- 权限标签要前置:多租户场景下,权限过滤要在检索前做,不能检索后再过滤,否则会泄露信息。
- 相似度阈值要调:设一个相似度下限,低于阈值的 chunk 不返回,避免噪声干扰生成。
- 重排模型可选配:检索召回一批后,用一个重排模型精排,能明显提升 top 结果的相关性。
5.5 我的独家排查口诀
最后分享一个我自己总结的排查口诀,遇到 RAG 答错时按这个顺序走:一看检索有没有,二看 chunk 全不全,三看元数据对不对,四看切分碎不碎,五看解析烂不烂,最后才看模型换不换。这个顺序是从数据入口往生成端倒着查,能最快定位到根因。我用了这套口诀之后,排查效率至少提升了一倍,也避免了很多次盲目换模型的浪费。
6. 文档结构化解析的进阶思路
6.1 从纯文本到结构化数据的升级
前面讲的解析,产物主要还是文本。但如果你的 RAG 要处理大量表格、表单、结构化数据,纯文本就不够用了。这时候需要把文档解析成结构化数据,比如把表格转成 JSON,把表单字段和值提取成键值对。这样检索时不仅能按文本相似度匹配,还能按字段精确查询。
举个例子,一份产品参数表,解析成结构化数据后,用户问“某型号的功率是多少”,系统可以直接按型号字段精确匹配,而不是靠向量相似度去猜。这种精确匹配的准确率远高于纯向量检索。所以对于结构化程度高的文档,我建议解析阶段就把它结构化,而不是硬塞进文本 chunk。
6.2 本体与图谱思路在 RAG 里的应用
当文档之间的关联关系很重要时,可以考虑引入本体或图谱的思路。简单说,就是把文档里的实体和关系抽出来,建成一张知识图谱,检索时既走向量召回,也走图谱的关系查询。比如用户问“A 功能和 B 功能有什么关系”,纯向量检索可能召回两段分别讲 A 和 B 的文本,但答不出它们的关系;有了图谱,就能沿着关系边找到答案。
这个思路在业内叫 graphrag 或本体 RAG,适合文档间关联复杂、需要多跳推理的场景。不过它的实现成本比普通 RAG 高不少,实体抽取和关系构建都需要额外的工作量。我的建议是:先用普通 RAG 跑起来,等遇到明确的多跳推理需求时再考虑升级,不要一上来就上图谱,容易过度设计。
6.3 agentic rag 的取舍
agentic rag 是让模型自己决定检索什么、检索几次、怎么用检索结果。它比固定流程的 RAG 灵活,能处理复杂问题,但代价是延迟高、成本高、可控性差。我的经验是,大部分企业知识库场景用固定流程的 RAG 就够了,agentic rag 适合那些问题复杂、需要多轮检索和推理的场景。
取舍的关键看你的问题复杂度。如果用户问题大多是“某个参数是多少”“某个流程怎么走”这类单跳问题,固定流程足够。如果是“对比 A 和 B 的差异并给出建议”这类需要综合多份文档的问题,agentic rag 才有优势。别为了追新概念上 agentic rag,结果延迟翻倍、成本翻倍,用户体验反而下降。
7. 把功夫花在文档进系统之前
做了这么多 RAG 项目,我最大的体会就是:RAG 的效果上限,在文档进入系统之前就已经决定了。解析和切分做得好,普通模型也能答得准;解析和切分做得烂,再强的模型也救不回来。所以每次有人问我“RAG 答不准换什么模型”,我都会先反问一句:你的文档解析和切分检查过了吗?
如果你现在手里的 RAG 系统效果不理想,我建议你先别动模型,花半天时间做三件事:把检索命中的 chunk 原文打出来看一遍,统计一下 chunk 长度分布,随机抽几十个 chunk 人工检查质量。这三件事做完,你大概率能找到真正的瓶颈。等解析和切分都扎实了,再去调 embedding 和生成模型,那时候每一分调优都能看到实实在在的效果提升。
最后再分享一个小技巧:给你的 RAG 系统加一个“反馈按钮”,让用户标记答错的案例。定期把这些错例拿出来复盘,看是检索没召回、chunk 不完整还是生成跑偏。积累几十个错例之后,你会发现自己系统的短板非常集中,往往就是那么两三个环节的问题。针对性修复这两三个环节,比漫无目的地换模型有效得多。