尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

双层RAG实践:用结构化条目+原始切片解决向量库召回不准

双层RAG实践:用结构化条目+原始切片解决向量库召回不准 做 RAG 项目做得越久我就越怀疑一件事把一堆文档切碎了扔进向量库真的能让模型“会”吗一次生产环境的翻车让我彻底改了这个想法。用户问的是某版本制度里“连续旷工三天”的认定口径向量库召回的全是讲考勤制度其他条款的片段模型一本正经地给出了一个揉在一起的答案连出处都对不上。后来我把检索链路改成“先查结构化知识条目再回溯原始切片”的双层结构同样的问题才真正稳住。这就是我要聊的双层 RAG——它不是新模型也不是新框架而是一套更符合人类查资料习惯的检索组织方式。如果你也在做知识库问答、合同审查、规章问答这类需要“给结论还要能给证据”的场景这篇文章应该能帮你少走不少弯路。1. 为什么单层向量库在真实业务里总是差口气1.1 向量召回的两个致命盲区先说清楚我所说的单层向量库指什么文档切块、embedding、建索引、query 检索、TopK 拼上下文一步到位。这套流程在小规模、问题单一的场景里确实能用但一放到真实业务里问题就出来了。第一个盲区是“相似≠正确”。向量检索是拿语义相似度排序的它理解的是“像不像”不是“对不对”。比如你问的是“离职赔偿金怎么算”它可能召回一段讲“经济补偿金”的话词面相近、语义也沾边但口径完全不同。模型拿到这些相似片段后很容易把两个相近但本质上不同的知识混在一起生成一个听起来有逻辑、实际是拼凑出来的答案。这种“流畅的错误”比直接答错更可怕因为用户很难察觉。第二个盲区是切块切碎了知识。固定长度切片会把一个完整知识点拦腰截断或者把两个相邻但不同的知识点混进同一块。模型拿到上下文的时候实际上拿到的是残片。举个例子一份合同里“违约责任”和“免责条款”往往挨在一起固定切片很可能把两者切进同一个 chunk。你问违约责任它把免责条款也带回来了模型不知道边界在哪里只能自己猜。而且单层向量库缺少“条目”这一层所有检索压力都压在一个向量索引上一旦索引数据量上来召回质量就会肉眼可见地波动。1.2 结构化条目 原始切片到底解决了什么问题双层结构的设计动机很简单先告诉模型“知识在哪一本、哪一章、哪个知识点”再告诉它“这一页的原话是什么”。第一层用结构化知识条目做索引解决“答案大概在哪个知识点”第二层用原始切片做证据解决“能不能引用原文”。这里有个很关键的认知条目是入口切片是凭证。结构化条目可以理解成图书馆的检索卡片或者官网的目录条目本身可以很简短但必须能精确指向原始位置。原始切片则保留文档原貌负责提供可核查的证据。两者合在一起既规避了向量检索只靠相似度导致的定位漂移又不至于因为结构化抽取而丢失原文细节。我打个比方你就明白了。单层向量库的做法相当于你在图书馆里直接搜“相关段落”搜出来一摞复印件但复印件上没写书名页码你也不知道它来自哪本书、讲的是哪个主题。双层 RAG 的做法是先查馆藏目录确定这本书在哪个书架的哪一层再伸手把那本书翻到具体页码看到原文之后才回答你。前者快但容易拿错书后者多了一步但每一步都可验证。1.3 单层不是不行是场景不合适我不是说向量库没用——向量库在双层结构里仍然是核心组件。我的意思是在知识库问答这类任务里单层结构把“定位”和“证明”两件事全押在向量相似度上任何一个环节出问题都会被放大。单层也不是没有优点部署快代码量少几十行就能跑通。对于一些内部工具原型它完全够用。可一旦进入生产环境你面对的是多版本文档、交叉引用、用户千奇百怪的提问方式单层的“脆”就暴露出来了。双层结构本质上把任务拆成了“定位”和“证明”两个阶段每个阶段都用它最擅长的工具代价是多一层索引管理收益是可解释性和准确率的大幅提升。2. 双层结构的核心先把知识变成“条目”2.1 结构化知识条目怎么建条目不是随便写一句话它是从原始文档里抽出来的、带有稳定标识的知识单元。常见形态有三种。第一种是实体卡片。比如“在职证明开具流程”维护负责人、所需材料、办理时限这些字段。第二种是事实三元组适合关系密集的资料比如“条款A - 适用条件 - 连续旷工三天”。第三种是摘要型条目把一个章节压缩成三到五句话保留主题和结论。实际操作中我不会只用一种通常是先做文档分块和标题层级解析再用提示词让大模型抽取条目字段最后由人工校对重要的条目。抽取时最关键的一点是每条必须带 source_id也就是能回溯到原始文档、章节、块 ID 的标识否则第二层就断掉了。以员工手册为例我会把“考勤管理”章节抽成一张卡片字段内容条目名称考勤管理制度-旷工认定适用范围全体员工核心规则连续旷工三天及以上视为严重违纪关联条款第8.2条、第9.1条原始块IDdoc_123_chunk_045查询“迟到三次会怎么样”时第一层命中这张卡片比直接去向量库捞原始片段准确得多。条目创建完之后我还会定期抽检防止大模型在摘要里加入原文没有的推断。2.2 保留原始切片的三个理由有人会问既然有了结构化条目为什么还要原始切片答案很简单模型要的是证据不是转述。第一可溯源。合规审查、法务问答这类场景答案必须能定位到原文段落结构化条目再干净也只是二手信息。第二可展示。面向用户的系统不可能只给一句话需要把命中的原文片段展示出来让用户自己判断。第三避免抽取信息损耗。知识抽取必然有压缩和误差原始切片放在那里即使条目抽得不够好第二层还能兜底。切片策略上我倾向于按语义段落切而不是按固定 token 数硬切。如果文档有标题层级先把层级解析出来再在层级内切块。元数据至少要带文档 ID、章节路径、页码、块 ID。overlap 可以给 50~100 token不要太多否则重复内容会污染检索结果。这里有一个很多教程不会提的细节切片不是越小越好。太小的切片虽然更容易命中但也更容易丢失上下文模型拿到断章取义的半句话反而会猜错。我一般控制在一块 500~1000 字左右具体看文档类型。法律条文类可以适当长一点因为条款之间的关联性强产品说明类可以短一点因为每个段落相对独立。2.3 知识条目与原始切片之间的关联关系这一步很多人忽略。条目表里存 entry_id、entry_content、source_id切片表里存 chunk_id、content、metadata关联关系就是一个 source_id 指向一个或一组 chunk_id。也可以反过来一个条目指向多个切片只要映射表足够干净。工程上建议用两张表别把条目塞进切片里面否则检索时又要做文本解析很难维护。用一个简单映射表存 entry_id 和 chunk_id 的多对多关系就够了。版本管理也是个坑。文档改了版切片重新生成条目如果不跟着更新source_id 就断了。我的做法是给整批数据加一个 version 字段条目和切片同属于一个版本检索时先按版本过滤再走双层链路。这样即使历史文档还在也不会干扰当前线上问答。3. 双层检索链路怎么搭3.1 第一层先定位知识条目第一层检索的输入是用户 query目标是候选的 entry。这层我不推荐只依赖向量检索最好做成混合检索关键词 BM25 加向量召回甚至加点规则匹配。原因很简单用户提问往往带着业务术语或简称比如“三天”“旷工”向量可能匹配不到BM25 反而能命中。第一层的目标是“宁缺毋滥”把候选条目压到 5~10 条。也可以把这一层做成分类器先判断 query 属于哪个知识主题再去主题下找条目。实测下来条目数量级比切片小得多检索准确率会明显高一个台阶。我常用的第一层流程是先用 BM25 召回一批条目再用向量召回一批条目然后取并集按一个简单融合分数排序。融合分数不搞太复杂因为候选量小直接用排名倒数的加权和就行。如果条目数量少比如几百条甚至可以直接用关键词命中排序省掉向量召回都行。很多时候第一层的瓶颈不在算法而在条目质量。如果条目本身命名混乱比如一个叫“迟到处理”另一个叫“考勤违规处罚”很多系统会自动把它们当成不同主题导致用户怎么问都只命中一个。所以我在建立条目时会给每个条目加别名和同义词比如条目“旷工认定”的别名包括“连续缺勤”“无故不到岗”。这一步不复杂但对召回率影响非常大。3.2 第二层从条目回溯原始切片拿到候选条目后顺着 source_id 取回对应的原始切片。这一步看似简单但有几个细节值得注意。第一取回的切片数量不用多上下文窗口有限一般 3~6 块就够。第二取回之后要做一次重排或过滤比如检查 query 里的关键词是否真的出现在切片里避免模型把不相关的片段一起编进答案。第三把条目的摘要和原始切片一起拼进 prompt让模型先看摘要、再看原文。我常用的 prompt 结构是先给系统说明再给命中的条目列表再给原始切片列表最后是用户问题并明确要求模型优先引用原文里的原话给不出处时直接说不知道。实际效果上我不建议把命中的条目内容当成可直接引用的答案因为条目是抽出来的摘要不是原文。正确的做法是条目负责指路切片负责说话。第二层的重排我先用规则重排关键词覆盖率高的切片往前排然后判断切片是否包含条目中提到的核心实体。如果你的系统有足够的标注数据再上交叉编码器重排。但大多数场景下规则重排已经能滤掉大部分噪音。3.3 工程实现骨架下面给一个极简的伪代码把整个主流程串起来方便你照着搭。# 伪代码双层 RAG 检索主流程 def double_layer_retrieve(query): # 第一层条目检索混合召回 排序 entry_candidates hybrid_search_entries(query, top_k5) # 第二层根据条目回溯切片注意去重 chunk_ids get_chunk_ids_by_entries(entry_candidates) chunks fetch_chunks(chunk_ids, top_k6) # 组装上下文先条目摘要后原始切片 context { entries: [e.summary for e in entry_candidates], chunks: [c.content for c in chunks], } prompt build_prompt(context, query) return prompt这里我故意省略了向量库的具体选型因为无论是 Milvus、FAISS、pgvector 还是 Qdrant只要支持按 ID 取向量或按元数据过滤都能用。双层结构的重点不在向量库本身而在数据组织和检索流程。国内团队如果用 Python 技术栈可以基于 LlamaIndex 或 LangChain 快速搭但我不建议直接套框架里的默认 Retriever因为那些组件大多针对单层结构设计需要自己写一个组合检索器。如果你已经有一套文档处理管道双层 RAG 完全可以基于现有管道改新增一个条目表再在 RetrievalQA 里加一次映射查询。4. 实操中的评估与调优4.1 怎么判断双层 RAG 真的更“好用”我建议不要凭感觉而是搞一个 30~50 条真实问题的测试集。每条问题都要标注标准答案、答案来源文档。评估维度至少三个评估维度说明建议指标检索召回率最终命中的切片是否覆盖标准答案出处命中率80% 算及格答案正确性模型输出与标准答案是否一致人工打分或 LLM-as-Judge引用准确率模型引用的原文是否真实存在于切片引用可追溯率100% 为目标我自己的经验是双层 RAG 在召回率和引用准确率上提升非常明显因为第一层把候选范围缩窄了第二层又强制回看原文。单层结构的输出经常看着流畅但引用经常是编的这是最危险的地方。跑测试集的时候别只看平均分还要按问题类型分组看。比如“简单事实型”“多条件判断型”“跨章节综合型”分开统计。双层 RAG 的优势主要体现在后两类如果你的测试集全是简单问题那单层也够用看不出差别。4.2 调优的关键旋钮我整理了一个调优对照表基本覆盖了我在生产环境里动过的所有参数旋钮可以怎么调经验和坑条目粒度太粗则多个主题挤在一个条目里太细则条目数量爆炸按“一个业务问题对应一个条目”的粒度最稳第一层 top_k调大能提高召回但会带入噪音一般 3~8 条宁可少而准切片长度太短失去上下文太长浪费 token按语义段落单块 500~1000 字比较平衡第二层切片数量与上下文限制相关3~6 块超出后模型容易抓不住重点重排方式关键词覆盖、交叉编码器规则重排便宜有效先用规则再上模型条目粒度是我踩坑最多的点。最开始我把整章内容都抽成一个条目结果很多问题命中同一个条目第二层取回的切片五花八门模型不知道该信哪一块。后来我把“考勤管理”拆成“请假规则”“迟到处理”“旷工认定”“加班调休”四个条目才真正把定位做准。切片数量也很关键。之前我为了追求召回第二层一次取 10 块切片结果模型上下文里塞满了无关信息回答反而变差。把数量降到 5 块之后准确率和引用率都上去了。记住一个原则双层 RAG 的第一层负责广撒网第二层必须收敛。5. 使用边界与落地经验5.1 什么时候该用双层什么时候单层就够双层不是银弹。当作息时间表、产品价格、简单 FAQ 这种“一问一答”场景单层向量库完全够用额外建条目反而是负担。但如果你面对的是合同、制度、规程、技术文档这类内容知识密集、版本多、需要引用具体条款双层几乎是必须。判断标准也很简单你的用户会不会问“这个结论出自哪里”如果会就别单层硬扛。还有一些场景我会建议直接上双层企业知识库、客服工单辅助、内部规章问答、法规政策解读、设备手册故障排查等等。这些场景的共同点是答案的正确性比答案的生成速度重要得多。另外要考虑维护成本。双层结构意味着多一张条目表、多一套抽取管道、多一批需要校对的数据。如果你的知识库只有几十篇文档团队也没有人力维护条目那老老实实用单层别为了炫技增加负担。如果文档上千篇且更新频繁双层带来的准确率收益会远超维护成本。5.2 落地中容易翻车的几个细节做生产项目这么久以下几个坑我基本都踩过写出来帮你避雷。第一个坑条目和原文不一致。模型抽取摘要时很容易把“可能”“一般”这种限定词丢掉导致条目比原文更绝对。比如原文说“连续旷工三天以上的可视情况解除合同”模型可能抽成“连续旷工三天即解除合同”。解决方式是人工抽检重点条目并且在 prompt 里强制要求抽取时保留原文限定词。第二个坑source_id 断裂。文档重新上传、重新切块后条目表的 source_id 还是旧块 ID导致第二层取回空内容。我建议把条目和切片放进同一个文档版本用版本号管理每次更新文档时一起重新抽取条目。第三个坑混合检索分数归一化问题。BM25 和向量检索的分数不在一个量纲直接用加权和容易失衡。我试过很多种方式最省事的是先各自召回再按排名倒序合并去重排名权重可以简单给 BM25 0.5、向量 0.5后续再根据测试集调。第四个坑模型把稀疏条目当答案。如果你只在 prompt 里放了条目摘要模型可能直接拿摘要里的内容作答但这些摘要是压缩过的可能丢失关键限定。我强制要求 prompt 里写明“条目仅用于导航不能作为回答依据回答必须引用原始切片原文”。5.3 我个人落地体会做这个项目时我最大的感受是双层 RAG 真正的难点不是检索代码而是知识组织。你可以在两天内把链路跑通但要把条目建设得像一份维护良好的手册得花好几周。如果你所在的公司连文档目录、版本号、编号规则都没有先别上 RAG先把文档治理做好。否则再好的双层结构喂进去的也是垃圾。最后再分享一个小技巧我会把“没有命中任何条目”单独设计一条兜底路径不让模型硬答。遇到这种 query直接回复“知识库中没有找到相关内容请换个问法或者联系管理员”。这比模型编一个错误答案强一万倍。毕竟 RAG 的价值不只是让模型更聪明更是让它在不知道的时候学会闭嘴。
返回列表