1. 为什么单层向量库开始不够用了
做过RAG(检索增强生成)的朋友大概都有过这种体验:知识库刚上线时效果惊艳,问什么都能答上来,demo演示一遍过。但用着用着就发现不对劲了——用户问一个稍微需要归纳的问题,比如“我们产品的退款政策分几种情况”,系统检索回来一堆零散片段,每段都沾点边,但拼起来就是不成体系。模型拿着这些碎片去生成,要么漏掉关键分支,要么把不同场景的规则混在一起,答得似是而非。
这个问题的根源不在于Embedding模型不够强,也不在于向量库选得不对,而在于单层向量库的检索粒度天然是“碎片化”的。它擅长的是“找到和问题最相似的文本块”,但它不理解“这些文本块之间是什么关系”。你问一个需要跨片段归纳的问题,它只能给你一堆平行切片,剩下的归纳工作全压在生成模型身上。生成模型上下文窗口有限,注意力也有限,碎片一多就开始丢信息。
我最初做知识库问答时也是单层向量库一把梭,后来被现实反复教育,才慢慢摸索出“双层RAG”这套结构。所谓双层,第一层是结构化知识条目,第二层是原始切片。两者各司其职,配合起来用,效果比单纯堆向量库好一大截。这篇文章就把我这套方案的完整思路、实现细节、踩过的坑全部摊开讲清楚,适合正在做RAG应用、被检索质量困扰的开发者参考。
2. 双层RAG的整体设计思路拆解
2.1 单层向量库的三个硬伤
先说说为什么单层不够。我把实际项目中遇到的问题归成三类。
第一类是归纳类问题失效。用户问“XX功能有哪些限制”,正确答案可能分散在五六个文档段落里,每段讲一个限制。单层检索按相似度召回,可能只召回其中三段,另外两段因为措辞和问题不够相似被漏掉。生成模型拿到三段就答三条,用户以为只有三条,实际上有六条。这种漏召回在单层结构里几乎无法根治,因为你没法保证“所有相关片段”的相似度都排在前K。
第二类是关系丢失。知识库里很多信息是有层级和关联的,比如“A方案包含B、C两个子步骤,B步骤依赖D前提”。单层切片把这些关系切断了,检索回来的片段是孤立的,模型看不到“B依赖D”这层关系,生成的答案就可能给出一个缺少前提的操作步骤。
第三类是噪声干扰。为了不漏召回,很多人把TopK调大,结果召回一堆弱相关片段。这些片段本身没错,但和问题关系不大,反而稀释了真正有用的信息,模型容易被带偏。TopK调小又漏召回,调大又引入噪声,这个矛盾在单层结构里很难平衡。
2.2 双层结构的分工逻辑
双层RAG的核心思路是:让结构化的归结构化,让原始细节的归原始细节。
第一层“结构化知识条目”是对原始知识的提炼和归纳。它把散落在多个切片里的信息,提前整理成一条条独立、完整、自包含的知识条目。比如“退款政策”这个主题,我会整理成一条结构化条目,里面列清楚所有退款情形、每种情形的条件、所需材料、处理时效。这条条目本身就是一个完整的答案骨架。
第二层“原始切片”保留原文的细节和上下文。当结构化条目命中后,我再根据条目里关联的原文位置,去第二层召回对应的原始切片,补充具体措辞、示例、边界情况。
打个比方,结构化条目像是书的目录和章节摘要,原始切片像是正文。用户问一个宏观问题,先查目录定位到章节,再翻正文看细节。单层向量库相当于只有正文没有目录,用户问宏观问题,你只能从正文里随机翻几页给他看。
2.3 两层之间怎么关联
两层不是孤立的,中间需要建立映射关系。我的做法是:每条结构化知识条目都带一个source_refs字段,记录这条条目是从哪些原始切片归纳出来的,存的是切片ID列表。检索时先命中结构化条目,再通过source_refs去第二层拉取对应的原始切片。
这个映射关系在构建阶段就要建立好。我通常的做法是:先对原始文档做切片,得到切片库;然后人工或半自动地基于切片库归纳结构化条目,归纳时记录用到了哪些切片。半自动的方式是用大模型辅助归纳,让模型输出条目内容的同时输出引用的切片ID,人工再校验一遍。
注意:结构化条目不要做得太细,否则就退化成另一种切片了。一条条目应该对应一个相对完整的知识单元,能独立回答一类问题。我一般控制一条条目覆盖3到10个原始切片的信息量。
3. 结构化知识条目的构建实操
3.1 条目该长什么样
结构化条目不是随便写一段话,它得有固定的结构,方便检索和后续处理。我用的字段结构大概是这样:
{ "id": "kb_refund_001", "title": "退款政策总览", "category": "售后服务", "summary": "涵盖全部退款情形、条件、材料与时效", "content": "本产品支持三种退款情形:...", "keywords": ["退款", "退货", "售后", "时效"], "source_refs": ["chunk_1024", "chunk_1025", "chunk_1031"], "updated_at": "2025-01-15" }title和summary是给检索用的,content是给生成用的,source_refs是连接第二层的桥梁,keywords辅助关键词检索。category用于分类过滤,当知识库很大时可以先按类目缩小范围。
这里有个经验:summary要写得像“这个条目能回答什么问题”,而不是“这个条目讲了什么”。前者更贴近用户的提问方式,检索命中率更高。比如写“涵盖全部退款情形、条件、材料与时效”,比写“退款政策说明”要好,因为用户提问往往是“退款需要什么条件”“退款要多久”,前者包含了这些提问词。
3.2 归纳条目的操作流程
我的归纳流程分四步。
第一步是聚类。把原始切片按主题聚成若干簇。小知识库可以人工看,大知识库用Embedding聚类加人工校验。聚类的目的是找出“哪些切片讲的是同一件事”,这些切片就是一条结构化条目的素材。
第二步是归纳。对每一簇切片,让大模型生成一条结构化条目。Prompt里明确要求:输出完整的知识单元,覆盖簇内所有切片的关键信息,不要遗漏分支情况,同时输出引用的切片ID。模型输出后人工过一遍,补漏纠错。
第三步是去重合并。不同簇可能归纳出内容重叠的条目,需要合并。合并时保留信息更全的那条,把另一条的source_refs并进来。
第四步是质量校验。逐条检查:这条条目能不能独立回答一类问题?有没有遗漏重要分支?source_refs指向的切片是否真的支撑这条内容?校验不通过的打回重做。
这套流程听起来繁琐,但知识库构建是一次性投入,后续维护成本很低。我做过一个两千多切片的知识库,归纳出约一百五十条结构化条目,整个流程走下来大概两天,之后检索质量提升非常明显。
3.3 条目粒度的把控
粒度是结构化条目最容易做错的地方。做太粗,一条条目塞太多内容,检索命中后生成模型要处理的信息量太大,反而容易丢细节;做太细,条目退化成切片,失去归纳价值。
我的判断标准是:一条条目对应一类问题。如果两类问题的答案有明显不同的结构,就拆成两条;如果两类问题只是措辞不同、答案结构一致,就合并成一条。
举个例子。“退款条件”和“退款时效”是两个不同的问题类型,前者答案是条件列表,后者答案是时间范围,应该拆成两条。“个人退款条件”和“企业退款条件”如果条件结构一致只是主体不同,可以合并成一条,在content里分主体说明。
粒度把控没有绝对标准,我的经验是:一条条目的content控制在200到500字之间比较合适。低于200字可能信息量不够独立成条,高于500字可能该拆了。
4. 原始切片的处理与两层协同检索
4.1 切片策略的调整
有了结构化条目兜底,原始切片的策略可以更激进一些。单层结构时,切片要尽量大,生怕切碎了丢上下文;双层结构下,切片可以切得细一点,因为宏观信息已经被结构化条目覆盖了,切片只需要保留细节。
我一般用语义切片而不是固定长度切片。按段落、按标题层级、按语义完整性来切,每片控制在300到800字。切的时候保留一定的重叠,避免边界信息丢失。重叠量我设的是切片长度的15%左右,实测下来既能保住边界信息,又不会引入太多冗余。
每个切片除了文本内容,还要存元数据:所属文档、章节路径、前后切片ID。前后切片ID在检索时有用,命中一个切片后可以顺带拉取它的前后邻居,补充上下文。
4.2 两层检索的触发逻辑
检索时的流程是这样的:
- 用户提问,先在第一层结构化条目库里检索,召回Top3到Top5条目。
- 判断召回条目的相关度。如果最高分低于阈值,说明这个问题结构化条目覆盖不到,直接走单层切片检索兜底。
- 如果命中结构化条目,取条目的
source_refs,去第二层拉取对应的原始切片。 - 把结构化条目内容和原始切片一起塞进生成模型的上下文。
这里的关键是阈值判断。不是所有问题都能被结构化条目覆盖,有些细枝末节的问题只有原始切片里有。如果强行用结构化条目回答,可能答非所问。所以需要一个兜底路径:结构化条目相关度不够时,退回单层切片检索。
阈值怎么定?我用的是余弦相似度,阈值设在0.75左右。这个值需要根据你的Embedding模型和实际数据调。调的方法是:拿一批测试问题跑一遍,看结构化条目命中的准确率,阈值调高准确率上升但覆盖率下降,调低反之,找一个平衡点。
4.3 上下文组装顺序
组装上下文时,顺序很重要。我的做法是:结构化条目在前,原始切片在后。
原因是生成模型对上下文开头和结尾的信息注意力更强,中间容易忽略。结构化条目是答案骨架,放前面让模型先建立整体框架;原始切片是细节补充,放后面让模型按需取用。如果反过来,模型先看一堆碎片,可能还没看到骨架就已经开始生成答案了。
结构化条目和原始切片之间加一个分隔标记,比如--- 以下是原始文档细节 ---,让模型清楚哪部分是归纳、哪部分是原文。这个标记看起来不起眼,但实测对生成质量有影响,模型能更好地区分“该归纳的内容”和“该引用的内容”。
5. Embedding模型选型与检索参数调优
5.1 两层用不同的Embedding模型
双层RAG的一个优势是:两层可以用不同的Embedding模型,各取所长。
结构化条目层,我倾向于用语义理解强、对短文本敏感的模型。因为条目本身是归纳过的短文本,title和summary都很精炼,需要模型能准确捕捉语义。这类模型通常在中英文语义相似度任务上表现好,对同义改写、意图识别比较准。
原始切片层,我倾向于用长文本表征好、细节保留强的模型。切片是原文,长度较长,需要模型能处理长文本且不丢失细节信息。有些模型对长文本会做截断或池化,导致细节丢失,这类就不适合切片层。
具体选哪个模型,我不在这里给具体名字,因为模型迭代太快,给了也很快过时。选型方法是:拿你自己的测试集,把候选模型都跑一遍,看检索命中率和最终生成质量。测试集要包含归纳类问题、细节类问题、边界问题,覆盖真实使用场景。
5.2 检索参数的计算与选择
几个关键参数需要调:
TopK。结构化条目层TopK我设3到5,因为条目总数不多,TopK太大引入无关条目。切片层TopK我设5到10,因为切片数量大,需要多召回一些保证覆盖。但切片层TopK不是越大越好,超过10之后噪声明显增加,生成质量反而下降。
相似度阈值。结构化条目层阈值0.75,切片层阈值0.6。切片层阈值低一些,因为切片是原文,措辞和问题可能差异较大,阈值太高会漏召回。
重排序。如果检索质量还不够,可以加一个重排序模型。先召回Top20,再用重排序模型精排取Top5。重排序模型比Embedding模型慢,但准。我一般在切片层加重排序,结构化条目层不加,因为条目层召回量本来就小。
提示:参数没有万能值,一定要用你自己的数据调。我见过有人直接抄别人的TopK=10,结果他的知识库切片粒度不一样,效果差很多。调参的投入是值得的,检索质量提升一点,生成质量提升一大截。
5.3 混合检索的补充
纯向量检索有个短板:对精确匹配不敏感。用户问一个专有名词、一个编号、一个特定型号,向量检索可能召回一堆语义相似但实体不对的片段。这时候需要关键词检索来补充。
我的做法是向量检索和关键词检索并行,各召回一批,然后融合。融合用RRF(倒数排名融合)比较简单,不需要调权重。两路各取Top10,融合后取Top5。实测下来,混合检索比纯向量检索在实体类问题上准确率高不少。
结构化条目层也可以用混合检索,但条目层的关键词字段是人工整理的,质量比切片层的关键词抽取高,所以关键词检索在条目层效果更好。
6. 常见问题与排查技巧实录
6.1 结构化条目命中率低怎么办
这是最常见的问题。用户提问,结构化条目层没召回相关条目,走了兜底路径,效果退回单层水平。
排查思路分三步。第一步看条目的title和summary是否贴近用户提问方式。很多条目写得太“官方”,用户提问太“口语”,两者语义空间对不上。解决办法是把summary改写成用户可能问的问题形式,或者给条目加同义问法。
第二步看Embedding模型是否适合短文本。有些模型对长文本好,对短文本反而弱。换一个短文本强的模型试试。
第三步看条目数量是否太少。条目太少,覆盖面不够,很多问题自然命中不了。这时候需要补充条目,把兜底路径里高频出现的问题归纳成新条目。
6.2 生成答案还是漏信息
命中了结构化条目,但生成答案还是漏了分支。可能原因有两个。
一是结构化条目本身就不全。归纳时漏了某些切片的信息。回去检查条目的source_refs,看引用的切片是否覆盖了所有相关切片。如果漏了,补充进去。
二是原始切片没拉全。source_refs里的切片ID可能因为切片库更新而失效,或者拉取时出了错。检查切片库和条目库的同步机制,确保source_refs指向的切片都存在。
三是上下文太长,模型注意力不够。结构化条目加原始切片可能超过模型的舒适上下文长度。解决办法是精简原始切片,只拉和问题最相关的几片,而不是把source_refs里的全拉出来。
6.3 两层更新不同步
知识库更新时,原始切片变了,但结构化条目没跟着更新,导致条目里的信息和切片对不上。这是双层结构特有的问题。
我的解决办法是建立更新联动机制。切片库更新时,记录哪些切片变了。然后检查这些切片被哪些结构化条目引用,把这些条目标记为“待复核”。人工复核后决定是更新条目内容还是只更新source_refs。
这个机制需要一点工程投入,但值得。我见过有人没做联动,切片更新了半年,结构化条目还是老的,检索出来的答案和原文对不上,用户投诉才发现。
6.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决动作 |
|---|---|---|---|
| 归纳类问题答不全 | 结构化条目未命中 | 看条目层召回分数 | 优化summary措辞或补条目 |
| 答案与原文不符 | 条目与切片不同步 | 比对条目content与source_refs | 建立更新联动机制 |
| 实体类问题答错 | 纯向量检索实体不敏感 | 看召回片段实体是否匹配 | 加关键词检索混合 |
| 生成答案冗长 | 上下文噪声多 | 看召回片段相关度分布 | 降TopK或加重排序 |
| 边界问题答偏 | 切片层召回不足 | 看切片层召回分数 | 降阈值或增TopK |
6.5 几个实操心得
第一个心得:结构化条目要定期复盘。用户提问分布会变,半年前归纳的条目可能覆盖不了现在的高频问题。我一般每个月看一次兜底路径的日志,把高频兜底问题归纳成新条目。
第二个心得:不要追求100%结构化覆盖。有些长尾问题就是没法归纳,强行归纳反而让条目变得臃肿。留20%到30%的问题走兜底路径是正常的,兜底路径的效果也不差,只是比结构化路径稍弱。
第三个心得:条目content里保留原文关键措辞。归纳时容易把原文的具体措辞改成自己的话,但有些措辞是用户会搜的关键词,改了就搜不到了。我的做法是归纳时保留关键实体和术语的原词,只改连接和表述。
第四个心得:两层检索的日志要分开记。哪层命中、命中分数多少、走了哪条路径,这些日志对调优至关重要。我一开始没分开记,出了问题根本不知道是哪层的锅,后来分开记之后排查效率高很多。
7. 什么场景适合上双层RAG
双层RAG不是银弹,它有适用场景。知识库规模小、问题类型单一的场景,单层向量库就够了,上双层是过度设计。知识库规模大、问题类型多、有大量归纳类问题的场景,双层RAG的优势才明显。
具体判断标准:如果你的用户提问里,有超过30%是“有哪些”“分几种”“整体上”这类需要归纳的问题,单层结构大概率撑不住,值得上双层。如果用户提问基本都是“XX是什么”“XX怎么做”这类点状问题,单层结构够用。
另外,双层RAG的构建成本主要在结构化条目的归纳上。如果知识库更新频繁,条目维护成本会比较高。更新不频繁的知识库更适合上双层。
我自己在实际项目里的体会是:双层RAG的投入产出比在知识库规模超过五百个切片之后开始显现。低于这个规模,单层调调参数也能凑合;超过这个规模,单层的漏召回和噪声问题会越来越明显,这时候双层的价值就体现出来了。最后再分享一个小技巧:结构化条目的ID命名带上类目前缀,比如kb_refund_001,这样在日志里一眼就能看出命中的是哪类条目,排查问题时省很多事。