
做RAG也有几年了从最开始把文档切一切塞进向量库到后来被召回质量折磨再到重新审视整个架构这个过程最大的感受是RAG不是一个静态的技术它一直在演变。今天这篇属于“RAG技术演进一览”系列的中篇重点聊RAG从“能用”走向“好用”中间那段关键变化——检索从粗糙变得精细生成从堆料变得克制流程从固定管线变成可编排的模块。你如果已经跑通了一个基础RAG demo但发现效果不稳定、召回质量时好时坏、答案经常不忠实这篇文章大概率值得你读一遍。这篇内容不会只讲概念我会把演进过程中的关键技术点、为什么这么改、实际落地时怎么选以及我踩过的坑都铺开来说。阅读时不需要你已经读过上篇我会在关键位置补一句上下文。建议搭配你手上的RAG项目边看边对照。1. 从Naive到Advanced再到ModularRAG演进到底动了哪些东西1.1 Naive RAG的“简单粗暴”和它的代价最早大家做的RAG流程基本一致离线把文档按固定字数切成小块embedding后塞进向量库线上拿到用户问题也做一次embedding在向量库里找最相似的TopK片段拼进Prompt里丢给大模型生成答案。这个流程看起来通顺实际用起来问题非常多。固定切块导致很多语义被拦腰截断一个完整的技术方案被拆到三个chunk里检索时每个chunk只命中一小部分答案自然残缺。更麻烦的是TopK检索只关心单片段与问题的相似度不关心片段之间的顺序和逻辑关系。比如一篇操作手册步骤一在chunk_5步骤二在chunk_12模型生成时虽然都看到了但顺序已经乱掉生成的流程会颠倒。还有一类是高噪声场景库里存了2000个内容相近的售后工单向量检索返回的TopK里可能有七八个相似但并不能回答问题最终答案被这些无关信息带偏甚至出现幻觉。这些问题在demo阶段很难暴露因为演示数据往往只有几十个文档、几十条问题。一旦放到生产环境知识库扩大到几万条甚至几十万条召回准确率直线下降。我见过一个内部知识库问答项目基础RAG上线后准确率只有52%用户问的问题基本是“发票报销流程是什么”这种高频业务词向量检索给出的结果经常是另一份合同里的发票条款。问题根源就在于“能检索到”不等于“检索得对”。1.2 是评测压力催生了演进不是空想出来的为什么后来RAG会演进得这么复杂很大一部分原因是评估方式变了。早期大家只看“生成的答案像不像人话”后来开始系统化地用忠实度faithfulness、答案相关性answer relevance、上下文相关性context relevance这些指标来度量RAG的质量。忠实度指的是模型回答是否严格基于检索到的上下文有没有自己脑补水话答案相关性是回答有没有问到点子上上下文相关性是检索回来的片段到底有没有用。这些指标一量化问题就摆在明面上。你会发现Naive RAG的上下文相关性很低TopK里经常混着一半不相关的内容忠实度也不稳定模型一遇到上下文里的矛盾信息就会自行选择一边开始发挥。评测框架还能让你对检索器单独打分把问题和标准答案预先准备好就能算出召回率、Hit Rate。有了这些数字优化的方向就清晰了先让检索结果更纯再让生成更“听话”。1.3 Modular RAG把固定流水线变成乐高积木Naive RAG被定义为“检索生成”两步走后来研究者把RAG拆成一个可以动态编排的模块集查询分析、查询改写、混合检索、重排、上下文压缩、生成、评估这些模块按需组合。于是出现了Modular RAG的概念。它不限定流程必须走哪几个模块你可以根据具体任务挑选知识库里的问题类型偏事实型就保留检索重排压缩如果是需要综合多个来源的问题就加子问题分解和多路召回。这种模块化的意义在于RAG从一道固定工序变成了可灵活调整的流水线。实际受益最大的是业务侧产品经理会告诉你“我们有些问题不需要检索直接让模型答就行”或“有些问题需要查两张表再做汇总”模块化RAG可以针对不同查询路由到不同路径而不是所有请求都走同一个又慢又重的链路。也为后面引入外部工具、记忆模块、多模态模块留了接口。2. 检索侧的进阶优化让“找到”变成“找对”2.1 查询改写很多问题本身就没说清楚用户不会永远用规范的话提问。实际系统里遇到最多的情况是问题太短或者依赖对话历史。比如用户先问“咱们的报销额度是多少”再补一句“那住宿标准呢”第二句里没有“报销”也没有“额度”直接拿去向量检索结果完全跑偏。这时候需要做查询改写——把对话历史里缺失的信息补全把口语转成更适合检索的表达。还有一种有效操作叫HyDE即让大模型先根据用户问题生成一个“假设性答案文档”用这个文档去做检索再拿实际检索到的上下文生成最终答案。原理是真实答案往往比问题更像知识库里的原文用“想象中的答案”去匹配更容易检索到语义相关的材料。我实测下来HyDE对描述模糊但答案比较固定的问题提升明显比如“出差住宿超标了怎么处理”生成一个假设文档后召回成功率高出不少。代价是多了一次大模型调用延迟增加不是每个请求都值得用。查询改写还有一个进阶玩法是子问题分解把一个复杂问题拆成几个相对独立的子问题分别检索再汇总。比如“对比A方案和B方案的优缺点”拆成“A方案优点”“A方案缺点”“B方案优点”“B方案缺点”四个子查询每个子查询单独检索最后让模型综合回答。这一招能明显提高多实体问题的覆盖率但要注意子问题拆得太多会导致最后上下文碎片化生成时反而拼不回来需要控制数量。2.2 索引策略从固定切块到按需组织基础RAG最常见的坑是不分青红皂白地按512或1024字符切块。更好的做法是按语义边界切比如使用中文句号、换行、Markdown标题、代码块标识作为天然分割点。针对不同内容类型要准备不同切块策略合同条款按“条款”切故障报告按“现象/原因/处理措施”切产品帮助文档按“标题层级”切。切块后还可以给每个块打上元数据标签比如来源文件、章节编号、更新时间、适用产品线。元数据在检索时的价值常被忽略。一个知识库里混着2022和2025年的产品政策用户问“最新的退换货规则”如果只做向量检索模型很可能混着旧政策回答。正确做法是先对元数据做过滤比如只检索policy_version2025的chunk或时间范围约束再进行向量召回。层级的过滤和向量检索组合能大幅减少无关内容这是性价比很高的一步。另一个有效的索引策略是父文档检索先把文档切成比较细的块用于检索但当召回某个细块后不直接把细块塞给模型而是想办法拿到它所属的更大章节或完整文档。因为细块匹配度高但语义不完整父文档能提供足够的上下文。实现时可以用一个“子块ID到父块ID”的映射表检索定位到子块后通过映射取父块再按需要裁剪后放入Prompt。这个方法在处理技术文档时最好用算是花小钱办大事。2.3 混合检索与融合排序向量不是万能钥匙纯向量检索对精确匹配很不友好。产品编号“KT-200”用户写成“KT200”向量可能找到一堆类似但完全不对的东西代码报错日志里的特定函数名靠语义相似度很难精确命中。BM25这类稀疏检索方法在这种场景反而有优势它能严格匹配关键词在专有名词、型号、固件版本这些场景下表现稳定。所以主流做法是混合检索把BM25和向量检索并行执行各取回一批候选再用RRFReciprocal Rank Fusion或者加权分数把它们合并排序。RRF的原理很简单对每个结果计算1/(krank)k一般取60把两路结果的倒数排名相加最终分数高的排前面。它不要求两路分数在同一量纲实现起来非常稳适合混合检索的初步融合。想要更精细可以用一个可学习的小模型对不同得分加权但大多数场景下RRF已经足够。我这里有一个真实案例某售后知识库纯向量检索的Hit Rate约0.64加上BM25后Hit Rate到0.78再用RRF融合后稳定在0.82。提升主要来自代码片段和产品型号的召回。但同时也暴露了新问题融合后TopK里混入了更多“关键词匹配但语义不对”的硬噪声这时候就该上重排序了。3. 生成侧的精细化上下文不是越多越好3.1 重排序把TopK里的正确答案捞出来如果你只做了一步检索就生成效果基本全看第一步的运气。向量检索给到的是“宽泛语义相关”的候选里面经常混杂大量“看着像但答非所问”的内容。重排序就是对这个候选集再做一次精排用更强的模型把最相关的内容提到最前面把无关内容压到后面甚至过滤掉。实操上我一般用Cross-Encoder类型的rerank模型把问题和文档片段拼接后走一个完整的Transformer编码器输出相关性分数。它比BiEncoder也就是双塔向量模型更准因为它让问题和片段在交互层直接对比而不是靠两套向量各自的语义近似。代价是计算成本高所以要控制重排序的候选数量一般对前20个到前50个候选重排排序后取Top3到Top5进入Prompt。这里有个值得注意的细节重排后的分数不能只用来排序还可以设定一个最小阈值。比如相关性分数低于0.2的chunk直接丢弃即使排在TopK里也不要。我见过不少项目只重排不筛分结果重排说A比B靠前但A的绝对分数也很低照样把垃圾塞给模型。设定阈值比堆参数更能提升忠实度。另外语言模型和重排模型的匹配度也很关键如果文档是中文就要用中文训练的重排模型否则分数失真很严重。3.2 上下文压缩别把Prompt塞成一个购物大礼包向量检索TopK取了5个chunk每个chunk平均800字加起来就有4000字。再混合检索、子问题分解最终放进Prompt的上下文可能轻松超过一万字。很多人以为大模型能吃下长上下文就无所谓实际上一旦上下文过长模型会迷失在中间复杂信息稍微绕一点就抓不住重点或者被某个角落的噪声带跑。这也是我坚持要在生成前做压缩的原因。压缩不是简单截断而是去掉与问题无关的冗余内容保留能帮助回答问题的关键句子。常见做法是用轻量级语言模型对每个文档片段打分抽取与问题相关的句子形成精简摘要也可以规则化一点按句子计算与问题的相似度只保留超过阈值且不重复的句子。LangChain里的ContextualCompressionRetriever就是这类思路的封装很多人只当它是一个可选组件实际上在知识库KPI优化里它往往是提升忠实度最直接的一环。还有一类压缩叫“选择性地保留格式”如果检索到的是包含代码的开发文档不能把代码块也给过滤了那可能导致关键答案被扔掉。正确做法是根据内容类型做差异化压缩比如文本段落做语义压缩代码块保留完整并附上文件名。压缩策略还是要跟后面的生成指令对齐否则压缩完的信息模型识别不了生成照样四处乱飘。3.3 生成策略的克制少讲故事多贴原文生成侧最大的问题不是模型笨而是模型太“聪明”。上下文里有一点点线索它就能顺着脑补出完整答案。所以在生成阶段最重要的改造是明确告诉模型只能使用给定的上下文禁止依赖内部知识如果上下文不够直接回答说不知道。这句话写在系统Prompt里效果立竿见影。也可以要求模型在答案末尾给出引用来源像“相关依据产品手册第3章2025版第12页”。让生成和检索形成闭环模型必须回答“为什么”验证它有没有真正使用检索内容。刚开始会遇到模型编造引用编号的情况可以把引用编号限制为上下文中实际出现的文档ID列表并要求模型只能从这些ID里选。这个小约束能大大减少伪造来源。我一直认为生成侧优化不是换一个更大更强的模型而是把生成规则、上下文结构和事实边界三件事都处理好。很多项目用了最贵的大模型效果却和小模型差不多基本就是因为在上下文里塞了太多矛盾信息生成阶段又没有任何约束。先把上下文清洗好再谈模型升级顺序不能反。4. 模块化RAG落地一套可以抄作业的流水线4.1 模块怎么拆为什么这么拆模块化RAG的关键不是“多了几个模块”而是明确了每个模块的输入输出和可替换性。我从实际项目里习惯拆成以下几块模块职责输入输出查询解析器判断问题类型做意图识别与必要改写用户原问题对话历史标准化的检索查询语句召回器执行向量检索、关键词检索或其他检索标准查询候选文档列表融合器合并多路召回结果做RRF或加权多路候选列表统一排序的候选集重排器精排候选设定相关性阈值统一候选集TopK且过滤后的最终上下文上下文压缩器压缩精简保留关键信息最终上下文精炼上下文生成器基于精炼上下文生成带引用回答精炼上下文Prompt模板答案与引用模块化之后每个模块可以单独升级而不影响其他模块。比如我觉得当前重排器效果不好直接换一个模型重新评估不需要动检索和生成代码。如果某类查询根本不需要检索查询解析器可以直接把请求路由到“只走生成器”的快速通道节省了大部分延迟成本。4.2 一个可参考的RAG流水线配置示例这是我搭建过的一条通用进阶RAG流水线较适合知识库问答场景。配置思路是查询先过一句轻量路由简单问题直接检索生成复杂问题则走完整链路。下面的伪代码展示了核心调度逻辑def rag_pipeline(query, history): query_type route(query) # 简单|复杂|多跳 if query_type 简单: chunks vector_search(query, top_k3) ctx compress(query, chunks) return generate(query, ctx, no_retrievalFalse) q rewrite_with_history(query, history) # 补齐省略、人称指代 sub_queries decompose(q) if query_type multi_hop else [q] candidates [] for sq in sub_queries: vec_hits vector_search(sq, top_k20) kw_hits bm25_search(sq, top_k20) fused rrf_fusion(vec_hits, kw_hits) candidates.extend(fused[:10]) reranked rerank(query, candidates, top_k5, min_score0.2) expanded fetch_parent_chunks(reranked) # 父文档补充上下文 compressed compress_with_type(query, expanded) # 区分文本与代码 return generate_with_citation(query, compressed)这个例子不是为了照抄而是想说明真正流程化的RAG什么样。你不需要一上来就把所有模块全加上可以先只加一个重排器再根据效果决定要不要加HyDE、子问题拆解或父文档检索。每加一个模块都要有对应的评估项否则你都不知道是哪一个模块变好了。4.3 延迟预算精检和快速路径必须共存进阶RAG的最大代价就是延迟。查询改写多一次大模型调用HyDE又多一次子问题分解还可能拆出三四个查询重排器本身又是一次推理每个模块的延迟叠加最终可能从原本的两秒涨到八秒。如果产品面向内部员工八秒勉强能忍如果面向终端用户那是灾难。我的调整思路是给系统配两条执行路径一条“实时路径”处理大多数简单检索问题走轻量检索重排Top5目标延迟1.5秒以内。另一条“深度路径”处理复杂推理或多跳问题可以接受3到5秒的延迟允许使用子问题分解、HyDE和多路融合。关键是如何路由可以基于问题长度、关键词数量、是否包含比较词、上一轮是否召回失败等信号做规则判断如果规则判断不出来就先用实时路径当实时路径生成置信度低时再转入深度路径。这种“先快后慢错了再重来”的设计在实际生产里比所有请求都强行走完整链路要稳得多。5. 实操记录一次从Naive到Advanced的升级复盘5.1 先做诊断再谈优化我在优化一个内部知识库RAG时先做了一套“体检”把问题分成六类检索缺失该有的东西没找回来、检索噪声找回来一堆没用的、上下文超长模型抓不住重点、答案不忠实自己瞎编、引用错误给了不存在的来源、交互生硬没有上下文感知的追问。每一类问题都用对应指标去定位检索缺失看递归召回率、Hit Rate检索噪声看重排前后的相关性分数分布答案不忠实用忠实度人工抽评。这个诊断过程花了两周比写代码时间长但特别值。因为体检后你会很清楚瓶颈到底在哪个环节而不是凭感觉把检索、模型、Prompt全部重写一遍。我见过太多项目一上来就换大模型效果没变好钱花了不少原因就是病根在检索侧压根不在生成侧。5.2 推荐的优化顺序先召回后精排再约束经过几次实战我总结出一个比较稳的升级顺序先补召回解决“检不到”的问题。可以加查询改写、HyDE、子问题分解也可以加BM25混合检索目标是让Hit Rate从0.6拉到0.85以上。再提精度解决“检不准”的问题。加入重排器设置最小相关性阈值切割掉明显不相关的内容。这一步后上下文相关性会有明显改善。然后控长度解决上下文碎片化的问题。做父文档检索和上下文压缩让喂给模型的材料既有完整语义又没冗余。最后管输出用系统Prompt约束事实边界和引用格式确保模型所有输出都能溯源。这个顺序的另一个好处是每一步都可以跑A/B测试单独验证效果。如果你一次加了四个优化点最后发现效果好了你也说不清是谁贡献的后面做维护会很被动。5.3 我们踩过的一些坑优化过程中印象比较深的坑有好几个写出来给大家避一避。第一个坑是HyDE“无脑上”。当时为了让召回率再涨一点把所有查询都先让大模型生成假设文档结果延迟翻了将近一倍Hit Rate却只涨了2个点甚至有些简单查询因为假设文档生成得奇奇怪怪命中率反降。后来改成仅对描述模糊或已经有一次检索失败的重试查询启用HyDE才让这个模块真正发挥价值。第二个坑是重排模型的语言错配。当时用了某个英文重排模型来给中文知识库排序检索出来的候选里明明有正确答案排序分数却被压得很低造成大量漏排。换用中文重排模型后相关性分布合理了很多。做RAG时各个模块的语言能力必须跟知识库语言匹配这个很基础但真的容易忽略。第三个坑是压缩器把代码块给“压”没了。技术文档里的代码经常包含关键变量名和报错信息压缩模型按句子抽取时把这些代码片段当噪声过滤掉了结果生成的修复建议全是空话。后来对代码块做了硬保护检测到代码块就直接保留最多裁剪行号。第四个坑是元数据过滤和嵌入模型之间没对齐。比如文档中日期字段存成字符串“2025-03-15”但用户问题里用的是“今年三月”过滤器就没有正确解析。后来统一把时间语义归一化成“公告日期: 2025-03”标签过滤才生效。这个坑提醒我们元数据过滤不等于硬编码替换还需要考虑用户表达差异。为了快速排查问题我这里整理了一份对比速查表方便你也对照自己系统现象可能原因优先排查点检索结果大量跑偏查询太短/向量模型弱查询改写、HyDE、混合检索正确答案没召回切块切断了关键语义语义分块、父文档检索TopK有正确答案但回答仍错上下文噪声过多/模型约束不够重排阈值过滤、上下文压缩回答总是很空泛上下文压缩过度或Prompt未指定语气保留代码/示例、调整Prompt模板引用编号对不上生成阶段未限制引用ID集合限制模型只能引用给定ID延迟太高模块叠加太多设置快速路径/深度路径路由RAG演进到中后期最大的变化其实不只是技术变复杂了而是优化思维从“加功能”变成了“可观测、可度量的流水线”。每一步都要能解释清楚解决了哪个指标而不是为了秀技术堆模块。我个人实际操作下来的体会是所有模块都有适用边界不存在一个放之四海皆准的组合。建议你也别急着把文章里提到的每一个技术都装上而是先从自己的业务数据里找最大瓶颈按“召回—精排—压缩—生成约束”的顺序一步步做升级每动一步就记一次评估指标这样你的RAG才会真正从“能跑”变成“好用”。如果后续有精力可以再往多模态检索、长上下文重排、评测数据自动生成这些方向探索那已经属于这一系列后半程的内容了。