后台总有人问我同一个问题:“我的RAG系统用的向量模型不差,向量库也没问题,为什么检索出来的片段就是不对?”问得多了,我总结出一个规律:大多数RAG检索效果差,问题不在模型,而在结构。这里的“结构”不单指某一种切分规则,而是一整套从文档解析、索引块组织、查询改写,到多路召回融合的检索骨架。这篇文章想把这些年做企业知识库、文档问答和私有资料检索时踩过的结构坑摊开来聊一聊,给正在调RAG的朋友一份能直接照做的排查思路。如果你也是刚搭好一个RAG Demo但检索结果忽好忽坏,或者已经换了好几轮Embedding模型却仍然不稳定,那这篇文章应该能帮你省下不少时间。
1. 先别急着换模型,先拆开“检索结构”这四个字
1.1 三种典型的“检索差”症状,根因都藏在结构里
我接触到的RAG项目,反馈“检索效果差”的方式通常就三种。第一种是“查不到”,用户问的内容明明在文档里写了,但系统召回Top5里就是找不到相关片段,最后模型只能硬编。第二种是“张冠李戴”,比如一份组织机构制度里同时提到了A部门和B部门,A负责预算,B负责报销,模型却把A的职责答到了B的头上。第三种是“部分正确但缺关键信息”,答案方向对,但漏掉了时间、金额、人员名单这类关键细节。
这三种症状表面上看是模型能力不足,但逐条去追召回链路时,绝大多数问题都出在同一个地方:文档在进入索引之前,就已经被不合理的结构处理破坏了。比如文档解析时丢了标题层级,明明是“第三章 请假流程”下面的内容,却和“第七章 薪酬发放”的段落切进了同一个块;再比如按固定字数切块时,把完整的一张表格拦腰截断。信息本身在库里,可它被切得七零八落,向量检索再怎么算相似度,也拼不回一个完整答案。
所以排查RAG效果差,我从来都是先从结构入手,而不是先怀疑向量模型或生成模型。结构就像是图书馆里给每本书分类和排架的规则,分类和排架做乱了,哪怕图书管理员找书的算法再强,也会把正确的书漏掉。
1.2 为什么大家第一反应总想换向量模型
答案很现实:换模型省事,而且确实能看到效果。一个更强的向量模型通常能提升几个百分点的召回率,这种即时反馈会让人觉得问题就在模型上。但换个角度想,如果文档块本身就有问题,比如一个块里混杂了三个不相关的话题,那再强的Embedding也只会把这个“混杂块”整体匹配出来,你得到的结果自然还是混杂的。
我见过一个项目,团队连续换了三版向量模型,指标都只提升了一点,后来把切分方式从“固定长度切块”改成“按章节标题结构切块”之后,Top5准确率直接跳升了一截。原因是文档里的核心信息本来集中在同一节,被固定长度切断后,向量模型每次只能看到半个事实,再强也算不出完整语义。
给大家一个排查顺序的建议:先查文档解析层是否保留了原始结构;再查Chunk层面是否把语义边界切对;接着查元数据和召回策略;最后才去动向量模型和重排序模型。把这个顺序反过来,大概率会浪费大量算力和时间在错误的地方。
2. 解析层、索引层、查询层:结构在三个环节里如何决定检索成败
2.1 文档解析层:标题层级和表格边界一旦丢了,后面全盘出错
文档解析是所有RAG项目里最容易被低估的一步。不少项目为了省事,直接把PDF、Word或网页里的纯文本抽出来,一行一行塞进向量库,标题、表格、列表、页码全部丢弃。这样的结果就是,系统只知道“有一段文字”,但不知道这段文字属于哪一章、哪一节、在讲什么主题。
正确做法是把文档先解析成带结构的中间格式,比如Markdown或HTML树,再用这个结构树来指导切块。解析时至少保留两类信息:第一是标题层级,第二是表格边界。标题层级能让你知道每个块在文档树里的位置,形成类似“首页>常见问题>退换货规则>第三条”这样的路径;表格边界则是很多知识库项目的重灾区,一张退货政策表里面同时包含时间、金额、条件,如果整表被切成两半,任何一侧的向量都无法表达完整语义。
实操上,我习惯在解析后先做一遍“文档体检”,把解析出来的标题树打印出来,确认章节没有丢失、乱码、错位。这一步花不了几分钟,但能避免后面所有环节基于错误结构做无用功。没有做过体检的项目,大概率会在某一天突然发现,库里几千个块其实都漏掉了文档的最后一章。
2.2 索引块层:没有上下文锚点的向量块只是浮游文本
什么是“浮游文本”?就是一个孤立的Chunk,里面只有几个句子,没有标题、来源、章节归属、前后文关系。比如一个块里写着“费用由乙方承担”,它可能来自采购合同也可能来自租房协议,可如果这个块自始至终没有携带任何上下文锚点,那检索的时候,模型只能靠这几个词去猜。
解决浮游文本问题的关键是给每个Chunk补齐元数据。我在搭建索引时,通常会让每个块携带这样一套字段:文档ID、标题路径、块类型(正文/标题/表格/代码/列表)、文档类型(规章制度、合同、技术手册、常见问题)、创建或生效时间、页码。这套字段本身就是“结构”的一部分,让每个向量块不再是一段浮游文字,而是文档树里一个有明确坐标的节点。
元数据更大的价值在于可以用它做结构化过滤。比如用户问“2024年修改后的年假政策是什么”,如果索引里记录了政策文件的生效年份,就能先把候选块精确过滤到2024年生效的文档版本,再让向量模型去计算语义相似度。这样一来,向量检索不用独自扛下整个搜索任务,系统整体的准确率会明显更稳。
2.3 查询改写层:用户问题与文档结构之间需要“对齐”
结构问题不只存在于文档侧,也存在于查询侧。用户提问时经常会省略大量上下文,比如“它多久能到?”如果系统直接拿这句话去向量库检索,得到的答案大概率是混乱的,因为查询本身缺少指代对象。这时候先做一个查询改写或指代消解,把问题补全成“智能门锁从下单到送达大约需要多久?”,召回效果会立刻不一样。
多跳问题更需要查询层面的结构调整。用户问“申请设备维修需要先完成哪两个审批流程?”这类问题如果直接被整体送去检索,向量库很难一次找到散落在不同页面、不同章节里的完整信息。合理的做法是把查询拆成两个子问题,比如“设备维修申请流程是什么”和“需要哪些前置审批”,分别去检索,再把结果合并后交给生成模型整合。这相当于先在查询层搭好一个结构,再把信息块按结构拼起来。
很多团队在检索结果差的时候,会反复调整文档侧,却忽略了查询侧也需要和索引结构对齐。一个简洁的规则是:用户今天是来问问题的,不是来提供结构化查询语句的,所以检索系统必须承担把口语问题翻译成结构化检索条件的责任。
3. Chunk怎么切:固定窗口、层级切分和父子索引的实验对比
3.1 为什么定长切块最容易“答非所问”
定长切块是很多RAG Demo的默认选择,比如把文档每隔500字或1000字切成一块,再加一段重叠窗口。它的优势是简单,不需要理解文档内容,但代价是物理边界会野蛮地切开句子的语义。
我真实遇到过的一个场景:某份规章制度里写着“工作时间上午9点到12点,下午1点到6点”,因为这句话正好跨过1000字切块点,前半个分句进了上一个块,后半个分句进了下一个块。用户问“几点下班合适”,检索到的块里只有“下午1点到”几个字,语义彻底残缺。加重叠窗口能缓解一点点,但本质上依然是“哪里断,取决于字数”,而不是“哪里断,取决于语义”。
定长切块还有一个隐藏问题:它会让每个块都带上相似的文档头内容。比如每章开头都有公司名称和制度编号,一堆快照块里充斥着同样的固定文本,会让向量的区分度下降,从而拖垮召回精度。如果你目前用的还是纯定长切分,而且检索效果不稳定,第一件事就是尝试切换成结构化切分。
3.2 结构化切分:按标题层级和段落边界“切对地方”
结构化切分的核心思路是“跟着文档自然结构走”,而不是跟着字数走。做起来不难:先用解析器把文档转成Markdown或HTML树,以各级标题为边界划分区域,然后把一个区域内的连续段落作为一根Chunk,或者把每个小标题下的段落作为一个Chunk。
我有一个常用的参考规则:一章如果包含5个小节,每小节2到3段,那就优先按小节切,而不是整个一章切成一块;切出来的块大小通常在200到800字之间,每一块都自带所属章节的标题路径。这样检索时,即使只命中了一块,模型也能通过标题路径知道它在回答哪个主题下的问题。
结构块的收益在表格内容上尤其明显。一张商品配送政策表,如果整体转成文本后当一个大块,向量检索命中它时往往很难定位到具体行;更好的做法是把表头拼接在每个表格行前面,比如“配送方式:工作日发货;配送时效:48小时内;发货地区:江浙沪”,每一行作为一个独立的检索单元。这样的检索粒度更细,同时信息又能保持自包含。
3.3 大块匹配、小块召回:父子索引模式的落地
无论怎么切块,都会遇到一个矛盾:小块语义更纯净但上下文不足,大块上下文更完整但噪声更多。父子索引模式正是为了解决这个矛盾而生。具体做法是准备两种块:子块用来做向量匹配,父块用来给生成模型提供上下文。检索时先用子块去和查询算相似度,命中后不直接返回子块文本,而是按照子块里记录的父块ID,把父块的完整内容取出来送给生成模型。
可以把它类比成图书馆查书:你通过目录页快速定位到“某本手册的第3章”,拿到线索后,再去书架把完整章节整本拿给阅读者看,而不是只把目录页那一页复印给别人。子块负责精准,父块负责完整,两者各司其职。
在实现时,子块和父块的对应关系可以存在元数据里:子块上记录parent_id,父块就是按二级标题或小节边界组织的一个较大段落集。父块也不能无限大,如果父块本身是一整章几十页的内容,送给生成模型的上下文会又长又散。参考经验是父块控制在2000到4000字以内,具体取决于模型上下文窗口和文档本身的章节粒度。
3.4 如何用数据决定Chunk大小,而不是拍脑袋
Chunk大小不是越统一越好,最好用数据说话。实操方法很简单:准备一个包含20到30条真实问题的评估集,把同一批文档分别按200字、400字、800字、1200字切块,跑一遍向量召回,记录每个问题正确答案是否出现在Top5里。大部分项目跑下来,会在某个区间看到明显的命中率平台期,再往上加字数,命中率不升反降。
我在处理一份600页的制度汇编时,做过一次这样的对比:定长1000字加100字重叠,Top5命中率只有78%左右;改成按层级切分加父子索引后,命中率提升到93%上下。这不是说1000字一定不行,而是提醒大家不要拿一个看似稳妥的参数直接上线。Chunk大小的合理区间和文档类型有关,规章制度以小节为边界最自然,产品说明书则以功能模块为边界最自然,后面这种依赖文档结构的判断,恰恰就是定长切分给不了你的东西。
4. 元数据、混合召回与重排:把结构变成可用的检索杠杆
4.1 元数据设计:给每个块一张“身份证”
前面提过元数据能让块有根,这里展开一下具体怎么做。我常用的元数据模板包含这样几个字段:doc_id、title_path、doc_type、effective_date、page_num、tags。其中title_path是最关键的,它完整记录了“章节路径”;doc_type和effective_date则经常被用来做时间过滤和文档类型过滤;tags可以人工维护,也可以自动提取。
举个例子,一个块可以长成这样:
{ "doc_id": "policy-2024-018", "title_path": "人事管理制度 > 请假管理 > 年假规则", "doc_type": "规章制度", "effective_date": "2024-01-01", "page_num": 12, "content": "职工累计工作满1年不满10年的,年休假5天;满10年不满20年的,年休假10天。" }这些字段的价值在于,它们让向量检索不再只是“语义搜索”,而是可以叠加一层精确的结构化筛选。传统关系型数据库有结构化查询,RAG里的元数据过滤就是你的结构化查询。用户问“去年生效的报销规则”,你先用effective_date把候选文档过滤到去年新规版本,再去做语义匹配,结果自然比纯靠向量近似靠谱。
这里有一条经验:元数据不是越全越好。字段太多,维护成本高,而且过滤条件设得过严时容易召回为空。好的元数据设计从查询出发,先记录你的用户最常按什么维度问问题,比如时间、部门、文档类型、产品型号等,再决定加哪些字段。其余信息宁可不放,也不要字段值大量为空。
4.2 混合检索:向量和关键词是互补的两套结构
只用向量召回的RAG,在一类问题上必然翻车:精准指标、编号、型号、人名。向量模型擅长语义相近的匹配,比如“如何申请电脑补贴”和“电脑补贴申请流程”可以互相召回,但它对“CS-2023-007号合同”这种字符串并不敏感,字面匹配的强信号会被语义池化过程稀释。
这时候需要引入关键词检索,经典的做法是用BM25这类稀疏检索算法。向量检索负责语义泛化,关键词检索负责精确字面匹配,两者各管一摊,再通过融合策略合并结果。我习惯先分别从两条路各召回Top20到Top50,再用RFF或加权求和做合并,而不是把两路分数简单相加——因为向量距离和BM25分数的数值尺度完全不一样,简单相加会被高分那头主导。
一个粗略的分工表如下:
| 查询场景 | 向量召回优势 | 关键词召回优势 |
|---|---|---|
| “如何申请办公设备” | 能匹配同义改写 | 按词面难命中 |
| “编号XJ-2019-088的合同” | 匹配不稳 | 精确命中 |
| “2024年新规下的报销上限” | 能理解“新规” | 无法区分新旧版本 |
| “这个零件故障代码E203” | 易混淆 | 精准定位 |
混合检索不是锦上添花,而是和文档结构并列的另一个基础能力。只做其中一路,相当于把查询结构强行压缩到一种表达方式里。
4.3 重排阶段的候选集结构,比重排模型本身更重要
很多人把重排序模型当作“救火队员”,希望它能把前面召回的坏结果重新救回来。但重排只能改变候选的排序,不能改变候选本身。如果召回阶段根本没有把正确答案放进Top100,重排再强也找不到那个答案。所以我会把重排看成一种“结构收束”的动作:先从多路召回得到一个足够宽的候选池,再用更强的交叉编码模型对候选池里的几十条结果逐条精排,最后取Top5送给生成模型。
实际操作里的一个建议是:先做轻量召回,比如向量Top50加关键词Top50合并去重;再用重排模型对合并后的结果重新打分,取前5到前10。候选池太窄会导致重排没有发挥空间,候选池太宽又会拖慢延迟,50到100条通常是一个平衡点。
一个容易忽略的细节是:重排输入最好带上查询改写后的完整意图,而不是原始口语短句。查询结构如果没做好,重排得到的Top5依然会偏离用户的真实目标。团队在重排阶段效果不理想时,先回头看看放在重排模型前面的候选集和人查询结构有没有对齐。
5. 换模型之前,先跑完这套RAG结构体检清单
5.1 搭一个20到30条的迷你评测集
很多人评估RAG效果时,喜欢上了线之后靠用户反馈来判断好坏,这在前期太被动了。我自己会先搭一个迷你评测集,规模不大,20到30条足够,关键是要从真实问答记录里抽样,而不是自己拍脑袋编问题。把这批问题分成几类:单点事实型、多跳推理型、排除型、比较型。下面这个表可以直接拿去做模板:
| 问题类型 | 例子 | 对结构的依赖程度 |
|---|---|---|
| 单点事实型 | “年假几天?” | 低,定长切分也可能命中 |
| 多跳推理型 | “申请设备维修前需要完成哪些审批?” | 高,依赖跨章节组合 |
| 排除型 | “哪些费用不能通过交通补贴报销?” | 高,依赖语义边界和元数据 |
| 比较型 | “预算内采购和预算外采购的流程差异?” | 高,依赖两段内容的可对比结构 |
每个问题最好都提前标注出标准答案在文档里的出处,比如“见制度汇编第3章第2节”,这样后续才能定位问题出在哪个环节。评测集不需要一开始就很完善,先有20条能覆盖常见问法的样本,就足以指导一轮结构优化了。
5.2 逐条回放检索链路,找“断点”
当某条问题检索效果差时,不要只盯着最终答案,要把整条链路拆开来看。我的做法是对每个测试问题记录四段信息:改写后的查询、召回Top5、重排Top5、生成结果。然后按下面这张表判断断点位置:
| 现象 | 判断方向 |
|---|---|
| 召回Top5里没有正确答案 | 问题出在解析结构或切块策略,正确答案所在的块没有被有效表达 |
| 召回Top5里有答案,但重排后掉出Top5 | 问题出在候选排序,通常要调整重排模型或融合权重 |
| 召回Top5有答案,重排Top5也有答案,但生成结果错误 | 问题出在提示词、上下文裁剪或生成模型本身 |
这个回放过程比较费人工,但每次调整前至少抽5条效果差的问题跑一遍,能快速定位结构瓶颈。很多人一开始就怀疑是生成模型乱发散,实际回放之后才发现是召回就没有带出关键信息,白怪了生成模型。
5.3 结构优化和参数调整的先后顺序
如果你已经确认“结构有问题”,但不确定先动哪里,我建议按这个顺序来做:先修文档解析,确认标题树和表格边界没丢;再改结构化切分,优先尝试层级切分或父子索引;然后补元数据过滤,缩小候选范围;接着加混合召回;最后才考虑升级向量模型和重排模型。
这条顺序的核心是“先让信息可被找到,再让找到了足够准确”。每一轮只改一个变量,重跑一次迷你评测集,记录前后指标,而不是一口气把解析、切分、召回、重排全换一遍。曾和我合作的团队里,有人同时改了切分策略和重排模型,整体准确率确实上去了,但没法确定到底是哪个改动带来的收益。最后又得调回去逐步验证,浪费了大半天时间,而这两件事本来可以一次只改一个。
这套体检流程本身成本不高,但能稳定避免“盲调”现象。一个能复现的评测流程,比任何昂贵的模型升级都有价值。
6. 表格、代码块和图谱:所谓“更结构”的边界在哪里
6.1 别让“加元数据”变成另一种形式主义
元数据是结构优化里最简单最见效的一步,但也很容易被做形式化。有的项目给每个块加了十几个字段,看起来分析得很专业,实际查询时却用不上,因为用户的问题根本没有那么多过滤维度。过多的字段还会拖慢索引写入,甚至某些没填充的字段在过滤时产生空结果。
我的建议是先把最常用的过滤维度做扎实:文档类型、生效时间、所属产品线或部门、章节路径。这四类字段能覆盖大多数企业知识库的高频查询场景。至于其他的标签,可以在评测集里看到明确需求后再补,不要一开始就搭一个庞大但空洞的元数据体系。如果发现某个过滤条件经常把答案误删掉,先查元数据值有没有错误,再决定字段要不要保留。
6.2 表格、代码块和列表要单独建一套索引
非规则内容是结构优化里最容易被忽视的区域。一个普通Chunk切分器会把一个表格当成连续文本直接切断,也会把一个代码文件按行数切块,但这两类内容其实都需要单独设计索引结构。
表格转成Markdown之后,如果整体作为一个大块,检索命中后模型很难快速定位到正确行列。更稳的做法是“表头加行”的索引方式:每一行前面拼接表头和上下文,比如“费用类型:交通费;上限标准:每月800元;需要凭证:发票”。代码块则建议按函数或类为最小单元切分,保留函数签名和注释,让每个代码块对应一个可独立运行或独立解释的逻辑单元。列表内容要保留缩进层级和项目符号,因为列表的层级本身就是重要的语义结构。
这些非规则内容如果处理得当,检索质量的提升往往比换模型还明显。原因很简单:这类内容在向量空间里的语义表达本就不稳定,结构化的索引方式等于帮模型提前把难点拆掉了。
6.3 什么时候才需要升级到知识图谱结构
知识图谱是RAG结构优化的高级选项,但绝不应该是默认选项。一个企业报销制度、产品技术文档这类内容,用父子索引加元数据过滤通常已经够用,研发成本和维护成本都比较低。只有当文档里实体关系非常密集,用户经常问“某个流程涉及哪些角色和哪些前置条件”这类多跳关系问题时,图结构才值得考虑。
上知识图谱意味着要做实体识别、关系抽取和图查询,后面还得保证图谱和原始文档的一致性,成本不低。我的经验判断标准很简单:先用向量加元数据加父子索引这套“轻结构”把基础撑住,如果测试集里的多跳问题仍然频繁失败,同时文档关系确实相对稳定、人工整理实体关系成本可控,再在图谱上投入资源。否则,投入产出比会很差。
我自己在调RAG时养成的一个习惯是,遇到效果倒退,不带测试问题直接换方案,而是先把召回链路的结果一层层打印出来,看是“根本没有相关片段”还是“有相关片段但没被排上来”。这个动作能立刻决定要不要动结构,多花一天时间把结构理顺,往往比盲目升级模型更解决问题。所谓结构优化,说到底就是让文档以它最自然的面貌进入检索系统,再让用户的问题以最匹配的方式去寻找它。