ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

RAG技术演进:从朴素检索增强到模块化架构的工程实践

RAG技术演进:从朴素检索增强到模块化架构的工程实践

这两年做 RAG 项目,被问得最多的一个问题不是“RAG 怎么实现”,而是“我已经接了大模型 API,为什么还要搞一套检索流程”。说实话,如果你只拿大模型当个聊天机器人,那确实用不到 RAG;可一旦想让它在自己的文档、自己的数据库、自己的知识库上给出可靠答案,RAG 就绕不过去。RAG(Retrieval-Augmented Generation,检索增强生成)解决的核心问题并不复杂:让模型在回答时先翻书,再说话。这篇文章是《RAG 技术演进一览》的上半部分,我会从最初代的朴素实现讲起,一直梳理到模块化 RAG,把“为什么要演进、每一步解决什么痛点、实践中踩过哪些坑”一条线讲清楚。适合正在做知识库问答、企业文档助手,或者准备从 0 搭建 RAG 系统的朋友。

1. 先搞清楚:RAG 到底在解决什么问题

1.1 大模型的“闭卷考试”困局:幻觉从哪来

把大模型理解成一个参加过“海量阅读”的学生,训练阶段它把互联网级别的文本压缩成参数,本质上是在闭卷备考。考试时你问它任何问题,它只能从脑子里存的参数里“回忆”答案。问题就出在这里:参数容量有限,细节越多的知识越容易记混,训练语料有截止时间,之后发生的事情它一概不知。

这就是幻觉的根源。不是模型变笨了,而是它被要求回答超出参数记忆能力的问题。你在企业场景里问“去年 Q3 华东区营收为什么下滑”,模型没见过这份内部财报,它只能根据训练语料里“类似公司”的规律编一个听起来合理的答案。这个现象在垂直领域尤其严重,因为公开语料里根本没有你的业务数据。

RAG 的思路很直接:把闭卷考试改成开卷考试。回答之前,先从外部知识库里检索相关的文档片段,把这些片段作为参考资料一起喂给模型,让它“看着资料作答”。模型不需要记住你的财务报表,它只需要具备阅读理解能力和推理能力,这两件事恰恰是它的强项。

1.2 微调为什么没成“万能药”

很多人第一个想到的替代方案是微调。既然模型不知道我的业务知识,那我用业务数据再训练一下不就行了?方向是对的,但性价比和效果都值得商榷。

微调的本质是把知识写进参数,这有几个绕不开的问题。数据量小的时候学不进去,数据量大了训练成本高,而且每次业务知识更新都要重新训练。更麻烦的是灾难性遗忘——学新知识的时候,模型可能把之前学会的通用能力丢掉。还有一个隐性问题:微调之后,你更难判断模型到底“知不知道”某个事实,出了问题也不好追溯。

对比维度微调RAG
知识更新重新训练,周期长更新知识库即可,分钟级
知识准确性依赖训练数据质量,易学错引用原文片段,可追溯来源
幻觉控制仍无法避免编造有上下文约束,明显缓解
成本算力成本高主要是检索和推理成本
可解释性黑盒,难定位错误原因可以输出引用的文档来源

这倒不是说微调和 RAG 互斥,实际项目里两者可以配合:微调负责“说话方式”,RAG 负责“事实内容”。但如果你只能在两者里选一个起步,优先做 RAG,它见效快、可控性强、出问题好排查。

1.3 为什么是“检索 + 生成”,而不是二选一

单独用搜索引擎,能找到相关文档,但输出的是链接列表,不是答案;单独用大模型,能生成流畅答案,但知识受限、容易编造。RAG 的精髓在于把这两者组合成一个管道:检索系统负责“划重点”,生成模型负责“写答案”。

这个组合还有一个隐性收益:可控性。你可以精确控制知识来源,删掉某份文档,模型就再也回答不出相关内容;你可以在答案里附上引用来源,用户能自己核实。这在企业合规场景里几乎是刚需。理解了这一点,后面看 RAG 的演进就会很清晰——所有改进都在围绕一个问题:让“划重点”更准,让“写答案”更稳。

2. 第一代 RAG:最朴素的“先查后答”

2.1 三阶段架构:索引、检索、生成

最早一批 RAG 系统的结构非常简单,就三步:建立索引、向量检索、拼接提示词生成答案。这个流程现在被追称为“朴素 RAG”,但它是理解后续所有演进的基石。

