做RAG也有几年了,从最开始把文档切一切塞进向量库,到后来被召回质量折磨,再到重新审视整个架构,这个过程最大的感受是:RAG不是一个静态的技术,它一直在演变。今天这篇属于“RAG技术演进一览”系列的中篇,重点聊RAG从“能用”走向“好用”中间那段关键变化——检索从粗糙变得精细,生成从堆料变得克制,流程从固定管线变成可编排的模块。你如果已经跑通了一个基础RAG demo,但发现效果不稳定、召回质量时好时坏、答案经常不忠实,这篇文章大概率值得你读一遍。这篇内容不会只讲概念,我会把演进过程中的关键技术点、为什么这么改、实际落地时怎么选,以及我踩过的坑都铺开来说。阅读时不需要你已经读过上篇,我会在关键位置补一句上下文。建议搭配你手上的RAG项目边看边对照。1. 从Naive到Advanced再到Modular:RAG演进到底动了哪些东西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_version=2025的chunk,或时间范围约束,再进行向量召回。层级的过滤和向量检索组合,能大幅减少无关内容,这是性价比很高的一步。另一个有效的索引策略是父文档检索:先把文档切成比较细的块用于检索,但当召回某个细块后,不直接把细块塞给模型,而是想办法拿到它所属的更大章节或完整文档。因为细块匹配度高,但语义不完整;父文档能提供足够的上下文。实现时可以用一个“子块ID到父块ID”的映射表,检索定位到子块后,通过映射取父块,再按需要裁剪后放入Prompt。这个方法在处理技术文档时最好用,算是花小钱办大事。2.3 混合检索与融合排序:向量不是万能钥匙纯向量检索对精确匹配很不友好。产品编号“KT-200”,用户写成“KT200”,向量可能找到一堆类似但完全不对的东西;代码报错日志里的特定函数名,靠语义相似度很难精确命中。BM25这类稀疏检索方法在这种场景反而有优势,它能严格匹配关键词,在专有名词、型号、固件版本这些场景下表现稳定。所以主流做法是混合检索:把BM25和向量检索并行执行,各取回一批候选,再用RRF(Reciprocal Rank Fusion)或者加权分数把它们合并排序。RRF的原理很简单,对每个结果计算1/(k+rank),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_k=3) ctx = compress(query, chunks) return generate(query, ctx, no_retrieval=False) 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_k=20) kw_hits = bm25_search(sq, top_k=20) fused = rrf_fusion(vec_hits, kw_hits) candidates.extend(fused[:10]) reranked = rerank(query, candidates, top_k=5, min_score=0.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才会真正从“能跑”变成“好用”。如果后续有精力,可以再往多模态检索、长上下文重排、评测数据自动生成这些方向探索,那已经属于这一系列后半程的内容了。
做RAG也有几年了,从最开始把文档切一切塞进向量库,到后来被召回质量折磨,再到重新审视整个架构,这个过程最大的感受是:RAG不是一个静态的技术,它一直在演变。今天这篇属于“RAG技术演进一览”系列的中篇,重点聊RAG从“能用”走向“好用”中间那段关键变化——检索从粗糙变得精细,生成从堆料变得克制,流程从固定管线变成可编排的模块。你如果已经跑通了一个基础RAG demo,但发现效果不稳定、召回质量时好时坏、答案经常不忠实,这篇文章大概率值得你读一遍。这篇内容不会只讲概念,我会把演进过程中的关键技术点、为什么这么改、实际落地时怎么选,以及我踩过的坑都铺开来说。阅读时不需要你已经读过上篇,我会在关键位置补一句上下文。建议搭配你手上的RAG项目边看边对照。1. 从Naive到Advanced再到Modular:RAG演进到底动了哪些东西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_version=2025的chunk,或时间范围约束,再进行向量召回。层级的过滤和向量检索组合,能大幅减少无关内容,这是性价比很高的一步。另一个有效的索引策略是父文档检索:先把文档切成比较细的块用于检索,但当召回某个细块后,不直接把细块塞给模型,而是想办法拿到它所属的更大章节或完整文档。因为细块匹配度高,但语义不完整;父文档能提供足够的上下文。实现时可以用一个“子块ID到父块ID”的映射表,检索定位到子块后,通过映射取父块,再按需要裁剪后放入Prompt。这个方法在处理技术文档时最好用,算是花小钱办大事。2.3 混合检索与融合排序:向量不是万能钥匙纯向量检索对精确匹配很不友好。产品编号“KT-200”,用户写成“KT200”,向量可能找到一堆类似但完全不对的东西;代码报错日志里的特定函数名,靠语义相似度很难精确命中。BM25这类稀疏检索方法在这种场景反而有优势,它能严格匹配关键词,在专有名词、型号、固件版本这些场景下表现稳定。所以主流做法是混合检索:把BM25和向量检索并行执行,各取回一批候选,再用RRF(Reciprocal Rank Fusion)或者加权分数把它们合并排序。RRF的原理很简单,对每个结果计算1/(k+rank),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_k=3) ctx = compress(query, chunks) return generate(query, ctx, no_retrieval=False) 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_k=20) kw_hits = bm25_search(sq, top_k=20) fused = rrf_fusion(vec_hits, kw_hits) candidates.extend(fused[:10]) reranked = rerank(query, candidates, top_k=5, min_score=0.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才会真正从“能跑”变成“好用”。如果后续有精力,可以再往多模态检索、长上下文重排、评测数据自动生成这些方向探索,那已经属于这一系列后半程的内容了。