ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

RAG检索优化实战:从查询改写、切分到混合检索与重排

RAG检索优化实战:从查询改写、切分到混合检索与重排 1. 检索结果答非所问问题到底出在链路的哪一环做RAG项目的人十个里有八个在第一次上线后都会遇到同一个尴尬用户问了一个明明知识库里有的问题系统却答得驴唇不对马嘴或者干脆答根据现有资料无法回答。这时候大多数人的第一反应是去调大模型参数、换更强的LLM但折腾一圈发现效果没变。我踩过这个坑之后才明白RAG答非所问九成以上的根因不在生成端而在检索端。1.1 先搞清楚RAG这条链路到底有几个环节会出错一个标准的RAG流程从用户输入到最终输出中间要经过这么几道关查询理解与改写、向量化Embedding、向量检索或混合检索、结果重排Rerank、上下文组装、最后才是LLM生成。任何一环出问题最终表现都是答得不对但病因完全不同。我习惯用一个类比来解释RAG就像你去图书馆找资料写报告。查询改写是你跟管理员描述你要什么Embedding是把你的描述翻译成图书分类号向量检索是按分类号去书架上拿书Rerank是把拿回来的一堆书按相关度排序上下文组装是决定把哪几本书的哪几页摊在桌上LLM生成是你读完这些资料后动笔写。如果最后报告写得烂可能是管理员理解错了你的需求也可能是分类号翻译错了还可能是书架上根本没那本书未必是写报告的人文笔不行。所以排查RAG问题的第一步永远是把链路拆开逐段验证而不是一上来就怀疑LLM。1.2 用检索命中率这个指标定位问题区间我一般会先算一个粗指标Hit Rate命中率。做法很简单准备一批测试问题每个问题人工标注出知识库里对应的正确文档片段chunk然后跑检索看Top-K结果里有没有包含这个正确片段。如果Top-5的命中率低于70%那问题基本锁定在检索端跟LLM没关系。这里有个实操细节测试集一定要覆盖不同类型的问法。我见过太多人只用文档里原句改个问号来测试命中率当然高一上线遇到口语化提问就崩。我的做法是每个知识点准备三种问法——原文式、口语式、以及需要跨文档推理的复合式这样测出来的命中率才有参考价值。# 一个极简的命中率测试脚本骨架 test_cases [ {query: RAG的检索增强具体增强在哪, gold_chunk_id: doc_12_chunk_3}, {query: 为什么我的知识库答不出问题, gold_chunk_id: doc_07_chunk_1}, # ... 覆盖多种问法 ] hit 0 for case in test_cases: results retriever.search(case[query], top_k5) retrieved_ids [r.chunk_id for r in results] if case[gold_chunk_id] in retrieved_ids: hit 1 print(fHit Rate5: {hit / len(test_cases):.2%})跑完这个脚本如果命中率低就继续往下拆是Embedding模型不行还是chunk切得不对还是查询本身没被正确理解。定位到具体环节再对症下药比盲目换模型高效十倍。1.3 查询改写被最多人忽略的第一道关很多RAG系统的查询改写环节是空的用户问什么就直接拿什么去检索。这在知识库文档措辞和用户提问措辞高度一致时没问题但现实中两者往往差得很远。用户问这个功能怎么收费文档里写的是计费策略说明字面重叠度极低向量检索很容易漏掉。我的经验是查询改写至少要处理三件事一是把口语化表达转成更接近文档的书面表达二是把指代消解掉它这个要还原成具体名词三是把复合问题拆成多个子查询分别检索。第三点尤其重要用户一句对比A方案和B方案的优缺点直接检索往往两边都拿不全拆成A方案的优缺点和B方案的优缺点两次检索再合并效果好得多。提示查询改写可以用小模型做也可以用规则LLM混合。我实测下来用同一个小参数量的LLM做改写成本可控效果比纯规则好很多。但要注意改写不能过度把用户原意改跑偏了反而更糟所以改写后的查询和原查询建议都保留做多路检索。2. 知识切分不当再好的模型也救不回来如果说检索是RAG的命脉那chunk切分就是命脉的命脉。我见过太多项目模型选的是顶配向量库也是主流方案但因为切分策略粗糙整个系统效果就是上不去。切分这件事看起来简单实际上坑最多也最考验对业务数据的理解。2.1 固定长度切分的三个致命伤新手最常用的切分方式是按固定字符数切加一点重叠。这个方法上手快但有几个绕不开的问题。第一个是语义截断。一个完整的论述被从中间切开前半段在chunk A后半段在chunk B检索时只召回ALLM看到的就是半句话自然答不全。第二个是标题与正文分离。很多文档是标题正文结构固定切分很容易把标题单独切成一个chunk正文切到另一个chunk结果标题那个chunk信息量极低却可能被召回浪费上下文窗口。第三个是表格和列表被切碎。表格一旦跨chunk行列对应关系就丢了LLM读到的是一堆错位的数字。我一般的做法是按文档结构切分优先固定长度兜底。具体来说先按标题层级H1/H2/H3切每个最小标题下的内容作为一个语义单元如果某个单元还是太长再在单元内部按段落切段落还长才用固定长度切并加重叠。这样能最大程度保住语义完整性。2.2 重叠窗口设多少得看你的文档类型重叠overlap是为了缓解截断问题让相邻chunk有部分内容重复检索时至少能捞到完整语义的一部分。但重叠设多少很多人是拍脑袋定的。我的经验值是这样的技术文档、法律条文这类逻辑严密的重叠设chunk长度的15%到20%因为一句话的完整意思经常跨段新闻、博客这类段落独立的重叠设5%到10%就够了设太多反而引入噪声。另外重叠不是越大越好重叠过大意味着存储和检索成本上升而且容易召回大量重复内容挤占上下文窗口。这里有个容易被忽略的点重叠部分在组装上下文时要去重。我见过系统把重叠内容原样塞给LLM结果LLM看到重复信息输出里也跟着重复体验很差。组装上下文时做个简单的去重或标记能明显改善输出质量。2.3 元数据是切分时最该顺手做的事切分的时候除了文本内容一定要把元数据一起存进去。什么是元数据就是这段chunk来自哪个文档、哪个章节、第几页、什么类型正文/表格/代码。这些信息在检索和生成阶段都有大用。检索阶段元数据可以用来做过滤。比如用户问的是2024年的政策你可以先用元数据把年份不等于2024的chunk过滤掉再在剩下的里面做向量检索精度立刻提升。生成阶段元数据可以用来做引用标注让LLM回答时能说清楚根据《XX文档》第3章用户信任度完全不一样。# chunk元数据的建议字段 chunk_metadata { doc_id: policy_2024_001, doc_title: 2024年度服务政策说明, section: 第三章 计费规则, page: 12, content_type: text, # text / table / code updated_at: 2024-06-01 }注意元数据字段不要贪多够用就行。字段太多会增加存储和检索的复杂度而且很多字段实际用不上。我一般保留文档标识、章节、类型、时间这四类基本覆盖了绝大多数过滤和引用需求。3. 向量检索不灵时混合检索和重排该怎么上向量检索是RAG的默认方案但它不是万能的。向量检索擅长语义相似不擅长精确匹配。用户问一个具体的编号、人名、专有名词向量检索经常召回一堆语义相近但就是没有那个关键词的chunk。这时候就需要混合检索和重排来补位。3.1 什么时候该上混合检索判断标准很简单如果你的知识库里存在大量专有名词、编号、代码、缩写就该上混合检索。纯向量检索对这些精确token不敏感而关键词检索BM25这类恰好擅长这个。混合检索的做法是把向量检索和关键词检索的结果融合。融合方式有两种主流一种是加权求和给两路结果各算一个分数按权重相加另一种是倒数排名融合RRF不看具体分数只看排名把两路排名做倒数相加。我实测下来RRF更稳因为它不依赖两路分数的量纲对齐省去了调权重的麻烦。# RRF融合的简化实现 def rrf_fusion(vector_results, keyword_results, k60): scores {} for rank, doc in enumerate(vector_results): scores[doc.id] scores.get(doc.id, 0) 1 / (k rank 1) for rank, doc in enumerate(keyword_results): scores[doc.id] scores.get(doc.id, 0) 1 / (k rank 1) return sorted(scores.items(), keylambda x: x[1], reverseTrue)那个k值一般取60是经验值不用太纠结在50到70之间效果差别不大。3.2 重排模型是性价比最高的一步优化如果只能做一项优化来提升RAG效果我会选加重排Rerank。原因很简单检索阶段为了不漏通常会召回Top-20甚至Top-50但真正能塞进LLM上下文窗口的可能只有Top-5。这中间的筛选用重排模型来做比单纯靠向量相似度排序准得多。重排模型比如各种Cross-Encoder架构的会把查询和每个候选chunk拼在一起打分虽然慢但精度高。我的做法是检索召回Top-30重排后取Top-5这个组合在大多数场景下效果和成本的平衡最好。这里有个坑要提醒重排模型和Embedding模型最好配套选。有些重排模型是针对特定Embedding训练的混搭可能效果打折。如果不想折腾选同一家或同一系列的产品省心。3.3 检索参数不是调一次就完事Top-K取多少、相似度阈值设多少这些参数没有一劳永逸的值得根据你的知识库规模和问题类型动态调。知识库大、问题宽泛K可以大一点知识库小、问题具体K小一点反而准。我一般会做一个小规模的参数扫描固定测试集把K从3扫到20看命中率和最终答案质量的变化曲线找到拐点。相似度阈值也是类似设太高会漏召回设太低会引入噪声扫一遍找到平衡点。这个过程花不了多少时间但收益很实在。参数常见取值范围调大后的影响调小后的影响Top-K3 ~ 20召回全但噪声多精准但易漏相似度阈值0.5 ~ 0.8噪声少但可能漏召回全但噪声多重排后保留数3 ~ 8上下文丰富但可能超窗精简但可能缺信息重叠比例5% ~ 20%语义完整但冗余精简但易截断4. 上下文塞不下、塞不对生成质量照样崩检索做对了chunk也切好了结果LLM还是答不好这时候问题往往出在上下文组装这一环。很多人以为把检索到的chunk一股脑拼起来丢给LLM就行实际上这里面的讲究不比检索少。4.1 上下文窗口不是越大越好现在很多模型支持很长的上下文于是有人就把Top-20的chunk全塞进去。结果呢LLM在长上下文里迷失了关键信息淹没在大量无关内容中反而答不准。这就是所谓的lost in the middle现象——模型对上下文中间部分的信息利用效率明显低于开头和结尾。我的做法是宁精勿多。检索召回可以宽但最终塞给LLM的一定是经过重排精选的Top-3到Top-5。如果确实需要更多信息宁可分多轮对话也不要一次性堆进去。另外把最相关的chunk放在上下文的最前面或最后面中间放次要的能稍微缓解中间迷失的问题。4.2 上下文里要带上来源感LLM生成时如果上下文里明确标注了每段内容的来源和结构输出质量会明显提升。我一般会在组装时给每个chunk加上类似这样的标记[来源2024年度服务政策说明 - 第三章 计费规则] 按使用量阶梯计费前1000次免费超出部分每次0.01元。 [来源结束]这样做有两个好处一是LLM知道这段内容的边界在哪不会把不同chunk的内容混着编二是生成时可以直接引用来源用户看到根据《XX》第三章会觉得靠谱。引用标注这个功能对提升用户信任度的作用被严重低估了。4.3 提示词模板要留不知道的出口这是我在实际项目里踩过的最大的坑之一。早期我的提示词是请根据以下资料回答问题结果LLM遇到资料里没有的问题也会硬编一个答案出来也就是幻觉。后来我改成明确告诉它如果资料中没有相关信息请直接说无法回答不要编造幻觉率立刻降下来。提示词模板里不知道的出口必须显式写出来而且要写得强硬一点。我常用的模板结构是这样的你是一个严谨的问答助手。请严格根据下面提供的资料回答问题。 规则 1. 只使用资料中明确提到的信息不要引入外部知识。 2. 如果资料中没有足够信息回答问题直接回复根据现有资料无法回答该问题。 3. 回答时标注信息来源。 资料 {context} 问题{question}提示规则里的第2条措辞越明确越好。无法回答比可能无法回答效果好直接回复比可以回复效果好。别小看这几个字的差别实测对幻觉率有影响。5. 知识库更新后效果变差增量维护的坑怎么绕RAG系统上线不是终点知识库是要持续更新的。而更新往往是效果回退的高发期。我见过系统上线时好好的运营往里加了一批新文档结果老问题的回答质量反而下降了。这背后的原因值得单独拎出来讲。5.1 新旧文档的Embedding分布漂移不同时间、不同来源的文档用同一个Embedding模型编码后向量分布可能不一致。新文档如果措辞风格和老文档差很多检索时容易出现新文档霸屏或老文档被挤掉的情况。这不是模型坏了而是数据分布变了。我的应对办法是定期做全量重编码而不是只对新文档做增量编码。全量重编码成本高一点但能保证所有向量在同一坐标系下检索一致性更好。如果知识库特别大至少也要定期抽样检查新旧文档的检索表现发现漂移及时处理。5.2 删除和修改比新增更容易出问题新增文档相对简单麻烦的是删除和修改。如果一篇文档被删了但它的向量还留在库里检索时就会召回已经不存在的内容LLM据此生成的答案就是过时的。修改同理改了正文但没更新向量检索到的还是旧内容。所以知识库维护必须做到文档和向量严格同步。我的做法是给每个文档一个唯一ID文档的任何变更增删改都触发对应向量的同步操作。删除时按doc_id批量删向量修改时先删旧向量再插新向量。这个逻辑听起来简单但实际做的时候很容易漏建议写成统一的更新入口所有变更都走这个入口避免散落各处。5.3 用回归测试守住效果底线知识库每次更新后我都会跑一遍回归测试集——就是前面提到的那个带标注的测试问题集。看命中率和答案质量有没有明显下降。如果下降了就回滚这次更新排查原因。这个测试集要持续维护每次发现新的bad case就补进去。时间长了它就变成了你系统的体检表任何改动的影响都能第一时间发现。我现在的习惯是没有回归测试通过知识库更新不上线这条规矩帮我避免了好几次线上事故。更新类型常见风险应对措施新增文档分布漂移、霸屏定期全量重编码删除文档残留向量被召回按doc_id同步删除修改文档旧向量未更新先删后插统一入口批量导入格式不一致导入前做格式校验6. 几个我反复验证过的实操心得聊完这五类问题再分享几个贯穿始终的经验都是我在多个项目里反复验证过的。第一先跑通最小闭环再谈优化。很多人一上来就纠结用哪个向量库、哪个重排模型结果链路都没跑通。我的建议是先用最简单的方案固定切分基础向量检索现成LLM跑通端到端拿到第一版效果数据再针对瓶颈逐项优化。没有基线优化就是盲人摸象。第二bad case比指标更重要。Hit Rate、召回率这些指标能告诉你大概怎么样但真正指导优化的是一个个具体的bad case。我习惯建一个bad case库每次发现答得不好的问题就记下来分析根因归类。时间长了你会发现问题往往集中在少数几类解决这几类整体效果就上一个大台阶。第三别迷信最强模型。Embedding模型、重排模型、LLM都不是越大越好。小模型在特定领域微调后效果可能超过通用大模型而且成本和延迟都更友好。我做过对比在垂直领域知识库上一个中等规模的Embedding模型配合好的切分策略检索效果不输顶级大模型但成本低一个数量级。第四把评估做成常态。RAG系统的效果会随着知识库变化、用户提问分布变化而波动一次评估说明不了问题。我现在的做法是每周自动跑一次回归测试生成效果报告有异常就排查。这个习惯让系统长期保持稳定也让我对每次改动的效果心里有数。最后说个我自己的体会RAG这件事工程细节的权重远大于模型选型。同样一套模型切分策略、检索参数、上下文组装方式不同效果能差出一倍。所以别总想着换模型先把链路上每个环节的细节抠到位效果自然就上来了。
返回列表