ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

RAG检索命中率上不去?切分、查询改写与元数据过滤才是关键

RAG检索命中率上不去?切分、查询改写与元数据过滤才是关键

1. 检索命中率上不去,先别急着换向量模型

做RAG项目的人,十个里有八个在第二周就会撞上同一堵墙:明明知识库里躺着正确答案,用户问出来,系统就是找不回来。这时候大多数人的第一反应是“向量模型不行,换一个”,然后花两天时间把bge换成m3e,再换成某个榜单上排名更高的模型,结果命中率只从62%爬到65%,投入产出比惨不忍睹。

我踩过这个坑之后才想明白一件事:检索命中率是一个系统工程问题,向量模型只是其中一环,而且往往不是最短板的那一环。真正吃掉命中率的,通常是文档切分策略、查询改写逻辑、元数据过滤这三件事。向量模型决定的是“语义空间的上限”,但如果你切分的时候把一段完整的答案拦腰截断,再好的模型也拼不回那个语义。

1.1 切分粒度决定了召回的天花板

先讲一个我实测过的案例。有一份产品FAQ文档,原始结构是“问题标题 + 三段式回答”。我第一版用固定长度切分,chunk_size设成512,overlap设成50。跑测试集的时候发现一个诡异现象:问“退款到账时间”,系统返回的片段里包含了“退款”两个字,但答案的关键句“一般在3到5个工作日内到账”被切到了下一个chunk里。检索命中了,但命中的是半截话。

后来我改成按语义边界切分,具体做法是:先用标题层级做一级切分,把每个FAQ条目切成独立单元;如果单个条目超过800字,再按段落切;段落内部如果还超长,才用固定长度兜底。这样改完之后,同一套测试集的命中率从64%直接拉到81%。切分策略的收益,远比换向量模型来得猛。

这里有个经验值可以参考:中文技术文档的chunk_size,我一般控制在300到600字之间。太短了语义不完整,太长了噪声占比高。overlap我通常设成chunk_size的15%到20%,目的是防止关键句正好落在边界上。但overlap不是越大越好,设成50%会让检索结果大量重复,反而稀释了有效信息。

1.2 查询改写是低成本高回报的杠杆

用户提问的方式和文档书写的方式,天然存在鸿沟。用户问“为什么我的订单一直没发货”,文档里写的是“订单物流状态异常处理流程”。这两句话在向量空间里的距离,可能比你想的远。

我现在的标准做法是加一层查询改写:用户原始query进来之后,先用一个小模型或者规则模板做扩展。规则模板很简单,比如把“为什么”开头的问句转成“原因 处理 方法”这样的关键词组合;把口语化的“一直没发货”映射成“物流 延迟 未更新”。这一步不需要大模型,用同义词表加简单的模式匹配就能覆盖七八成场景。

如果预算允许,用一个小尺寸的LLM做query改写效果更好。我试过用Qwen2.5-1.5B做few-shot改写,给它五个示例,让它把用户问题改写成三个不同角度的检索query,然后三路召回取并集。这个方案在我的测试集上把召回率从78%提到了89%,延迟只增加了200毫秒左右。用1.5B的小模型做改写,比用72B的大模型做检索要划算得多。

1.3 元数据过滤是最被低估的武器

很多人建向量库的时候,只存了文本和向量,把文档来源、更新时间、文档类型这些元数据全扔了。等到检索的时候发现,用户问的是“2024年的政策”,结果2022年的旧文档因为语义相似度高被排在了前面。

我的做法是:入库的时候强制带上source、doc_type、update_time、section这四个字段。检索的时候先做一轮元数据粗筛,再做向量精排。比如用户问“最新的报销标准”,我直接在检索前加一个update_time >= 2024-01-01的过滤条件,把候选集从十万级降到千级,再跑向量检索,速度和准确率同时提升。

注意:元数据过滤不要设得太死。我见过有人把doc_type设成精确匹配,结果用户问“怎么配置”,系统只搜“配置指南”类型的文档,漏掉了“操作手册”里同样包含配置步骤的段落。元数据过滤应该是“优先”而不是“唯一”,建议做成加权而不是硬过滤。

