RAG检索进阶:从向量检索到多路召回与Rerank的工程实践

发布时间:2026/9/7 10:05:06
RAG检索进阶:从向量检索到多路召回与Rerank的工程实践 1. 检索为什么成了RAG的“天花板”做了几年RAG项目我最大的体会是决定一个RAG系统上限的往往不是大模型多聪明而是检索这一关到底能不能把真正有用的上下文捞回来。早期做知识库问答的时候我的做法特别朴素——文档一切片embedding一算余弦相似度一排序把top 5丢给大模型。demo跑起来效果还行一问一个准。可真上了生产环境问题就全暴露了用户问的是口语化说法知识库里写的是标准化术语向量相似度根本匹配不上用户想找的是跨章节、跨文档的关联信息单靠文本语义相似度压根搜不出来还有一类问题是关键词完全一致但问的是不同意思向量检索直接给出了一堆不相关段落。这一章的定位很明确进阶。我会把过去一年多在多个RAG项目里实际落地过的检索方案串起来讲——从dense vector search的原理与调参到多路召回、rerank排序、query改写、Graph RAG、Ontology RAG再到现在的Agentic RAG最后聊一聊RAG测评怎么做。内容不追求面面俱到只挑那些真正影响线上效果的关键点展开。如果你已经在做RAG但是发现不管怎么调prompt、换模型回答质量就是上不去那问题大概率出在检索策略上。这一章就是冲着这个来的。2. Dense Vector Search向量检索不是“算个相似度”那么简单2.1 Embedding模型选型通用模型和领域模型的差距有多大很多RAG项目翻车翻在第一步——embedding模型选错了。先用一个实际案例说明问题。在一个政务政策问答项目里用户会问“外地户口能在本地办社保吗”政策原文的表述是“非本市户籍人员参加社会保险的应当提交居住证明”。如果用一个在通用语料上训练的embedding模型这两个句子的向量相似度往往很低因为表面词汇差异太大了。但是换成在政务文本上微调过的embedding模型它学会了“外地户口”和“非本市户籍”在语义上是等价的相似度分数可以提升0.1到0.2。这里有一个需要提前说明的点不是所有情况都要上领域模型。如果知识库内容本身就是技术文档、通用常识这类文本通用模型完全够用。但如果你做的是政务、法律、医疗、金融这类专业领域强烈建议在领域语料上做embedding模型的继续预训练或微调。我自己在项目中评估embedding模型的流程是这样的先准备200到300条领域内的典型问答对不需要很多但覆盖面要广。对每一条query在知识库里用不同embedding模型做一次检索人工判断top 10里有没有正确答案。统计Recall10。哪个模型命中率高就选哪个。这段评估流程看起来简单但实际踩坑概率很高。最常见的问题是测试集太小导致选出来的模型过拟合。我建议测试query至少覆盖知识库的各个主要模块而不是集中在某一个业务板块。2.2 距离度量、索引参数与检索窗口的权衡选好模型只是第一步。dense vector search真正要花的功夫在参数调优上。先说距离度量。主流向量数据库支持cosine similarity、inner product和euclidean distance。我的习惯是embedding模型没有显式做向量归一化时优先用cosine如果用的是OpenAI等自带归一化的模型inner product和cosine等价选哪个都行。但如果模型本身没有归一化直接用inner product会让长文本向量天然占优势检索结果偏向长段落这是非常隐蔽的坑。再说索引参数。大部分项目用的都是HNSW索引几个关键参数的意义和处理经验如下参数含义我的经验值说明M每个节点的最大连接数16到32M越大召回越准但内存占用越高efConstruction建索引时的搜索宽度100到200影响索引质量建完再改需要重建索引efSearch查询时的搜索宽度50到100实时可调越大召回越全但延迟越高我实际测试过一组对比数据在一个10万段落的政务知识库里把efSearch从50调到200Recall10提升了大约5个百分点但P99延迟从20毫秒涨到了80毫秒。具体怎么平衡取决于业务是离线分析还是在线问答。在线问答场景我一般把efSearch控制在100左右同时开着多副本分摊流量。还有检索窗口。很多初学者把top_k设得很小比如3或者5以为这样给大模型的上下文更精准。但实际项目中top_k更应该取决于知识片段的粒度和答案的分布。如果知识库是政策法规那种条文式文本正确答案往往就集中在一两条里面top_k设5没问题但如果知识库里是长篇技术文档答案可能分布在多个章节top_k至少要10到15才有机会覆盖全。我的做法是召回阶段top_k取20到30重排之后再砍到5到8这样既保证召回率又能控制喂给大模型的tokens。3. 多路召回从“单一通道”到“漏斗式融合”3.1 为什么单路召回必然遇到瓶颈只用dense vector search做召回通常会在三类query上栽跟头。第一类是精确匹配类。典型的场景是用户查询设备型号“ZX-3000保修期多久”embedding模型把“ZX-3000”编码成一个稠密向量但这个编号在训练语料里可能很少出现向量表示并不稳定。反而是BM25这种基于词频的稀疏检索看到“ZX-3000”这个词就直接命中了。第二类是缩写和术语类比如“RAG”和“检索增强生成”“NLP”和“自然语言处理”向量检索经常匹配不上但词典扩展后BM25可以。第三类是多跳关联类比如“张三在A部门提交的报销单审批到哪一步了”这类问题依赖实体关系和流程状态纯文本检索很难覆盖。所以多路召回不是为了炫技而是因为不同类型的query天生适配不同的检索方式。工程上的做法是funnel架构多路召回并行执行保证每一路都能覆盖一类query最后融合排序。3.2 各路召回通道怎么搭、怎么融我常用而且觉得稳定的召回通道组合是这样的稠密向量召回承担主要的语义匹配任务召回top 30。稀疏关键词召回BM25覆盖精确匹配、产品型号、人名地名等专有名词场景召回top 20。知识图谱/图检索通道针对实体关系类query先在知识图谱中做实体链接再沿着关系路径检索相关事实。规则通道针对高频FAQ做归一化匹配命中了直接置顶。多路召回的代码框架其实不复杂我第一次落地的时候大概花了一个下午搭原型。核心就是定义一个统一的检索接口每一路返回带score的候选集然后丢进融合器。def multi_recall(query: str, top_k: int 30): candidates {} # 稠密检索 dense_hits dense_search(query, top_ktop_k) # 稀疏检索 bm25_hits bm25_search(query, top_ktop_k) # 图谱通道 graph_hits graph_search(query, top_ktop_k) # 规则通道命中直接高优先级 rule_hits rule_search(query) for hits, weight in [(dense_hits, 0.4), (bm25_hits, 0.3), (graph_hits, 0.2), (rule_hits, 0.1)]: for doc_id, score in hits.items(): candidates[doc_id] candidates.get(doc_id, 0) weight * normalize(score) sorted_cands sorted(candidates.items(), keylambda x: x[1], reverseTrue) return [doc_id for doc_id, _ in sorted_cands[:top_k]]融合算法的选择上我踩过好几个坑。最简单的是加权求和但前提是各路score必须先做归一化否则BM25的分数范围跟cosine完全不是一个量级加权求和就变成了某一路的独角戏。更稳定的是Reciprocal Rank FusionRRF核心思路是按照排名而不是分数来融合公式是score sum(1 / (k rank))k一般取60。RRF的好处是不需要归一化对分数尺度不敏感而且对某一路返回结果质量差的情况更鲁棒。我现在的项目基本都改用RRF了。另外多路召回的延迟优化有一个实战技巧各路通道并行执行用asyncio.gather并发调用而不是串行等待。实测下来三路召回从串行的200毫秒左右降到80毫秒左右对整个链路的时延帮助非常明显。4. Rerank把最前面一页的结果真正排对4.1 双塔召回和交叉重排的本质区别召回阶段用双塔模型dense、sparse都是这类query和doc各自独立编码成向量在向量空间里做近似检索。双塔的优势是快doc向量可以离线算好建索引但代价是query和doc之间没有充分的交互——两个向量只是最后算了一下相似度中间的细粒度语义匹配关系完全丢失了。rerank阶段用交叉编码器cross-encoderquery和doc拼接成一个长序列送进同一个模型让模型对它们的每个token做深度交互能捕捉到“问题里的这个词对应文档里的那个短语”这种细粒度关系。代价是慢每个候选都要单独过一次模型所以rerank只能对召回后的少量候选做精排。用更生活化的比喻召回像是HR按简历关键词筛一遍先捞出一批可能合适的人rerank是面试官跟候选人面对面聊把真正合适的挑出来排在前面。两者分工不同不能互相替代。我做过一个对比实验同一组querytop 5准确率从召回直接截取的62%提升到rerank后的81%。这个差距在RAG里是决定性的因为大模型能看到的内容顺序直接影响生成质量——排在前面的上下文被利用得更充分排在后面的信息可能被忽略。4.2 重排模型选型与工程化细节重排模型的选择空间比embedding小。开源方案里我最常用的是BGE系列bge-reranker-base或bge-reranker-large中文效果不错部署成本也可控。如果追求极致效果可以试试Cohere Rerank这类商用API但考虑到数据隐私和成本很多内部项目还是选开源模型自部署。关于重排的工程化细节我总结了几个实操经验重排窗口限制交叉编码器对输入长度敏感超长文本会被截断或性能急剧下降。我的做法是对超长候选做滑窗切片每个窗口单独打分后取最大值。性能优化rerank阶段的延迟大头在模型推理。批量处理是最直接的手段一次把20个候选拼成一个batch利用GPU并行计算延迟能降一半以上。把rerank模型用ONNX Runtime或TensorRT加速后CPU上也能跑得动。不要对全量候选rerank先向量召回top 20到30再rerank出top 5到8。如果对几千个候选直接rerank延迟马上爆炸收益也不会更好。代码层面的实现举个例子我用sentence-transformers加载模型一次批量打分30个候选耗时大约50毫秒效果比之前逐个打分快了不止一倍。from sentence_transformers import CrossEncoder model CrossEncoder(BAAI/bge-reranker-base, max_length512) def rerank(query: str, docs: list[str], top_k: int 5): pairs [(query, doc) for doc in docs[:30]] scores model.predict(pairs, batch_size16) sorted_idx sorted(range(len(scores)), keylambda i: scores[i], reverseTrue) return [docs[i] for i in sorted_idx[:top_k]]这里有一个细节要提醒rerank模型的score分布和embedding模型不一样不同query之间的score绝对值不可比较。所以如果代码里有“score 0.5才采用”这种硬阈值逻辑很容易误伤。我对重排分数的处理是只按相对rank截断不做绝对阈值过滤。5. Query理解与改写检索之前先搞清楚用户想表达什么5.1 查询改写让“问得不准”变成“搜得没错”用户的原始query直接拿去检索经常效果不好。口语化表达、指代不明、缺失上下文这些都是高频问题。Query改写就是在检索之前先用规则或LLM把query变换成更适合检索的形式。我实践下来最实用的三种改写手段是同义词和术语扩展维护一个领域词典把query里的口语词映射到知识库的标准术语。比如“社保”扩展成“社会保险”“工资”扩展成“劳动报酬”。词典之外的场景可以用LLM做同义改写但词典的精准度更高、延迟更低。HyDEHypothetical Document Embeddings先让LLM根据query生成一段假设性的回答文档再用这段文档做向量检索。好处是假设文档和知识库文档的语义空间更接近召回率更高。代价是每次检索前多一次LLM调用延迟增加几百毫秒。查询补全会话场景里用户的第二轮提问往往是“那需要什么材料”“流程是什么”这类省略句式。需要把上一轮的主题拼进去变成完整query再检索。我的经验是query改写要做得克制。改写幅度越大越有可能引入噪声。每次改写最好还能做一次原query和改写query的双路召回最后融合结果这样即使改写失误也不会彻底丢失原始意图。5.2 意图路由不同问题走不同的检索链路不是所有query都适合走同一条召回链路。有些问题适合直接查知识库文本有些问题需要查结构化数据有些问题应该走FAQ精确匹配。意图路由负责在检索前判断“这个问题该走哪条路”。最简单的实现是规则路由适用于场景边界清晰的情况。比如政务系统里用户问“办理护照需要什么材料”走政策文档通道“我的护照办到哪一步了”走业务系统查询通道。判别逻辑可以是几个关键词组合也可以是一个文本分类模型。更灵活的是用LLM做路由。给LLM一个函数列表让它判断当前query应该调用哪个工具。这是Agentic RAG的基础形态后面第7节还会展开。我自己在项目里的经验是路由策略的优先级高于召回策略。一次完全按意图走对通道的检索效果远好于在所有通道里做无差别搜索。所以做高级检索策略前先把“用户问题分为哪几类、每类走哪条链路”这件事想清楚比调任何参数都值。6. Graph RAG与Ontology RAG从“找相似文本”到“找实体关系”6.1 知识和实体在文本间的距离太远了传统向量检索解决的是“文本相似”的问题Graph RAG解决的是“实体相关”的问题。举个具体例子。知识库里有两条文本文档A“张伟就职于星辰科技有限公司担任研发部总监。”文档B“星辰科技有限公司在B轮融资中获得了2亿元投资投资方包括红杉资本。”用户问“张伟所在的公司最近获得了多少融资”。向量检索对这个问题通常会失败因为query里的“张伟”“融资”和文档B没有一个共同的表层词汇语义上也不够接近。但是如果在知识图谱里建模了“张伟—就职于—星辰科技”和“星辰科技—获得—2亿元融资”这两条边一条图查询就能拿到答案。知识图谱的本质是把实体和关系从非结构化文本中抽离出来变成显式的三元组头实体关系尾实体。这样一来多跳推理就变成在图上沿关系路径跳跃而不是靠向量空间里的隐式语义。Graph RAG的落地瓶颈在于知识图谱的构建和维护成本。当前实践中比较常用的做法是先用LLM或NER模型从文档里抽取实体和关系再入库。抽取质量直接决定图谱质量这一步需要评估和人工审核。一些小规模项目图谱里的实体控制在几千到几万个效果就已经能体现出来了。6.2 Ontology RAG把“实体关系”再往上抽象一层Ontology RAG是Graph RAG的进一步抽象。Graph RAG关注具体实体张伟、星辰科技Ontology RAG关注概念和类别人、公司、投资事件以及它们之间的关系“就职于”连接“人”和“公司”。构建好本体层之后图谱就变成了一棵层级清晰的概念树每个具体实体都是某个概念的实例。为什么要做这层抽象因为在很多专业领域用户问的是概念层面的问题不是实体层面的问题。比如问“研发类岗位的晋升周期一般是多久”知识库里散落着各个公司、各个岗位的具体信息。如果只做Graph RAG需要先在图上做大量实体遍历但有本体层的话可以直接定位到“研发岗”这个概念的属性查询路径大大缩短。在我做政务知识库的经验里Ontology RAG对两类问题帮助特别大“XX类事项的办理时限是多久”这种概念属性查询。“哪些情况需要提供XX材料”这种跨实体归纳查询。落地Ontology RAG前期要多花时间梳理领域本体把概念、属性、关系定义清楚。这块工作没有现成工具能替代需要业务专家和算法工程师一起完成。我当时做了一个很笨但有效的做法把知识库文档通读一遍列出所有高频名词和动词再让业务专家确认哪些是核心概念、概念之间什么关系最后才形成本体草案。整个过程大概花了两周。7. Agentic RAG让检索从“一次性查询”变成“多步推理”7.1 从单次检索到Agent规划传统RAG是“一次检索、一次生成”的线性链路Agentic RAG的核心差异是引入了Agent循环系统可以先检索一轮根据结果判断信息是否足够不够就继续检索、改写query、换一个数据源或者调用工具直到收集到足够信息再生成最终答案。为什么需要多步因为复杂问题的答案不是单个检索就能搞定的。比如“对比A方案和B方案在实施成本上的差异”需要先分别检索两个方案的介绍再找成本数据最后做对比聚合。单次检索只能拿到某一方面的信息。Agentic RAG的基本循环可以抽象成四步。观察当前状态包括已有的检索结果和用户原始问题规划下一步行动是再检索还是直接回答执行行动调用检索工具或API评估结果判断信息是否足以回答问题不足则回到第二步。这跟人类解决问题的流程其实是一样的。7.2 一个最小可用的Agentic RAG循环为了讲清楚原理我写了一个最小实现不用LangChain这类框架只用原生Python方便理解核心逻辑。class AgenticRAG: def __init__(self, retriever, llm, max_steps3): self.retriever retriever self.llm llm self.max_steps max_steps def run(self, query): collected_docs [] current_query query for step in range(self.max_steps): # 1. 检索 docs self.retriever.search(current_query, top_k5) collected_docs.extend(docs) # 2. 让LLM判断信息是否充足 context \n\n.join(collected_docs) judgement self.llm.judge(query, context) if judgement[enough]: # 3. 信息够了生成最终答案 return self.llm.answer(query, context) else: # 4. 信息不够让LLM生成下一步检索计划 current_query judgement[next_query] # 达到最大轮次用已收集的信息生成答案 context \n\n.join(collected_docs) return self.llm.answer(query, context)这个实现有一个明显的缺陷会在实际项目中暴露出来多轮检索收集到的doc会越来越多塞进context的成本越来越高也容易引入噪声。我在实际项目中做了一些调整——每轮检索后先做一次rerank把低质量的doc剔除掉collected_docs设置最大数量比如15个超出后按相关度裁剪给LLM的judge指令里明确说明“信息不足的时候要指出具体缺什么而不是笼统地说再搜一搜”。Agentic RAG的收益和成本都要清楚。收益是能解决多跳和对比类问题成本是延迟和token消耗显著增加。一次两轮的Agentic RAG全程可能耗时3到5秒token消耗是单轮的2到3倍。所以我的建议是先用路由把简单问题分流到普通RAG链路只有复杂问题才走Agentic RAG不要让所有流量都扛这个开销。8. RAG测评怎么做用指标说话而不是靠感觉调优8.1 检索层指标RecallK、MRR、NDCG怎么用很多RAG项目上线后效果不好一个关键原因是团队没有建立系统化的测评体系。我接触过的不少项目优化完全靠“调一个参数丢给业务方体验”这很难持续。检索层的评估是第一步目标很明确衡量检索系统能不能把正确答案捞回来。核心指标有三个RecallK前K个检索结果里出现正确doc的比例。RAG场景里这是最重要的指标因为如果检索捞不回来后面做再多都没用。MRRMean Reciprocal Rank衡量正确答案排在最前面的程度。正确doc排名越靠前MRR越高。NDCGNormalized Discounted Cumulative Gain能处理“多个相关doc且相关程度不同”的情况。适合答案分散在多个doc里的场景。这三个指标的选择逻辑很直接如果query只有一个正确答案看RecallK和MRR如果答案可能分布在多个doc中看NDCG。检索测评集的建设是关键。我维护测评集的方式是积累历史线上query包括真实用户问题和bad case对每条query标注知识库中的标准答案doc必须是一个个具体的doc id不能只有答案文本覆盖知识库各业务模块按业务分布抽样而不是均匀抽样。政务项目里我通常每两周迭代一次测评集跟着知识库更新节奏走。8.2 端到端评估只看生成答案质量会忽略什么检索层指标只能说明“结果里有没有正确答案”不能说明“大模型最终回答对不对”。所以还要有端到端评估。我常用的端到端评估框架是这样组织的评估维度评估内容常用方法答案正确性生成的答案是否准确、是否基于检索到的上下文LLM-as-Judge打分人工抽检忠实性答案是否有知识库依据是否幻觉检索到的上下文与答案逐句核对覆盖率问题的所有子方面是否都被回答LLM评分维度拆分相关性答案是否对用户有实际帮助人工打分、满意度抽样LLM-as-Judge的方式现在很普及但要注意避免一个坑Judge模型本身也是模型也有自己的偏好和误判。我的做法是let一个通用大模型当裁判给明确的评分标准同时每一条结果都输出评分理由方便人工抽检。重要的评估集一定保留人工抽检环节每周抽20到30条复核防止自动化评估跑偏。Bad case分析是我认为整个测评环节里价值最高的一步。我的固定套路是找出端到端评分低的query逆向追踪链路判断问题到底出在哪一层。是query没理解对检索没捞到正确doc还是捞到了但rerank把它排下去了又或者是上下文已经对了但大模型生成阶段答偏了。定位到具体环节后再针对性优化而不是盲目换模型、调参数。这一步做好了RAG优化的效率会翻倍。9. 常见问题与排查技巧实录9.1 高发问题速查表这一年多做了不少RAG相关项目整理了几个出现频率很高的线上问题附排查思路和处理方式。现象可能原因排查方法处理建议检索结果为空query太短/太模糊向量检索和BM25都捞不到检查token命中情况看ES/向量库的hit数加同义词扩展、增加查询改写、降低相似度阈值召回率低正确答案不在结果里embedding模型领域适应性差或知识库切分粒度过大用测试集对比不同embedding模型的Recall10检查切分后的doc长度分布换领域embedding模型调整切片策略召回结果多但top结果不相关efSearch参数太小或HNSW索引建得不好增大efSearch看Recall变化检查HNSW的M参数增大efSearch到100以上必要时重建索引rerank后效果反而变差重排窗口截断关键内容或batch size太小导致GPU利用不充分打印被截断的doc检查关键断言位置超长doc滑窗切片增大batch size生成答案有幻觉检索到的上下文本身矛盾或相关doc被排到后面检查喂给LLM的context具体内容看答案是否有依据rerank后做去重和矛盾过滤只保留高相关片段端到端延迟过高链路串行调用、LLM调用次数过多查看各阶段耗时占比召回并行化、Agentic RAG轮次限制、小模型先路由9.2 两个值得记一辈子的调优案例第一个案例是某政务知识库的检索召回率一直上不去换了三种embedding模型都没用。最后排查发现根因在知识库文档结构上——每一份政策文件都被切成固定512字的小块但政策的答复依据往往分散在文件不同章节单块文本的语义根本不完整。后续把切片策略改成按章节标题做结构化切分长章节内部再按语义边界切分子块同时保留章节级metadata供检索过滤召回率立刻上了一个台阶。这提醒了我检索效果的上限很大程度上由索引结构决定。第二个案例是重排模型上线后检索端指标Recall10没有变化但端到端答案质量明显提升用户满意度从68%涨到了82%。这其实印证了一件事检索端的Recall解决的是“有没有”重排解决的是“排得好不好”。大模型对上下文顺序和位置很敏感排在后面的正确答案就算存在也可能在生成长文本的过程中被忽略。所以评估RAG系统不能只看某一层指标要看到端到端效果的差异。9.3 上线后的监控与迭代最后说上线后的事。RAG系统上线不是终点知识库在更新、用户query在变化检索策略也要持续迭代。我的做法是给整个链路加日志埋点记录每个环节的关键指标query原文、检索命中的doc id列表、rerank后的分数顺序、最终喂给LLM的context片段、生成的答案。这些日志是后面做bad case分析和优化迭代最重要的数据资产。基于这些日志我每隔一段时间会做一次全链路效果复测把新query和旧query混合起来跑一遍测评观察各项指标的变化趋势。还要做漂移监控如果某类query的召回率突然下跌很可能意味着知识库更新时新文档的embedding质量出了问题或者切片失败的文档混进了索引。这些工作听起来繁琐但正是它们决定了RAG系统在真实业务里能不能稳定跑下去。我在多个RAG项目里最深的体感是高级检索策略没有银弹每一招都有适用场景和成本。多路召回解决了覆盖度问题rerank解决了排序质量问题query改写解决了用户表达和知识库词汇的鸿沟Graph RAG解决了多跳关系推理Agentic RAG解决了复杂任务的多步规划。做方案选型时不要追求一次上全而是基于bad case反推把当前最痛的问题先解决掉再逐步叠加其他策略。检索优化是一场持久战建立一套有效的评测体系比任何一个单独的策略都更有价值。

相关新闻