第一步是索引。把知识库里的文档切成块,每块文本用嵌入模型转成一个稠密向量,存进向量数据库。第二步是检索。用户提问时,把问题也转成向量,在向量库里用余弦相似度或内积找出最相似的几个文本块。第三步是生成。把问题、检索到的文本块、系统提示词拼在一起,交给大模型生成答案。

用伪代码表示就是这个样子:

def rag_answer(question, knowledge_base): # 1. 把问题编码成向量 query_vector = embed(question) # 2. 在向量库中检索最相似的 top-k 个文本块 top_chunks = vector_search(knowledge_base, query_vector, top_k=5) # 3. 将文本块放入上下文,交给大模型生成 context = "\n\n".join([chunk.text for chunk in top_chunks]) prompt = f"请仅根据以下资料回答问题,并在最后标注来源。\n\n资料:\n{context}\n\n问题:{question}" return llm_generate(prompt)

这套流程我当时第一次跑通的时候挺兴奋的,但冷静下来就会发现它到处是漏洞。后面几代 RAG 的演进,本质上都在给这三步打补丁。

2.2 嵌入与向量索引:需要理解的基础盘

很多人对“嵌入”这个概念发怵,其实可以把它想成“给文本拍一张语义照片”。嵌入模型把一段文本映射到一个高维向量空间,语义相近的文本在这个空间里的距离也近。“苹果”和“水果”的向量挨得近,“苹果”和“数据库”——如果你说的是那个科技公司——可能也不远。

这里有个基础工程经验:向量检索吃的是嵌入模型的质量,而不是向量数据库的 fancy 程度。你用弱一点的嵌入模型,装进再强的向量库,检索质量也不会好。主流的开源向量库已经足够支撑中小规模场景,关键更在于文本切块策略和查询重构,这两件事我后面专门讲。

还有一个新手的常见误解:向量数据库是全能的。实际上向量检索只负责“用语义找相似”,它完全不理解“文档里提到 A 但真正的答案在排除 A 之后的 B 这种约束条件”。这就是为什么索引阶段就要把元数据设计好,后面检索阶段才能做条件过滤。

2.3 朴素 RAG 的核心问题清单

把朴素的“先查后答”放到真实业务里,你会迅速撞上几堵墙。

第一堵墙是检索不准。用户的问题往往和文档里的表述不一致,比如用户问“今年利润怎么样”,文档里写的是“归属于母公司股东的净利润同比变化率”,这种 query 和文档之间的表达差异,靠纯向量检索很难召回。我在一个金融问答项目里实测过,直接向量检索的 Top-5 命中率只有六成左右,相当于每五次回答里就有两次找错了资料。

第二堵墙是上下文污染。检索回来的 5 个文本块里可能只有 2 块真正有用,另外 3 块是不相关信息,甚至还有相互矛盾的记录。你把它们一起塞给模型,模型很容易被带偏。

第三堵墙是上下文窗口限制。文档块越多、越长,成本越高、延迟越慢,超过窗口长度还会直接报错。

这几堵墙单靠朴素 RAG 是绕不过去的,于是有了“高级 RAG”这波演进:在检索前后同时下手,把“模糊检索”变成“精准供给”。

3. 高级 RAG:在检索前后打补丁

3.1 查询端的补救:让问题先“翻译”再检索

检索不准的第一大原因是 query 太粗糙。用户不会按照你索引里的语言习惯提问,所以最直接的办法是:在检索之前,先让大模型把用户问题改写成更适合检索的形式。

我常用的策略有三个。

第一个是“多查询扩展”。把一个模糊的大问题,拆成几个不同角度的小问题去分别检索,再把结果合并。比如“公司今年发展怎么样”,可以拆成“公司今年营收情况如何”“公司今年有哪些新业务”“公司今年的人员规模变化”,分别检索后把结果去重合并。实测下来比单次检索的召回率高不少,代价是多了几次查询调用,延迟会略微上升。

第二个是 HyDE(假设性文档嵌入)。思路是先让大模型针对问题生成一段“假设性回答”,然后用这段回答去检索,而不是用原始问题去检索。什么时候好用?当用户的问题过于简短、信息量太少的时候。比如用户就问“你怎么看”,这个问题本身没法直接检索,但大模型生成的假设回答里会包含“从行业趋势看……从公司基本面看……”这类具体内容,拿去检索就靠谱得多。

第三个是查询分解。适用于复杂多跳问题,比如“张三负责的项目被哪个部门接收了”,需要先查出“张三负责什么项目”,再查“这个项目现在归属哪个部门”。第一次检索的结果用于第二次检索的条件,链路式地推进。