2. 答非所问的根源:上下文窗口里塞了太多废话

检索命中了正确的文档片段,但LLM生成的答案还是跑偏,这种情况我遇到太多次了。排查下来,问题往往出在“喂给LLM的上下文”上。检索模块返回了Top-5的片段,你一股脑全塞进prompt里,其中三个片段是相关的,两个是噪声。LLM在生成的时候,会被噪声带偏,尤其是当噪声片段里包含和问题相似但答案相反的内容时。

2.1 重排序不是可选项,是必选项

向量检索的本质是“粗筛”,它保证的是召回率,不是精确率。Top-20里可能有5个真正相关的,但它们的排序未必靠前。这时候需要一个重排序模型来做精排。

我常用的方案是bge-reranker-v2-m3,它是一个交叉编码器,把query和每个候选片段拼在一起打分,比向量点积的精度高出一大截。实测下来,加一层重排序,Top-3的准确率能从71%提到88%。代价是延迟增加,因为交叉编码器要对每个候选片段单独推理。我的折中方案是:向量检索召回Top-20,重排序取Top-5,再送给LLM。这样延迟控制在可接受范围内,精度也够用。

如果你不想引入额外的模型,还有一个土办法:用LLM自己做重排序。把Top-10的片段编号后塞给LLM,让它输出“与问题最相关的三个片段编号”。这个方案零额外部署,但会消耗token,而且LLM有时候会犯懒,只选第一个。我一般只在没有GPU资源的时候用这招。

2.2 上下文去重和压缩能救命

检索回来的片段之间经常有大量重叠,尤其是overlap设得比较大的时候。三个片段讲的是同一件事,只是切分位置不同。这些重复内容不仅浪费token,还会让LLM误以为“这个信息很重要,出现了三次”,从而过度强调。

我的处理流程是这样的:先做文本去重,用简单的n-gram重叠率判断,重叠超过70%的片段只保留信息量最大的那个;然后做上下文压缩,把每个片段里和query无关的句子删掉。压缩可以用一个小模型做抽取式摘要,也可以用规则——比如只保留包含query关键词的句子及其前后各一句。

经过这两步处理,原本3000 token的上下文能压到1200 token左右,LLM的答案质量反而更高。上下文不是越多越好,精准比海量重要。

2.3 prompt里的指令顺序有讲究

这是一个很细节但影响很大的点。我对比过两种prompt结构:

第一种是把检索到的上下文放在前面,问题放在最后:

[上下文1] [上下文2] [上下文3] 问题:xxx

第二种是把问题放在前面,上下文放在后面:

问题:xxx 请根据以下资料回答: [上下文1] [上下文2] [上下文3]

实测下来,第二种结构的答案准确率更高。原因可能是LLM在处理长上下文时,对开头和结尾的信息注意力更集中。问题放在开头,模型带着问题去读上下文,提取信息的针对性更强。这个差异在上下文超过2000 token时尤其明显。

3. 知识库更新之后,旧答案还在往外冒

RAG系统上线之后,知识库不可能一成不变。产品文档更新了、政策调整了、FAQ增删了,这些变更如果处理不当,就会出现“用户问的是新政策,系统答的是旧条款”的尴尬局面。

3.1 增量更新不是简单删了重加

最粗暴的做法是每次更新都把整个知识库重建一遍。数据量小的时候没问题,但到了十万级文档,重建一次要几个小时,期间服务不可用,这显然不现实。

我的做法是给每个文档分配一个稳定的doc_id,更新的时候先根据doc_id删除旧向量,再插入新向量。听起来简单,但有两个坑:

第一个坑是向量库的删除操作往往是软删除。你调了delete接口,但底层只是打了个标记,实际数据还在,检索的时候可能还会被召回。解决办法是删除之后立刻做一次compact,或者用带版本号的过滤条件,检索时只取最新版本。

第二个坑是关联片段的孤儿问题。一个文档被切成十个片段,你更新了其中三个片段对应的原文,但另外七个片段的向量还是旧的。如果这些片段之间存在语义依赖,就会出现新旧混杂的情况。我的建议是:以文档为单位更新,不要以片段为单位更新。文档变了,整个文档的所有片段全部重新生成。

