做RAG(Retrieval-Augmented Generation)一年多了,坦白说,第一次听到这个词的时候我也觉得这就是个"缝合怪"——检索加生成,听起来没什么玄机。直到我把一套基于大模型的问答系统真正推到生产环境,才发现这里面坑比想象中多得多。今天这篇就把我从原理到工程落地的完整经历梳理一遍,包括我怎么设计数据流水线、怎么调召回质量、怎么评估效果,以及踩过的那些说得出口和说不出口的坑。无论你是在做企业知识库问答、私有文档分析还是智能客服,这套方案应该都能给你一些参考。
1. RAG是什么:先搞懂它在解决什么问题
1.1 大模型的两个"硬伤":幻觉与知识过期
先聊个基础但绕不开的话题:大模型为什么需要RAG?直接原因就是大模型有两个先天缺陷——幻觉和知识过期。
幻觉这个词现在很多人都在说,但真正被它坑过的人才有体感。我最初把一个客服问答机器人接到业务线,用户问"某某产品支不支持离线支付",模型张口就答"支持",实际上这个功能早就在半年前下架了。你问它为什么这么答,它自己也不知道,它只是基于训练数据里"产品通常支持多种支付方式"这种统计规律在生成答案。这在开放域闲聊里无伤大雅,但在企业场景里就是事故。
另一个问题更隐蔽:知识过期。一个大模型从训练完成那一刻起,它对世界的认知就冻结了。无论你用的是哪个版本的模型,它都不可能知道昨天新发布的政策文件、前天更新的产品参数、上周刚离职的员工联系方式。模型训练一次动辄几个月,你不可能每次数据变化都重新训练一遍。
这两个问题叠加起来,就催生了一个朴素的思路:既然模型记性不好,那我能不能在它回答问题之前,先把相关资料找出来塞给它?RAG的核心逻辑就是这么简单——不是让模型背课文,而是开卷考试。
1.2 RAG的核心思路:拿证据说话
开卷考试这个类比我觉得是最贴近RAG本质的。想象一下:你让一个学生闭卷回答"2026年最新政策有哪些变动",他只能凭记忆东拼西凑,大概率答错;但你给他一堆最新的政策文件让他翻着查,他答对的概率就高得多。RAG做的事情,就是给大模型一个可以随时翻阅的资料库。
具体到技术上,RAG分两个阶段:索引阶段把外部文档切成小段、向量化、存入数据库;查询阶段把用户问题同样向量化,去数据库里检索最相似的文档片段,然后把"问题 + 检索到的文档片段"组装成Prompt喂给大模型,让它基于这些材料生成回答。
这里有一个非常关键的设计理念:模型输出的内容必须有据可依。你在Prompt里明确告诉模型"只根据以下资料回答,资料里没有的就说不知道",模型就算想编,它也没有多少发挥空间。当然,后面我会说到,这个"不编"的约束在实际工程里并没有那么牢固,还需要做一系列加固。
1.3 为什么不用微调:成本与效果的博弈
每次聊RAG,总有人问:这些事用微调(Fine-tuning)不是也能干吗?我的回答是:能干,但性价比完全不是一回事。
微调的本质是改变模型的权重,让模型"记住"新的知识。但这有几个问题。第一,成本高。微调需要准备大量的高质量标注数据,需要GPU训练资源,每次业务数据更新你都得重新训练。我见过一个团队为了把几十页产品手册"喂"进模型,花了三周时间整理训练集,最后效果还不稳定。第二,知识是静态的。模型一旦微调完,知识就固化在参数里了。你改一个产品价格,就得重新微调一次,这在业务快速迭代的场景里根本不可行。
RAG的优势在于知识外置。文档放在外部数据库里,更新文档就是更新知识,不需要动模型本身。我维护过一个知识库,里面有两千多篇技术文档,每周都有新增。按微调的思路,我每周末都得跑一次训练任务;用了RAG之后,新文档解析入库就完事了,几分钟的事。
当然,微调并非一无是处。它适合改变模型的"行为方式",比如让它把回答语气调整得更正式、学会固定格式的输出模板。但如果是纯粹注入知识,RAG是更优解。实践中更合理的做法是两者结合:用RAG解决"模型不知道"的问题,用微调解决"模型不会说"的问题。
2. RAG系统拆解:一条完整的数据流水线
2.1 离线建库阶段:从原始文档到检索条目
RAG工程落地第一步,是把一堆乱七八糟的文档变成可供检索的数据库。这个过程不是"读文档->存起来"这么简单,背后有一条完整的处理链:加载、解析、清洗、切分、向量化、写入存储。
加载与解析是最容易翻车的一环。常见的文档格式有PDF、Word、Markdown、HTML、PPT,每一种都有各自的坑。PDF看着简单,但里面嵌套着扫描图片、表格、多栏排版时,常规解析库根本搞不定。我踩过最典型的坑是:某品牌PDF里"产品特性"的那几页是图片形式,OCR没做,结果检索系统搜不到任何关于特性的内容,用户问相关问题时模型直接说"资料中未提及"。后来我在这条流水线上加了OCR兜底,图片类PDF也能提出文字来。
清洗这一步很多人会忽略,但它直接影响切分质量。页眉页脚、重复的目录、乱码字符、超链接残留,这些噪声如果不处理干净,后面切出来的片段就是一堆语义垃圾。比如我处理过一个Word导出的文档,每一页都带着公司名和页码,切分后大量片段开头都是"某某科技有限公司7",检索时这些片段还经常因为公司名被误命中,干扰排序。
清洗完之后才是切分。这一步单独拿出来讲,因为它的细节太容易被低估。
2.2 切分策略:chunk size怎么定才能不丢语义
切分(Chunking)是RAG里最容易被忽视、实际影响最大的环节之一。切太细,单个片段语义不完整;切太粗,片段之间语义混杂,检索精度下降。它像切蛋糕:切小了拿起来会碎,切大了吃着不方便。
我在实践中用过三种切分策略:固定大小切分、按结构切分、语义切分。固定大小就是按字符数或token数硬切,比如每512个token一段,相邻段之间重叠50个token。这个方案实现最简,但效果最不稳定,经常把一句完整的话从中间截断。按结构切分是利用文档本身的层级结构,比如Markdown的标题、PDF的章节,在每个二级标题或三级标题处断开。这个方案质量高很多,但依赖文档格式规范,遇到乱七八糟的排版就失灵。语义切分是基于embedding相似度动态找切分点,实现成本最高,一般团队没必要一上来就上这个。
切分大小怎么选?没有标准答案,但我给一个经验范围:一般场景300到500个token比较合适,上限不超过800。为什么这么定?因为chunk太小,单靠一个片段很难覆盖完整的事实描述;chunk太大,向量化时语义被稀释,检索出的片段里也掺了大量无关内容。有个很实用的检验方法:挑出你知识库里最有代表性的10个问题,手动查一下每个问题对应的top3片段,看它们的内容是否恰好能回答这个问题。如果片段太碎答不上来,就调大chunk;如果片段太杂掺了很多无关内容,就调小chunk。
另外,重叠(overlap)也很关键。我一般设50到100个token的重叠,这样能保证在切分边界处的关键信息不会因为被"拦腰截断"而丢失。后来我还试过一种做法:切分时附带段落上下文信息,比如在每段前面加上所属的章节标题。这个"标题注入"的技巧对改善检索效果立竿见影——因为很多查询的关键词就藏在标题里。
2.3 向量化与存储:Embedding模型和向量数据库怎么选
切分完的文档片段,下一步要做embedding,也就是把它们变成一组向量。这个向量的含义,可以粗糙地理解为"这段话在语义空间里的坐标"。语义相近的文本,坐标距离也近。
Embedding模型的选择直接影响检索效果,没有之一。业界常用的有OpenAI的text-embedding-ada-002、Cohere的embed系列,开源的有BGE系列、GTE系列、E5系列。我的建议是:如果你的场景以中文为主,优先考虑中文表现更好的BGE或GTE系列;如果中英混合且对英文依赖高,可以考虑通用型更强的模型。选模型不要只看榜单分数,一定要在你的业务数据上跑一跑,看检索出来的结果是不是真的符合直觉。
我用过一个很典型的案例:某团队一开始贪省事直接用了某个英文表现很好的通用模型,中文问题检索出来的相关片段经常"答非所问"。后来换成了对中文做了专门优化的模型,同样的代码、同样的chunk配置,召回准确率立刻显著改善。这个教训说明:embedding模型是RAG的"眼睛",眼睛看不准,后面全都白搭。
向量数据库的选择也需要权衡。当前主流方案有专门的向量数据库如Milvus、Qdrant、Weaviate,也有基于传统数据库加向量插件的方案如PostgreSQL的pgvector。如果你的数据量在百万量级以下、团队不想额外维护一套存储,pgvector完全够用;如果数据量到了千万量级、对QPS和延迟要求高,那专门优化的向量数据库更合适。我目前在用的方案就是在PostgreSQL上跑pgvector,原因是团队本来就依赖PostgreSQL,少一个组件就少一个运维负担。数据量上去之后再迁移也不迟,接口兼容性已经做得不错了。
2.4 在线查询阶段:从问题到答案的完整链路
离线建库只是准备工作,真正的战斗发生在查询阶段。一个完整的在线查询流程是这样的:
用户输入问题->Query理解与改写->混合检索->重排序->组装Prompt->大模型生成。
Query改写这个环节容易被新手忽略。用户的问题往往是不完整、口语化、有歧义的。比如用户问"它支不支持多人协作?"——"它"指的是什么?如果知识库里有多个产品文档,模型去检索时根本不知道要找哪个产品。我的做法是维护一个轻量级的意图识别和指代消解模块,或者直接把之前几轮对话的内容拼进query里一起检索。这个用大模型本身就能做,给一个"将用户问题改写为独立完整表述"的指令即可,耗时不长但效果改善很大。
检索环节我想特别说一点:别只用向量搜索,要上混合检索。向量检索擅长找"意思相近"的内容,但对关键词精确匹配反而不敏感。比如用户搜"ISO 9001认证",向量检索可能找出"质量管理体系认证"的文档,但搜不出文中只有"ISO 9001"这几个字眼的那段。BM25这类传统关键词检索恰恰擅长这个。我目前的做法是向量检索 + BM25关键词检索各跑一遍,再用RRF(Reciprocal Rank Fusion)把两份结果融合。所谓RRF公式不复杂,简单说就是给同一个文档在多个结果列表中的排名取倒数并求和,排名越靠前得分越高。这个方案朴素但极其有效,召回质量立刻上一个台阶。
检索完之后,重排序(Rerank)是承上启下的关键环节。初检召回的可能有一二十个候选片段,但它们和问题的相关度参差不齐。重排序模型(通常是cross-encoder结构)把"问题 + 片段"成对送入模型,给出精细的相关性打分,然后取top-k进上下文。Cross-encoder的精度比双塔encoder高,因为它能让问题和文档在模型内部做充分的交互。代价是计算量大,但因为只对初检出来的几十条做排序,实际开销完全可接受。我见过的几乎所有"检索结果看起来对但答案不对"的问题,最后都靠加Rerank解决了一部分。这是性价比极高的一步。
最后是组装Prompt。这一步的细节经常决定成败。我维护过一套Prompt模板,长这样:
系统指令(大意): 你是一名专业的问答助手。请严格基于以下提供的资料回答问题。如果资料中没有相关信息,请直接回答"根据现有资料无法回答",不要编造。引用资料中的信息时,请标注来源编号。
用户输入部分: 资料1:... 资料2:... ...
问题:...
这里的关键是"不要编造"+"标注来源"。实际测试下来,加上这两句话之后,模型的幻觉率明显下降。标注来源不仅能帮用户核实答案,还能在答案出错时快速定位是检索问题还是生成问题。
3. 从Demo到可用:工程落地必须跨过的五道坎
3.1 召回质量:为什么搜出来的东西不对
Demo阶段随便搭个RAG跑通很容易,但到生产环境你会发现最扎心的一个问题:明明文档里有正确信息,检索却搜不到。
这个问题的成因有几个层面。第一是chunk切分不合理,关键信息被拦腰截断或分散到多个chunk里;第二是embedding模型能力不足,语义表征不够准确;第三是用户query和文档的表述方式差异太大。比如用户问"保修的期限是多久",文档里写的是"自签收之日起一年内",语义完全一致但字面完全不一致,弱一点的embedding模型就匹配不上。
解决这个问题要组合拳:chunk调优是基础,混合检索是保底,Rerank是提精度。还有一个技巧是做同义词和表述归一化——在query侧把"质保""售后""保修"这类同义词统一映射到标准词条再检索。我后面专门加了一个基于词典和规则的同义词扩展模块,召回率又涨了一截。
另一个容易忽视的点是元数据过滤。如果你的知识库覆盖多个产品、多个业务线,每次检索都全库搜,相关性必然被稀释。应该在索引时给每个chunk打上产品线、文档类型、更新时间等标签,查询时先按业务上下文过滤一遍再检索。这个方案我实测下来,检索精度提升非常明显。
3.2 重排序:让最相关的文档排前面
重排序前面已经提到,这里专门展开说一些工程细节。有很多团队跳过Rerank直接把top-k传入大模型,理由是"大模型自己会判断"。这话我不太认同。初检返回的top-k是按向量相似度排的,里面经常混着几个"形似但神不似"的片段。你把这些片段一股脑喂给大模型,大模型的注意力会被无关信息干扰,尤其是当无关片段信息量很大的时候,模型甚至可能跑偏去答那些无关内容。
我在生产环境用的Rerank方案是bge-reranker系列模型,没有用更重的GPT-4级排序。原因很简单:QPS高,用heavy模型扛不住,bge-reranker在中文场景的表现已经够用。Rerank的输入输出很简单,把query和候选文档逐对拼接送进去,输出相关性分数,然后按分数取top-4或top-5。注意top-k的选择也有讲究:取太少,信息可能不够;取太多,上下文太长、token浪费、模型注意力分散。我最终稳定在top-5,兼顾覆盖和聚焦。
3.3 上下文组装与Prompt设计:让模型"只答该答的"
Prompt设计在RAG链路里看似简单,实际操作时却容易被忽视。最大的一个隐性坑是上下文里资料太多,模型不知道以谁为准。比如top-5片段里有一条是2023年的旧文档,这会导致模型优先用最新信息,而旧文档片段就会干扰判断。
我的解法是在Prompt里显式标注每份资料的时间戳和来源,并加一句"请优先采用最新且相关性最高的资料"。这个你看下来会觉得很简单,但效果非常稳。还有一个技巧是给每条资料加一个内容描述前缀,比如"资料1(产品说明手册,2025年3月更新):..."。描述前缀能帮助模型快速把握资料内容类型,减少"误读"的机会。
另外,Prompt的指令要具体到"什么情况下该说什么话"。比如加一句"如果你无法从资料中找到确切答案,请明确回答:无法回答,并建议用户联系人工客服"。比单纯的"不要编造"更好用——它给模型一个明确的行为兜底路径,而不是让它自己揣摩。
3.4 评估体系:没有评测就没有优化
这个点我必须强调,因为它本质上决定了RAG能不能从"能跑"变成"好用"。没有评估,你改动一个参数都不知道是变好还是变坏,完全是闭眼开车。
我搭建评估体系用过两条腿走路:基于指标的自动评估和基于样例集的人工回归。自动评估用的是RAGAS里的几个核心指标:忠实度(Faithfulness)衡量生成答案是否忠于检索到的资料,答案相关度(Answer Relevancy)衡量答案是否切题,上下文相关度(Context Relevancy)衡量检索到的内容是否相关。这几个指标现在都有对应的评测框架直接调用,跑起来不费事。
但光看指标还不够,我维护了一个固定的评测问题集,大概100个问题,全部来自业务方和真实用户询问。每次改动RAG链路的任何环节,我都用这100个问题跑一遍回归,人工抽查回答质量,看改进和回退情况。这个习惯帮我避免过至少三次"指标上去了体验反而变差"的事故。有一个印象很深的例子:某次调大chunk size后,上下文相关度指标涨了,但人工抽查发现模型回答变得啰嗦,因为每个chunk里都掺了半段无关文字。这种问题只看数值是发现不了的。
3.5 性能与成本:响应时间与存储优化
落到生产环境,还有一个躲不开的问题:性能和成本。
响应时间的瓶颈通常不在大模型生成,而在检索链路。向量检索本身的延迟可以做到毫秒级,但如果你在检索前做了Query改写(要调大模型)、做混合检索(要同时查两个索引再融合),延迟就会叠加。我的实测经验是,纯向量检索平均延迟在50到100毫秒,加上Rerank会涨到200毫秒左右,再叠加大模型生成时间,整体用户等待时间在2到5秒之间。对大多企业内问答场景,这个水平是可以接受的。
如果想进一步压延迟,有几个方向。缓存是见效最快的一个:把高频问题及其答案缓存起来,命中后直接返回,完全跳过检索和生成链路。我做过一组对比,加了Redis缓存后,热点问题的响应时间从3秒降到200毫秒以下。索引优化也有很大空间:如果用pgvector,可以调HNSW索引的M参数和ef_search参数,M越大检索越准但索引越大,ef_search越大召回质量越高但延迟越高。我最终调的参数是M=16、ef_search=100,在1万条文档片段规模下质量和延迟平衡得很好。
存储成本方面,向量数据比普通文本要贵不少。一个512维的向量,float存储占2KB,1万条就20MB,百万条就是2GB。好在有量化手段,把向量从float32压到int8甚至二进制,体积能缩小几倍到几十倍,精度损失在可接受范围。我的经验是:先用float32跑通,发现存储瓶颈后再做量化,不要一开始就上量化找罪受。
4. 实战踩坑记录:这些问题我几乎都遇到过
4.1 知识库里的表格全部乱掉
知识库里文档类型多样,其中一大类是包含表格的。刚上线时,我对PDF里的表格直接做文本提取,结果表格的行列关系全部丢失,比如"产品型号A,价格100元"变成了"A 100元"两坨文字,检索时完全匹配不上。后来我专门引入了表格解析工具,把表格结构转成Markdown格式再入库。这个改动看起来不大,但对表格类文档的检索效果提升近乎立竿见影,实测召回率明显改善。
经验:如果你的知识库里有大量表格型数据,别用通用的PDF解析器糊弄过去,一定要用专门的表格结构识别工具,把结构信息保留下来。
4.2 相似文档互相干扰
知识库里有很多版本迭代的文档,比如产品手册V1.0、V1.1、V2.0。这些文档内容大量重叠,检索时经常把旧版本的片段和最新版本混在一起,导致模型有时候基于过时信息回答。
解决思路有两个:一是给旧版本文档打上"已废弃"的元数据标签,在检索时直接过滤掉;二是设定规则,当检索结果同时命中同一篇文档的多个版本时,只保留最新版本。这两个方案我都用了,效果比较彻底。
4.3 多轮对话中的指代问题
多轮对话场景下,用户后续的问题经常依赖前面的上下文。比如用户先问"你们有哪些数据集成方案",系统答了;用户再问"第二个多少钱?"——"第二个"指的是方案列表里的第二个。如果不处理这个指代,单独把"第二个多少钱"丢进检索,什么都搜不到。
我的做法是在检索前加一个query改写模块:把最近几轮对话交给大模型,让它把当前问题改写成一个脱离上下文也能被理解的独立查询。这个模块用模型接口即可,成本不高,但对多轮场景的体验帮助巨大。最开始我嫌多一次模型调用增加延迟,后来发现这点延迟换来的是"用户不再反复重述问题",值。
4.4 增量更新:索引删不干净,旧数据阴魂不散
知识库要不断更新,但向量数据库的删除操作有时候不像想象中那么"干净"。有一次我更新了一批文档,但旧版本的向量没有及时在索引里删除干净,导致检索时新旧内容同时出现。最后排查发现,那段删除逻辑只做了批量替换操作,没有同步清理对应的元数据记录。
这个问题的排查经历让我养成了一个习惯:任何更新操作都要有完整的增删改流程和校验机制。改索引前先备份,更新后跑一遍样例回归,确认旧数据真的不存在了再收货。这听起来像是常识,但每一个在线上被旧数据坑过的人都懂,常识最容易在拷机的时候被忽略。
4.5 检索结果的"轮回"困境
这里说的"轮回",是指某种片段在多个不同问题下都被检索出来,导致模型反复用它作答,覆盖不到其他有用信息。比如知识库里有一段关于"产品支持的操作系统"的通用说明,无论用户问"支持Windows吗""能装Linux吗""有macOS版吗",这段都会被召回到top5,占据了上下文空间。
我的办法是给这类"万金油"片段提高重复率惩罚,或者强制降低它们的权重。另外,调整不同来源文档的检索权重,让高价值、垂直内容更容易顶上,也能缓解这个问题。
5. 工具选型解析:我用的这套组合拳
5.1 为什么坚持自研而非全盘购买
市面上已经有不少成熟的RAG编排框架和平台,直接拿来用确实省事。我自己也评估过,但最后选择了"半自研"路线:核心链路自己控,通用组件接成熟库。
这样选的原因主要有三点。第一,业务场景太特殊,文档类型杂、查询方式多,通用平台的定制能力常常不够灵活。第二,可控性,自研链路可以精确掌握每一环节的耗时和效果,出问题能定位到具体模块。第三,数据安全,很多企业场景要求私有化部署,数据不出内网,通用云平台很难满足。
当然这个选择也付出了代价:从搭框架到优化效果,前后花了不少时间。如果你们的场景以公开文档问答为主,文档格式统一、查询模式简单,直接用成熟框架快速上线没什么不好。关键判断标准是:你的业务是否需要深度定制。需要就自研核心链路,不需要就快速集成,不要在架构上过度投资。
5.2 组件层面的具体选型建议
我当前的RAG技术栈大概是这样的:文档解析用专业解析库加OCR兜底,向量化用开源BGE中文模型微调过一版,存储用PostgreSQL加pgvector,检索用向量加BM25混合加Rerank,编排层用LangChain的部分工具函数。
这里用不用LangChain,我也纠结了很久。它提供了很多抽象的封装,上手快,但抽象层也意味着排查问题时要多绕一层。后来我的取舍是:只使用它成熟的加载器工具,核心流程(检索、重排、组装Prompt)都自己写逻辑。这样既省了重复造轮子的成本,又避免了框架过于黑盒带来的排查困难。
给一个原则:能用简单方案解决的,不要为了上框架而上框架。框架本身不产生价值,价值在于你如何处理你的数据和业务。
6. 如果还要进一步:从单点RAG到Agent化
RAG做到后面你会发现,单轮"检索+生成"的架构虽然有价值,但在复杂任务面前还是有点力不从心。比如用户问"帮我整理一份上季度所有项目的风险清单,并按照严重程度排序"——这个问题涉及多次检索(找项目列表、找风险描述、找严重程度判断)和多步推理,单次RAG根本搞不定。
这时候就需要把RAG从"一次检索一次生成"升级为"多轮检索多轮生成"的Agent形态。核心思路是:把RAG的检索能力嵌进一个更大的智能体循环里。智能体根据任务需求拆解计划,分步调用检索工具,把每一步的结果汇总、验证,最终产出答案。
我在实验环境里搭过一版这样的Agent化RAG:它内部维护一个任务队列,每一步先判断"还需要哪些信息",然后调用检索工具获取资料,再让大模型生成中间结论,直到所有子任务完成。效果确实更强大,但代价也明显——链路变长、延迟更高、出错概率更大。我的建议是:如果单轮RAG能解决80%的问题,就不要急着上Agent化。先把基础打牢,等业务复杂度真的突破单轮上限时再演进,会从容很多。
从原理到工程落地,RAG这条路不算长,但每一步都有细节要抠。这篇里我尽量把实操层面的经验都抖出来了。如果你正在搭RAG,希望这些内容能帮你少踩几个坑。后续有空我再单独聊聊Agent化这个方向的新探索。