简单记录一下我现在生产环境里的查询改写方案:先用规则判断问题类型,能用向量直接检索的就不交给大模型改写,只有规则命中“模糊、多跳、代词指代”等特征时才触发 LLM 改写。因为每次改写都是有成本有延迟的,规则优先能省不少 token。

3.2 索引端的补救:切块策略与元数据过滤

查询端改好了,回头修索引。同样的文档,切块方式不同,检索效果天差地别。我见过太多人直接把文档按固定长度 500 字一截,结果一句话被拦腰切断,语义残缺,检索质量自然上不去。

切块的核心原则是“按语义边界切”。优先用标题、段落、列表等结构标记作为切分点,实在没有结构标记时再用固定长度,但要做重叠窗口,让相邻块之间保留一定重合,避免关键信息正好落在切缝上。

切块策略的选择直接影响两个方向的效果:块越小,向量越聚焦,召回精度越高,但上下文碎片化,模型容易看不到全貌;块越大,上下文信息越全,但噪声也越多,检索精度下降。这是 RAG 系统第一个需要反复调优的平衡点。

切块方式优点适用场景
固定长度实现简单,速度稳定格式统一的清洗后文本
结构切分语义完整性高Markdown、HTML、合同条款
父子文档切块检索用小块,生成用大块长文档、复杂技术资料
滑动窗口重叠避免关键信息落在切缝无结构纯文本

另一个容易被忽略的索引端优化是元数据过滤。你的知识库肯定不是一堆无差别文档,可能有版本、部门、年份这些属性。像“2023 年员工考勤制度”这种问题,如果库里同时有 2020 版、2022 版、2023 版,先按元数据过滤掉过期版本,再向量检索,效果比单纯靠向量计算区分要好得多。毕竟向量的能力在于“语义相关”,不在于“版本新旧”。

3.3 融合检索与重排序:从粗筛到精排

不管查询端和索引端怎么优化,单一向量检索始终有一个结构性短板:它只看语义相似,不看关键词精确匹配。

典型的反例:文档里有一句话“该产品不计入营收,只作为赠品”,用户问“赠品会计入营收吗”,向量检索可能因为“产品、营收”这些词太像,把这句话排在很前面,但它恰恰是个否定句。这时候如果配合传统的稀疏检索“BM25”,精确匹配“赠品”“营收”这几个词,反而能搜到更准确的片段。

所以“混合检索”几乎成了高级 RAG 的标配:向量检索召回语义相近的内容,BM25 召回关键词精确命中的内容,两边结果用 RRF(Reciprocal Rank Fusion)合并。RRF 的原理非常简单——给两个结果列表里的文档按排名打分,排名越靠前分越高,最后汇总排序,实现起来也就十几行代码。

混合检索把候选集拉开到几十甚至上百条之后,下一步必须做“重排序”。向量检索是粗筛,它算的是“嵌入向量的近似相似度”;重排序是精排,它用更精细的模型逐对计算“问题和文档片段的相关程度”,把真正准的候选排到前面。

我建议的生产链路是:混合检索召回 Top 50,重排序模型精排取 Top 5 进上下文。别一上来就把 Top 50 全塞给大模型,又贵又容易上下文污染。重排序这步是 RAG 效果提升最立竿见影的一个环节,值得花最多时间选型和调参。

做完重排序,高级 RAG 还有一个细节:上下文压缩。检索回来的文本块里往往只有一两句和问题真正相关,其他都是噪音。压缩模块负责把文本块裁成与问题相关的片段,甚至把多个片段整理成精简的摘要。这一步既省 token,又能显著降低模型被无关信息带偏的概率,属于那种“做了之后抱怨明显变少”的优化。

4. 模块化 RAG:把流程拆成可插拔的零件

4.1 从“管道”到“模块”:设计哲学的变化

高级 RAG 把所有优化堆在一起,流程线变得很长:查询改写、混合检索、重排序、压缩、生成。这时候问题来了——每个节点都是定死的,想调整其中一个环节就得改整条链路的代码,不同需求之间反复拉扯。于是出现了模块化 RAG 的思路:把整条检索生成链路拆成独立模块,每个模块可以被替换、被调整、被重新编排。

这个思路在工程上完全不陌生,就是高内聚低耦合。检索模块不用管生成模块怎么实现,路由模块负责决定走哪条分支,生成模块只接收整理好的上下文。不同的业务场景变成不同的编排配置,而不是不同的代码分支。