3.2 版本冲突的检测机制

有些场景下,新旧知识会同时存在。比如公司有两个版本的报销制度,旧版适用于2023年之前的费用,新版适用于2024年之后的。用户问“出差住宿标准”,系统需要根据时间来判断用哪个版本。

这种场景下,元数据里的effective_date和expire_date就派上用场了。检索的时候带上当前日期做过滤,自动排除已过期的文档。如果用户明确问的是历史政策,再放开时间过滤。

我还加了一个简单的冲突检测:如果检索结果里同时出现了两个版本的同名文档,且它们的相似度都很高,就在答案里加一句提示——“检测到该问题存在多个版本的资料,以下回答基于最新版本,如需历史版本请说明”。这样既避免了答错,又给了用户选择权。

3.3 缓存失效策略

为了降低延迟,很多人会在RAG前面加一层缓存:相同的问题直接返回缓存答案。但知识库更新之后,缓存里的旧答案就成了定时炸弹。

我的缓存策略是:缓存key里带上知识库的版本号。每次知识库更新,版本号递增,旧缓存自然失效。同时设置一个最大缓存时间,比如24小时,即使知识库没更新,缓存也会定期刷新。这样既保证了性能,又避免了“知识库更新了但用户还看到旧答案”的问题。

4. 多跳问题和知识割裂:单次检索搞不定的场景

有些问题不是“检索一个片段就能回答”的。比如“我们公司去年营收增长了多少,主要驱动因素是什么”,这需要先检索营收数据,再检索驱动因素分析,然后把两者结合起来。单次向量检索只能召回一个维度的信息,遇到这种多跳问题就歇菜了。

4.1 查询分解:把复杂问题拆成子问题

我现在的标准流程是:用户query进来之后,先判断它是不是多跳问题。判断方法很简单,用一个小模型做分类,或者用规则——如果query里包含“和”“以及”“分别”“对比”这类连接词,大概率是多跳。

确认是多跳之后,用LLM把原问题拆成2到3个子问题,每个子问题独立检索,最后把检索结果合并。比如上面那个问题,拆成“去年营收数据”和“营收增长驱动因素”两个子查询,分别召回后再拼在一起送给LLM生成答案。

这个方案的关键在于子问题的检索结果要标注来源,让LLM知道哪些信息来自哪个子问题。我通常会在上下文里加标记,比如[营收数据]和[驱动因素],帮助LLM建立对应关系。

4.2 迭代检索:让LLM自己决定下一步查什么

比查询分解更进一步的方案是迭代检索。第一轮检索之后,把结果和原问题一起送给LLM,让LLM判断“现有信息是否足够回答问题”。如果不够,让LLM生成下一个查询,再检索一轮。最多迭代三轮,防止死循环。

这个方案的效果很好,但成本也高,因为每轮都要调LLM。我的优化是:第一轮用向量检索,如果Top-1的相似度超过阈值(比如0.85),直接生成答案,不走迭代;只有相似度低的时候才启动迭代检索。这样大部分简单问题走快速通道,只有真正复杂的才走多轮。

4.3 知识图谱补位:解决关系型问题

有些问题涉及实体之间的关系,比如“A产品的负责人是谁,他之前负责过哪些项目”。向量检索擅长语义匹配,但不擅长关系推理。这种场景下,知识图谱或者GraphRAG就派上用场了。

我的做法是混合检索:向量检索负责语义召回,图谱检索负责关系召回,两者结果合并后重排序。图谱的构建不需要太复杂,把文档里的实体和关系抽出来,存成三元组就行。查询的时候,先用NER识别query里的实体,然后在图谱里查一跳或两跳的关系。

这个方案的门槛在于图谱构建,需要一定的NLP工程能力。如果团队没有图谱经验,可以先从简单的实体链接做起——把文档里的产品名、人名、项目名识别出来,建立倒排索引,检索的时候做实体匹配加权。这比完整的图谱方案轻量得多,但能解决一部分关系型问题。

5. 评估体系缺失:你不知道你的RAG到底好不好

我见过太多团队,RAG系统上线之后,靠“感觉”判断好坏。用户反馈说“还行”,就觉得没问题。直到某天老板问了一个问题,系统答错了,才发现原来一直有问题。

