1. 检索命中率上不去先别急着换向量模型做RAG项目的人十个里有八个在第二周就会撞上同一堵墙明明知识库里躺着正确答案用户问出来系统就是找不回来。这时候大多数人的第一反应是“向量模型不行换一个”然后花两天时间把bge换成m3e再换成某个榜单上排名更高的模型结果命中率只从62%爬到65%投入产出比惨不忍睹。我踩过这个坑之后才想明白一件事检索命中率是一个系统工程问题向量模型只是其中一环而且往往不是最短板的那一环。真正吃掉命中率的通常是文档切分策略、查询改写逻辑、元数据过滤这三件事。向量模型决定的是“语义空间的上限”但如果你切分的时候把一段完整的答案拦腰截断再好的模型也拼不回那个语义。1.1 切分粒度决定了召回的天花板先讲一个我实测过的案例。有一份产品FAQ文档原始结构是“问题标题 三段式回答”。我第一版用固定长度切分chunk_size设成512overlap设成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选型生成环节的LLM7B到14B的参数规模在RAG场景下性价比最高。更大的模型确实更强但RAG的核心是“根据给定上下文回答”这个任务对模型规模的要求没有开放式生成那么高。我常用Qwen2.5-7B-Instruct做生成效果稳定部署成本可控。关于prompt里的“不知道”指令一定要在prompt里明确写“如果上下文中没有相关信息请回答‘根据现有资料无法回答该问题’不要编造”。这句话能挡掉大部分幻觉。我还会加一句“回答时请引用原文”让LLM养成基于上下文说话的习惯。关于中文分词的坑如果你的检索链路里有BM25或关键词检索中文分词器选不好会严重影响效果。我试过jieba、HanLP、pkuseg最后在技术文档场景下选了jieba的搜索引擎模式配合自定义词典把产品名和术语加进去。分词这事没有银弹得根据你的语料调。关于测试集的更新测试集不是建一次就完了。每次知识库大更新、每次LLM换版本都要重新跑一遍测试集。我见过换了LLM之后旧测试集通过率从90%掉到70%的情况因为新模型对prompt的敏感度不同。测试集是你的安全网别让它过期。这些经验没有什么高深的理论都是一个个项目里试出来、踩出来的。RAG这个方向工具和模型迭代很快但底层的工程逻辑变化没那么快——切分、检索、重排、生成、评估每个环节都有它的脾气。把每个环节的边界摸清楚比追新模型要实在得多。
阅读完成 · 觉得有帮助?