我最早把系统改成模块化结构时,最大的感受是调试变得舒服了。以前哪儿出了问题,要在几百行链式代码里找;现在每个模块可以单独测试,单独返回日志。某个模块的效果不好,单独换掉就行。比如你想从“固定用向量检索”改成“按问题类型路由到不同检索策略”,只需要改路由模块的配置,不用动其他任何东西。

4.2 四个高频模块:路由、记忆、验证、反思

模块化 RAG 里最值得先拆出来的几个模块,我按使用频率排个序。

路由模块是入口大脑。它决定一个 query 该怎么处理:走向量检索还是走关键词检索,用不用重排序,甚至要不要走大模型生成还是直接返回模板答案。实现上既可以用小模型的分类能力,也可以用规则判断。典型的场景是:合同条款这种格式化的知识,用关键词检索更快更准;政策解读这种开放性的问题,走完整 RAG 链路。路由的目的就是别让所有问题都走最重的那条路。

记忆模块解决多轮对话的问题。用户追问一句“那它后来怎么样了”,“它”指代什么,如果每轮都独立检索,上下文根本连不上。记忆模块会把历史对话压缩成可检索的记忆片段,和当前问题一起作为检索输入。注意这里不要把整段历史全部塞进检索,对话越长噪声越多,合理的做法是只保留与当前问题相关的那两三轮历史。

验证模块负责审核上下文质量。检索回来的片段,到底和问题相关不相干、有没有互相矛盾,模型在生成前先检查一遍。如果验证不通过,就触发重新检索或换一个检索策略,而不是硬着头皮生成。这是减少幻觉非常有效的一道防御线。

反思模块更进一步,它不仅生成答案,还在生成之后自查。抽取答案里的关键陈述,回到上下文里逐条核对是否有据可依,发现没有依据的内容就修正或删除,甚至触发补充检索再重写。产品上的表现就是:答案更保守,但更可靠。

4.3 模块化 RAG 与 Agent 的边界在哪

走到模块化 RAG,你会发现它和 Agent(智能体)的边界越来越模糊。Agent 的特点是“能调用工具、能自己决策下一步做什么”,模块化 RAG 恰恰也是这个味道——路由是决策,验证是检查,反思是自我纠错。

我的看法是,两者不是替代关系,而是一条连续谱系。朴素的 RAG 是“给定流程跑通”;高级 RAG 是“在固定流程里加优化”;模块化 RAG 是“让流程可编排”;再往前走一步,就是 Agent 化的 RAG——模型动态决定检索几次、用哪个检索工具、发现资料不足时怎么补充信息。

在实际落地时,不必纠结名分。你需要关心的是:你的系统需要多大的自主决策空间。内部知识库问答,规则明确,模块化 RAG 足够;研究型助手、需要自主搜集资料的场景,才需要引入 Agent 的循环决策。先跑通模块化,再逐步放开自主性,是比较稳的路径。

5. 质量度量:RAG 系统究竟好不好

5.1 两阶段指标:检索质量与生成质量

很多团队做 RAG 只关心最后一句话“答得对不对”,这是最大的误区。RAG 是管道系统,最终效果同时受检索和生成两段影响。不把这两段指标拆开,你就永远定位不了瓶颈在哪儿。

检索段的指标负责回答“相关资料找没找全、找得准不准”。核心看三个:Recall@K,前 K 条结果里包含多少正确答案,衡量找全能力;Precision@K,前 K 条结果里有多少真正相关,衡量找准能力;MRR 和 NDCG,关注正确答案是不是排在前面,衡量排序质量。如果检索段 Recall 都不过关,那后面生成再怎么优化都是无米之炊。

生成段的指标负责回答“答案从资料里用的对不对”。核心看三个:忠实度,答案里的关键信息有没有忠实于检索到的上下文,有没有引入上下文之外的编造内容;答案相关度,生成结果是不是在正面回应用户的问题;上下文充分度,检索到的信息够不够支撑这个问题。这一段的评分常用大模型来打,但要控制好打分的 prompt,避免模型对长答案和短答案有系统性偏差。

5.2 评测集怎么构建:从 30 条种子问题开始

没有评测集,RAG 优化就是“感觉流”,今天觉得好一点,明天又觉得退步了,全凭印象。我在每个 RAG 项目里都会先花两天时间搭评测集,看似慢,后面省的时间远超这两天。

起步方式是找真实用户日志里的高频提问,整理出 30 到 50 条种子问题。先把种子问题按难度分层:单跳问题(一个事实就能回答)、多跳问题(需要聚合多个片段)、复杂约束问题(有时间、部门、版本等过滤条件)。然后针对每个问题标注三个东西:标准答案、检索相关的文档 ID、答案中必须出现的关键信息点。