5.1 构建测试集是第一步

没有测试集,所有的优化都是盲人摸象。测试集的构建不需要很复杂,从真实用户问题里抽100到200条,人工标注每条问题的正确答案和对应的文档片段。这个工作量大,但一次投入长期受益。

我的测试集包含三类问题:简单事实型(答案在一个片段里)、多跳推理型(需要多个片段组合)、边界型(知识库里没有答案,系统应该回答“不知道”)。第三类最容易被忽略,但恰恰是用户体验的关键——宁可说不知道,也不要胡编乱造。

5.2 检索和生成分开评估

RAG的评估要拆成两个环节:检索环节看召回率和精确率,生成环节看答案的准确性和忠实度。

检索环节的指标:Top-1命中率、Top-3命中率、MRR(平均倒数排名)。我一般要求Top-3命中率在85%以上,否则就要优化切分或检索策略。

生成环节的指标:答案准确性(人工打分)、忠实度(答案是否完全基于检索到的上下文,没有编造)。忠实度可以用一个小模型做自动评估,把上下文和答案一起送给模型,让它判断“答案里的每一句话是否都能在上下文中找到依据”。

5.3 持续监控和bad case归因

上线之后,我会记录每次检索的query、召回的片段、生成的答案、用户的反馈(点赞/点踩)。每周做一次bad case分析,把答错的问题归类:是检索没召回,还是召回了但排序不对,还是LLM生成时跑偏了。

这个归因过程很重要。如果是检索问题,优化切分和查询改写;如果是排序问题,加重排序模型;如果是生成问题,调prompt或者换LLM。不同环节的问题,解法完全不同,不能一概而论。

我自己的经验是,RAG系统上线后的前三个月,每周都能从bad case里找到可优化的点。三个月之后,优化频率降到每月一次,系统基本稳定。但监控不能停,因为知识库在变、用户在变、问题类型也在变。

6. 一些零散但实用的经验

前面五章讲的是系统性的问题和解法,最后分享几个我在实操中攒下来的零碎经验,不一定成体系,但都挺管用。

关于向量库选型:小规模(十万级以下)用FAISS就够了,轻量、快、零依赖。上了百万级考虑Milvus或Qdrant,分布式和过滤能力更强。不要一上来就上重型向量库,运维成本会吃掉你的开发时间。

关于embedding维度:不是越高越好。768维和1024维在我的测试集上差距不到2%,但存储和检索成本差了近一倍。除非你的领域特别垂直、语义特别细腻,否则768维足够用。

关于LLM选型:生成环节的LLM,7B到14B的参数规模在RAG场景下性价比最高。更大的模型确实更强,但RAG的核心是“根据给定上下文回答”,这个任务对模型规模的要求没有开放式生成那么高。我常用Qwen2.5-7B-Instruct做生成,效果稳定,部署成本可控。

关于prompt里的“不知道”指令:一定要在prompt里明确写“如果上下文中没有相关信息,请回答‘根据现有资料无法回答该问题’,不要编造”。这句话能挡掉大部分幻觉。我还会加一句“回答时请引用原文”,让LLM养成基于上下文说话的习惯。

关于中文分词的坑:如果你的检索链路里有BM25或关键词检索,中文分词器选不好会严重影响效果。我试过jieba、HanLP、pkuseg,最后在技术文档场景下选了jieba的搜索引擎模式,配合自定义词典把产品名和术语加进去。分词这事没有银弹,得根据你的语料调。

关于测试集的更新:测试集不是建一次就完了。每次知识库大更新、每次LLM换版本,都要重新跑一遍测试集。我见过换了LLM之后,旧测试集通过率从90%掉到70%的情况,因为新模型对prompt的敏感度不同。测试集是你的安全网,别让它过期。

这些经验没有什么高深的理论,都是一个个项目里试出来、踩出来的。RAG这个方向,工具和模型迭代很快,但底层的工程逻辑变化没那么快——切分、检索、重排、生成、评估,每个环节都有它的脾气。把每个环节的边界摸清楚,比追新模型要实在得多。

返回列表