先说结论:RAG 检索不准,大概率不是向量模型的锅,而是入库阶段就没有对症下药。很多人一上来就把“检索结果差”归咎于 embedding 选得不够好、向量维度不够高,但实际排查下来,九成的问题出在文件解析、切分策略、元数据设计和索引结构上。同样的 PDF,合同类和产品手册类应该走完全不同的入库流程;同样的 Markdown,API 文档和会议纪要也不能用同一套切片规则。这篇文章就把我在多个 RAG 项目里踩过的坑、试出来的方案一次讲清楚。
1. 先把锅甩明白:检索不准的根因到底在哪
1.1 向量模型只负责“语义压缩”,不负责“理解”
很多人对向量有一个误解,觉得 embedding 模型应该像人一样“读懂”文档,所以检索不准就换更大更强的 embedding,或者从 bge 换到 OpenAI 的 text-embedding-3。但实际上向量模型做的事情只是将文本压缩成一个高维向量,尽量让语义相近的文本在向量空间里距离更近。它是在“语义压缩”,不是在做“语义理解”。
举个例子:把“苹果手机怎么设置铃声”和“iPhone 如何更换铃声”这两个句子向量化,它们的向量距离会比较近,因为两句话的核心语义确实一致,这是 embedding 模型真正擅长的事。但如果你的知识库里有一份 200 页的 PDF,里面混着合同条款、技术规格、公司介绍,直接被切成长度为 500 字符的 chunk,每个 chunk 里可能有一半的内容是废话,另一半才是关键信息,那 embedding 压缩出来的向量就会被“稀释”。这时检索不准,问题真的不在向量模型,而在你入库时没有把文档中的“有效语义单元”提取出来。
我在实际项目中做过一个对比实验:同一个知识库、同一套 QA 测试集、同一个 embedding 模型,唯一区别是入库方案。粗糙的固定长度切分方案,检索命中率只有 52%;而按文档结构精细化处理后,命中率到了 81%。这个差距不是换模型能补回来的。
1.2 检索不准的四个隐形杀手
真正影响 RAG 检索质量的,通常是下面四件事:
第一,解析率没有查过。PDF 转出来的文本是不是乱码,Word 里的表格有没有被丢掉,扫描件有没有做 OCR,这些在入库之前如果没有验证,后面所有环节都会放大问题。我见过非常夸张的情况,一个 50 页的 PDF,解析之后只剩 6 页有效文本,其余全是空的,检索结果当然一塌糊涂。
第二,切片策略一刀切。不同类型文件的边界完全不同。一份契约文档,条款和条款之间是天然边界;一份操作手册,章节和步骤之间是边界;一份财务报表,表格和表头之间是边界。统一用固定字符数切分,等于把这些边界全部打碎。你切出来的 chunk,很多都是“语义断头路”。
第三,元数据缺失。如果你入库时没有记录文档的标题、章节号、页码、层级,甚至没有记录它属于哪一类文件,那么检索阶段就只能对着向量做全局相似度计算。全局检索在小规模数据上勉强能跑,但一旦数据量上来,没有元数据过滤的检索,基本就是大海捞针。加了一个简单的日期或分类过滤条件,检索精度往往能提升一大截。
第四,检索策略和入库策略没有联动。入库方案决定了你能用什么样的检索方式。如果你入库时给每个表格单独建了一个块并附加了表格描述文本,那检索时就可以针对“表格类查询”走专门的路径。如果你入库时只是把表格当作普通文本混在段落里,检索时即使是明确的表格问题,也只能靠语义相似度硬碰运气。
检索质量问题绝大多数在这四个环节产生,而不是向量模型本身。明白了这一点,下面每一步才好展开。
2. 文件类型的差异化处理:不要再把所有文件塞进同一个流水线
2.1 办公文档(PDF / Word / PPT):先分层解析,再按结构切
办公文档是所有 RAG 项目里最常见也最容易被粗暴处理的类型。很多搭建教程给的方案是:pdf 加载器把文本抽出来,按固定 chunk_size 切片,入库完事。但实际项目中,PDF 的排版形态差异极大——有的 PDF 是文本型的,可以直接抽取;有的是扫描图片型,需要 OCR;有的混合了两者。Word 文档里存在表格、批注、页眉页脚;PPT 里存在文本框、图形、备注。
我现在的做法是先把文档解析分成两条路径:文本型 PDF 和 Word 用解析器直接抽取文本,同时记录标题层级和页码;扫描型 PDF 走 OCR 流程,识别完文本后还要做一次文本校正,因为 OCR 结果里经常有把“0”识别成“O”这样的错误。解析完成后,对文本内容和表格内容分别成立单独的块,再基于标题结构来规划切分点,不按固定长度硬切。
这里有个极其关键的细节:不要直接“无脑拼接”整份文档后切片。一个自然的处理是:先抽取大纲,把每一章、每一节作为基础容器,然后再把内容填进容器里。如果某节内容过长,再按段落或句组二次细分;如果某节内容很短,就保持整节作为一个块,不要为了统一 size 硬拆。
顺序上,解析结果一定要先抽样检查。我习惯在入库前打印 20% 的解析文本,人工扫一眼有没有乱码、有没有缺字段、表格是不是完整。解析不了的文件,做得再多也没用。
2.2 表格与 Excel:别把表格拍平了存向量,要“双轨入库”
表格是 RAG 检索里最容易翻车的文件类型。原因简单:表格的语义不在某个格子单独的词上,而在表头与行列的组合关系中。你把表格转成一段纯文本,比如“姓名 张三 年龄 28 部门 技术部”,再扔给 embedding,模型只会把它当成一串普通文本,完全感知不到“哪一列对应什么含义”的结构关系。
我的方案是“双轨入库”:一条轨放表格的语义描述向量,一条轨放表格结构的原始数据。
具体做法:对每个表格抽取时,会生成两个块。第一个块是“表格描述块”,内容是一段给向量模型用的自然语言描述,比如“这是一个员工信息表,包含姓名、年龄、部门三列,共 50 行数据,其中张三属于技术部,年龄 28 岁”。第二个块是“表格数据块”,将原始表格结构化保留为 JSON 或者 Markdown 表格格式,作为精确查询时的数据源,并且设置好 block_type=table 的元数据标记。
检索的时候就有两条路径:如果用户问的是“张三在哪个部门”,语义向量可以先通过表格描述块命中候选表格,然后程序读取对应的表格数据块,做精确的行列定位,把“技术部”这个答案返回给用户。如果只做向量检索,把整张表塞成一段文本,那这种问题基本答不准。
你可能会问:为什么不直接把整张表向量化?因为表格一旦长起来,动辄几十行、几百行,直接向量化后信息被压缩得面目全非,而且不同行之间的相似度极高,容易互相干扰。拆成“描述块 + 数据块”之后,检索精度和答案准确性都会有明显提升。
2.3 扫描件与图片型文档:OCR 只是半成品,还要做版面还原
不少知识库里的文件是扫描件,尤其合同、书籍、历史档案。这种文件直接向量化等于自欺欺人。因为原始内容根本不在文本层,不先 OCR,你向量化进去的几乎是空白信息。
OCR 本身不复杂,但真正容易被忽略的是 OCR 后的版面还原。扫描件往往有多栏排版,OCR 工具按行输出的文字顺序是乱的,第一栏读完跳第二栏,再跳回第一栏,最后出来的文本完全没有逻辑顺序。这种文本直接切片,切出来的 chunk 里语义支离破碎。
我在实践中给扫描件定了一套流程:先 OCR 得到带坐标的识别结果,然后根据坐标信息判断栏位,还原正确阅读顺序,再按标题层级识别出章节结构。最后输出的文本保留标题、编号、段落和独立表格。只有这样,后续基于结构切分才有意义。OCR 出来的文本必须经过版面整理,否则它的可用性约等于零。
另外一个现实问题:OCR 会引入识别噪声,包括错字、漏字、标点异常。在做向量化前可以加载一个轻量级的文本纠错模块,对明显异常的词做纠正,例如“姓名:张兰”被识别成“姓名:张 兰”时,去空格、校正格式。这些细节直接关系到后续检索时字面匹配的准确率。
2.4 Markdown 与 HTML:利用天然结构,别做“标题黑洞”
Markdown 和 HTML 是结构特征最明显的文件类型。Markdown 本身带有 #、##、列表、代码块、表格符号;HTML 有 h1、h2、p、table、li 标签。如果对这些直接用纯文本切片,相当于把这些现成的语义边界全部丢掉。
我的做法是,在解析阶段保留每个块对应的层级信息。Markdown 文件解析时,把二级标题作为顶层切分点,其下所有内容并入该节点;如果单个二级标题下内容过多,再按三级标题细分成子块。以代码为主的 Markdown 文档,代码块单独抽出作为一个块;以表格为主的内容,仍然用上面说的表格双轨方案处理。
HTML 网页抓取的内容会更脏,里面通常混有导航栏、页脚、侧边栏、广告和推荐位。推荐做法是:先做正文提取,类似于读取文章主内容区,丢掉无关元素;再做清洗,把超链接文本、图片 alt 描述、Cookie 弹窗文本等全部剔除;最后再按标题标签和段落结构切块。如果你把整页 HTML 的纯文本都塞进去,检索结果里很容易出现导航栏和页脚的无关文本。
2.5 代码仓库:统一向量化等于灾难,建议“词法块 + 调用关系”双通道
把代码仓库直接喂给 RAG,是很多团队做过又后悔的事。代码和自然语言的语义结构差异太大。代码的逻辑存在于函数定义、类定义、模块依赖和调用关系中,而不是在变量名的字面相似度里。
我在代码类知识库里的处理方式是双通道:一个通道用于把“函数、类、方法”作为独立块向量化;另一个通道保存调用关系和依赖图。向量通道解决的检索问题是“找到与你描述相关的函数”、“找到某个模块”;调用关系通道解决的是“这个函数被谁调用”、“哪些模块依赖这个组件”。当用户问到工程结构类问题时,答案从调用关系通道里取,而不是靠向量猜。
再补充一个细节:代码块入库前要把注释和文档字符串抽取出来和代码主体拼接。原因很简单,注释是代码语义最好的自然语言锚点。如果只存代码,用户用自然语言提问时,embedding 很难在代码文本和自然语言之间建立足够的语义联系。加上注释之后,向量检索命中率会明显好转。
2.6 图片和纯图片型知识:RAG 知识库能存图片吗
这是很多人都会问的一个问题。如果你说的“存图片”是指把图片文件的二进制直接塞进向量库,那答案是:没有意义,也不推荐。向量库不是文件服务器,图片检索应该走的是“图文双路模型”——先对图片做视觉特征提取,再与文本做跨模态对齐,而不是把图片文件本身向量化。
RAG 知识库中处理图片的合适做法是:如果图片是文档中的插图,入库时保留图片的描述文本和上下文文本,配合视觉模型生成 caption,让向量包含“图片内容描述的语义”;检索阶段,当用户的 query 命中了某段带插图的内容时,再把图片对应的文件路径作为附件一起返回给前端展示。这才是图片在 RAG 里的正确归宿——图片是检索结果的补充展示,而不是检索对象本身。
3. 切片策略的实操细节:固定长度切分何时用,何时绝对不能用
3.1 固定长度切分为什么会翻车
固定长度切分(chunk_size=500,overlap=50)几乎是所有 RAG 教程的默认起手式,因为简单、速度快。但它的缺陷也很致命:它完全不考虑语义边界。几个典型的翻车场景:
- 一个表格刚好被从中间切开,表头在第一块,数据在第二块,两块的语义都不完整。
- 一个代码函数被切成两半,每一半都不是可理解的完整逻辑。
- 段落和段落之间的关联性被打断,某一块丢失了关键的上下文主语,比如“该公司”被切到后一块,而“公司”具体指代哪家,在前一块。
这些切法会直接拉低 embedding 的质量,因为模型拿到的是一堆残块。凡是文档本身带有明显的结构性边界,我都不建议用固定长度切片。
3.2 按文档结构切分:Markdown 标题、PDF 章节、表格边界
文档结构切分是目前最值得优先采用的策略。具体落地时可以按顺序执行下面几步:
第一步,解析文档大纲。对 PDF 和 Word,提取标题、章节号、层级关系;对 Markdown,直接读取 # 和 ## 标签;对 HTML,读取 h1-h3 标签。第二步,把每一个“标题节点”下的所有内容归拢到一起,形成一个候选块。第三步,对每个候选块判断长度是否合适,如果它超过设定的目标长度,就按段落拆成多个子块;如果它本身很短,就保持整块。第四步,把表格、代码块、列表等特殊结构单独抽出来建块,并且它们的父级标题作为元数据一并记录。
这套流程下来,你的每个块不再是“任意裁出来的 500 字”,而是有着明确语义边界的独立内容单元。向量化的质量通常会有肉眼可见的提升。
3.3 语义切分的落地:先粗切、再合并,别迷信模型切分
语义切分听起来高级,但落地时不要一上来就上大模型。成本高、速度慢,而且有些长文档模型一次也处理不完。我的实践是“先粗后精”的两步法:
第一步,粗切:基于章节、段落、句子边界,把文档切成一个一个小片段,确保每个片段内部是一个完整的句子或段落。第二步,精合并:设定一个最小长度阈值,比如 150 字符;低于这个阈值的相邻片段,按语义相关度合并——看它们的标题是否相同、是否属于同一个列表、是否前后文承接。合并的目标是形成一个长度在 200~500 字符区间且语义连贯的块。
这样做的效果,和直接用 embedding 模型做语义切分类似,但成本和稳定性都友好得多。大模型切分适合那些结构特别混乱、无标题可依的文件,比如纯聊天记录、杂乱的笔记。大多数正式文档,结构切分 + 粗切精合已经完全够用。
4. 入库阶段的元数据设计:检索效果的分水岭
4.1 元数据是过滤条件,不是摆设
很多人把元数据理解成“给块加个来源标签”,其实元数据是检索阶段最重要的过滤工具。如果检索时没有元数据过滤条件,所有查询都变成在全库范围内的向量相似度计算。库越大,相似噪声越多,命中率越低。而加了元数据过滤,等于先把搜索空间缩小到一个高相关的分区,再在这个分区内做向量检索,精度完全是两个量级。
实践中最好的元数据体系是“两级”的:文档级元数据和块级元数据。文档级元数据描述整个文档的信息,比如文档标题、文档类型、来源、日期、作者、所属产品线、是否经过 OCR 等;块级元数据描述该块自身的信息,比如所属章节标题、章节编号、页码、块类型(段落、表格、代码、列表)、块序号、父子块关系等。
入库的时候把这两级元数据一起写入向量数据库,检索阶段先按文档级元数据过滤(比如只看指定日期之后的文档、只看某产品线的文档),再按块级元数据过滤(比如只看某个章节下、只查表格类型块)。这种过滤策略的效果,往往比换 embedding 模型要大得多。
4.2 一个可以照抄的元数据模板
下面是一个比较通用且实用的块级元数据模板,按需裁剪即可:
- block_id:块的唯一编号,建议用“文档ID + 章节号 + 序号”拼接。
- doc_id:所属文档的全局唯一 ID。
- doc_type:文档类型,如 pdf、word、markdown、html、table、image。
- title:一级标题或文档标题。
- chapter:二级标题或当前章节标题。
- chapter_no:章节编号,比如 3.2。
- page_no:页码(如果有)。
- block_type:block 类型,如 paragraph、table、code、list、formula。
- parent_id:如果当前块是从上级块切分出来的,记录父块 ID,用于层级检索。
- timestamp:文档时间戳或入库时间。
- embed_model:记录生成向量的模型版本号。
这个模板我一直在用。有一个很大的收益:当某个块检索出来后回答质量差,可以顺着元数据追查它的来源、类型和切分方式,快速定位是不是入库阶段的问题。没有元数据的时候,出了质量问题只能靠猜。
5. 检索侧的配套优化:入库方案和检索策略要联动
5.1 混合检索:向量 + 关键词,别再只靠语义
向量检索擅长语义模糊匹配,关键词检索(如 BM25)擅长精确字面匹配。RAG 里的现实是:用户提问既可能是模糊口语(“这个证书怎么申请”),也可能是精确术语(“CMMI 三级认证流程”)。如果只靠向量,精确术语里特有的关键词可能被 embedding 稀释;如果只靠关键词,口语化问法又命中不了任何文档。
所以入库方案从一开始就要为混合检索做准备。切片的时候保留原文的“字面信息”,检索的时候分别跑向量检索和 BM25 检索,然后做结果融合。融合策略不复杂,最简单的可以用 RRF(Reciprocal Rank Fusion):对两路结果的分排序给定一个加权倒数排名分,合并后取 Top-N。这个方案实测下来的效果通常比单一向量检索好不少,尤其在专业术语密集的领域内。
5.2 重排序:检索结果粗选和精选要分开
很多 RAG 流程里,检索完 Top-N 直接丢给大模型生成回答。但 Top-N 里往往混着很多语义相似但实际无关的块。这里我强烈建议加一个 Rerank 环节:向量检索负责“粗选”,从全库召回 50 个候选块;Rerank 模型负责“精选”,把这 50 个块按与问题的相关度重新排序,只取前 10 个送入大模型。
Rerank 的成本不高、集成也不复杂,但效果提升非常明显。选择 Rerank 模型时优先挑跨语种能力强的,中文场景下很多开源 rerank 模型已经够用。这个环节既不需要改入库方案,也不需要懂复杂的算法,性价比极高。
5.3 基于元数据过滤的召回策略
入库时记录了元数据,检索时就该把它用起来。我的一个习惯做法是:在问答型 RAG 中预先把用户的问题做一个意图预判,如果问题含有明确的对象限定词,比如“2023 年的预算表”“A 产品线的缺陷记录”,就把对应元数据过滤条件直接附加到检索请求中。这一步在向量检索之前执行,能大幅减少无关候选。
这种召回方式依赖的正是入库阶段规范的元数据写入。如果你入库时没有给每个块打上 doc_type、date、product_line 这类标签,到检索阶段就只能对着整个库裸跑向量,效果自然不稳。可见,检索优化不是事后补救,它在前端入库的那一刻就已经被决定了。
6. 常见问题与排查技巧实录
6.1 入库完检索结果全是空:先查解析丢失
遇到检索结果大面积为空、或者答非所问时,我一般先看解析环节有没有丢内容。操作方式:从向量库里随机抽 20 个块,打印原文,看看文本是否完整、是否有乱码、是否有大量缺失。尤其是 PDF 文件里的表格、图片旁的 caption、页眉页脚附近的文字,都是常见的丢失区。如果结果不完整,那就不是向量的问题,也不是切分的问题,而是解析器压根没把内容抽出来。OCR 的扫描件更要注意,识别率不高时,先提升 OCR 质量再谈下一步。
6.2 query 很短但检索结果全是噪音:切分粒度和关键词密度不匹配
如果你发现用户输入几个关键词时,召回结果全是含有这些词但语义关联度很低的块,问题大概率出在切片粒度太粗或太细。切得太粗,一个块里语义太多,embedding 无法聚焦;切得太细,语义上下文丢失,只留下字面相关性。权衡下来,我通常把通用文本的目标块长度设在 300~500 字符之间,表格、代码块这类特殊内容按结构单独建块。如果你做的是专业领域技术文档,句和句之间的连贯性要求更高,块可以稍微长一些,控制在 500~800 字符。
6.3 表格检索永远答不对:别再只靠向量撞运气
表格问题答不对,十有八九是因为你根本没有保存可查询的结构化数据。我见过太多项目,把表格转成纯文本塞进向量库,用户问“这个表格里某一行是什么”时,RAG 完全依赖向量相似度去猜,结果可想而知。改用“表格描述块 + 表格数据块”的双轨入库方案之后,表格类问题的正确率明显上升。记住:表格不是文本,表格是结构化数据。你要给检索系统两条路,一条用来找表格,一条用来查数据。
6.4 线上改了切分策略,历史数据要不要全量重建
强烈建议全量重建。向量化和切分策略是强耦合的,你改了切分逻辑,意味着每个 chunk 的边界都变了,旧的向量对应的文本已经不存在了。如果只增量入库,新旧块混在一起,检索时会出现同一个文档一部分是旧切法、一部分是新切法,语义边界不统一,质量反而更容易劣化。实践中我会维护一个文档版本号,每次切分策略变更后,只对受影响文档做全量重建,其他文档保持不动。所有模型和切分策略都写入到元数据里,以后出问题可以把策略演进历史倒推出来。
7. 一些值得带走的实战心得
最后分享几个我在真实项目中的操作感受:入库方案远比选模型重要。换模型能带来的提升,很多时候不如把 PDF 解析做好、把元数据加上、把切分从固定长度改为结构切分。每当你觉得检索效果差,先花半天时间查解析和切分,再考虑换 embedding 模型。
统一入库只是一个起点,真正决定 RAG 上限的是你是否愿意为每一类文件单独设计处理流程。文档类型一旦确定,就把解析、清洗、切分、元数据、检索路径都定了下来。不同文件用不同方案,这其实不是复杂度变高了,而是让每一类文件都只做它最擅长的事。
另外建议做一个可视化抽样工具,入库后随机抽几个块,展示切分结果和对应元数据。这个工具可以极大加速排查问题,因为大部分检索错误只要看一眼块的内容和边界就能找到原因。
项目上线后,也不要停下优化入库策略的步伐。RAG 系统的效果,说到底不是靠某一个时刻的完美配置一劳永逸,而是在持续调整中逐步逼近“能让用户真正信任检索结果”的状态。