评测集建好之后,每次改动索引策略、换嵌入模型、调检索参数,都跑一遍评测集,对比指标变化。我见过太多项目效果变差却不知道从哪天开始变差的,有了评测集,这类问题基本上不会再出现。

5.3 可以当作起点的经验阈值

评测指标定下来之后,得有参照系才知道好坏。这些是我的个人经验值,不同业务数据会有差异,但作为起步参照足够。

Top-5 的 Recall 尽量做到 0.8 以上才谈得上“检索可用”。如果低于 0.6,先别折腾生成端,回去调切块和查询改写。忠实度评分低于 0.7 说明生成端还有明显的上下文污染或者幻觉,优先检查重排序有没有把无关候选排进 Top 5。答案相关度低于 0.8 但忠实度很高,通常说明问题本身没被理解对,回去改查询改写。

还有一条容易踩的坑:不要只看平均分,要重点看失败案例聚集在哪个层级。如果多跳问题普遍丢分,说明索引和检索都还有问题;如果单跳简单问题也丢分,基础链路可能没建对,这是更严重的信号。

6. 工程实践中的避坑笔记

6.1 工具选型的几个务实建议

关于选型,先给结论:模型优先于数据库,质量优先于框架。

嵌入模型是整个检索链路的核心。同一个知识库,不同的开源嵌入模型,检索效果能差出 20 个百分点的 Recall。中文场景尤其要选在中文语料上有针对训练的模型,直接用通用英文模型处理中文,效果惨不忍睹。选型时拿自己的业务数据去评测,不要只信榜单上的评测分数。

向量数据库的选型要看规模。中小知识库(百万级向量以内)选轻量级开源库就够,后端用文件存储都行;到了千万级向量、高并发在线查询,再考虑部署企业级向量数据库集群。刚开始就用集群,纯属给自己增加运维负担。

最后说框架。框架能简化代码,但不要被框架绑架。我建议先理解每个环节的原理,再用框架搭系统。不然出了问题,你连是框架的 bug 还是自己配置的 bug 都分不清。

6.2 chunk 大小与 top_k 的调参经验

chunk 大小没有普适最优解,但有几个可复用的起点。中文场景下,500 字左右的块是各方面比较均衡的起点;技术文档、合同这类结构完整的,优先按章节切,而不是按字数切。句子特别长、信息密度特别高的场景,比如招股书,可以适当缩小 chunk 到 200 到 300 字。

重叠窗口建议设置在整个 chunk 长度的 10% 到 20%。太大浪费 token,太小兜不住切缝处的信息。top_k 的经验值是:没有重排序时用 3 到 5,有重排序时粗召回 50 到 100、精排后取 3 到 5。别迷信大 top_k,上下文越长,噪声越大,模型被带偏的概率越高。

参数调优别同时动多个旋钮。一次只调一个变量,跑评测集看指标变化,才能定位谁在起作用。我见过有人同一天改了切块策略、换了嵌入模型、加了重排序,效果变好了也说不清是谁的功劳,后面想继续优化都不知道从哪下手。

6.3 线上问题排查清单

症状排查思路常见解法
问题简单但检索不到资料先看 query 改写是否符合文档用语补查询改写规则或改用 HyDE
检索结果相关但答案仍答错看忠实度得分,判断生成是否脱轨加强 prompt 约束,加入引用验证
答案含有上下文没有的信息检测生成段幻觉,检查压缩模块引入验证模块,对关键陈述复查
多轮对话答非所问检查记忆模块是否带入历史噪声只保留与当前问题相关的历史轮次
相似问题效果不稳定检查切块边界是否偏移改结构化切块,给文档加元数据过滤
响应延迟高定位耗时集中在检索还是生成粗召回缩小候选集,或重排序模型换轻量

这个清单本身也是演化出来的。我最早做 RAG 的时候,遇到线上问题就手动查日志,费时费力。后来把每次失败的案例沉淀成规则,逐步形成了这套最快的排查路径,分享出来,希望能帮你少走一段时间弯路。

我个人的体会是,RAG 不是一个“搭好就完事”的系统,而是一个需要根据坏例不断调整的系统。每次觉得效果稳定了,总能在真实用户 query 里发现新的召回盲区。这篇先把从朴素到模块化的主线路讲完;下一篇文章我会把更前沿的方向,比如 GraphRAG、长文档 RAG、Agent 化检索,单独拆开做一次深度分享,如果你正在做相关项目,可以先留意这部分内容。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进