RAG 这个词这两年火得不行,但很多人第一次接触它的时候,脑子里其实是一团浆糊:一会儿说向量库,一会儿说 Embedding,一会儿又冒出个"检索增强",听着都懂,真让自己搭一个,又不知道从哪下手。我刚开始做 RAG 项目的时候也这样,文档翻了一堆,代码抄了一堆,跑起来效果却一塌糊涂——问它公司年假怎么算,它给我扯到食堂菜单上去了。
后来踩的坑多了才慢慢想明白一件事:RAG 本质上就两条链路,一条离线,一条在线。离线那条负责把知识"腌入味",在线那条负责把问题"对上下"。绝大多数人做不好 RAG,不是模型不行,而是这两条链路的边界没理清楚,该在离线做的活儿拖到了在线,该在在线做的判断又塞进了离线。今天我就把这两条链路从头到尾拆一遍,讲清楚每一步在干什么、为什么这么干、以及我实际踩过的那些坑。
这篇文章适合谁看?如果你正准备给自己的业务搭一个知识库问答,或者已经搭了一个但效果不理想,又或者你只是想把 RAG 的概念真正落地成能跑的东西,那这篇应该能帮到你。我不打算堆术语,尽量用大白话把每个环节讲透。
1. 先搞清楚 RAG 到底在解决什么问题
1.1 大模型的两个硬伤:不知道新事,也记不住私事
要理解 RAG,得先理解它为什么会出现。大语言模型有个天生的毛病:它的知识是"冻结"在训练数据里的。训练截止到某个时间点之后发生的事情,它一概不知;你公司内部的规章制度、产品文档、客户案例,它更是闻所未闻。你直接问它"我们公司报销流程是什么",它要么瞎编,要么礼貌地告诉你它不知道。
另一个毛病是"幻觉"。模型在不确定的时候,倾向于编一个听起来很合理的答案,而不是老实说"我不知道"。这在闲聊场景无所谓,但在企业知识库、客服、法律咨询这类场景里,编造答案是致命的。
RAG 的思路其实特别朴素:既然模型不知道,那我就在它回答问题之前,先把相关资料找出来塞给它,让它"看着材料答题"。就像开卷考试,你不需要把所有知识背下来,只要能在需要的时候翻到对应的那一页就行。这个"翻书"的动作,就是检索;把翻到的内容连同问题一起交给模型,就是增强;模型基于这些内容生成答案,就是生成。合起来就是 Retrieval-Augmented Generation。
1.2 为什么是"两条链路"而不是一条
很多人一开始会把 RAG 想成一个线性的流程:用户提问 → 检索 → 生成答案。这个理解没错,但它只描述了"在线"那一半。真正让 RAG 能跑起来的,是背后那条看不见的离线链路。
打个比方。你开一家图书馆,读者来借书(在线查询),你能不能在读者站在柜台前的那几秒钟里,把全馆几十万本书现场编目、贴标签、上架?显然不行。这些活儿必须提前干完,读者来了直接查目录、取书就行。离线建库就是"提前编目上架",在线查询就是"读者来了查目录取书"。
这两条链路的关注点完全不同:
- 离线链路关心的是"知识怎么被切分、怎么被表示、怎么被存进去",它决定了检索的上限——如果知识在离线阶段就没处理好,在线阶段再怎么优化也救不回来。
- 在线链路关心的是"问题怎么被理解、怎么被匹配、怎么被组织成答案",它决定了检索的下限——同样的库,在线处理得好不好,直接体现在用户看到的答案质量上。
我见过太多项目,在线部分调得花里胡哨,又是重排序又是多路召回,结果离线阶段把文档切得七零八落,一个完整的表格被切成三段,检索出来全是残片,模型看着这些碎片也只能瞎猜。所以下面我先把离线链路讲透,再讲在线链路。
2. 离线建库:把知识"腌入味"的全过程
离线建库这条链路,我习惯把它拆成四个动作:加载、切分、向量化、入库。听起来简单,但每一步都有讲究,下面逐个说。
2.1 文档加载:别小看"把文件读进来"这件事
加载这一步,说白了就是把各种格式的原始资料读成纯文本。PDF、Word、Markdown、HTML、Excel、数据库记录,甚至图片里的文字,都可能成为知识来源。这一步看起来最没技术含量,但恰恰是最容易埋雷的地方。
我踩过最典型的一个坑是 PDF。很多 PDF 是扫描件或者排版复杂的文档,直接解析出来全是乱码或者错位的文字。有一次我处理一份产品手册,解析出来的文本里,表格的行列全乱了,本来"型号 A 支持功能 X"变成了"型号 A 功能 X 支持",语义完全变了。后来我加了一步 OCR 校验,对解析结果做人工抽查,才发现问题。
还有一个坑是元数据丢失。原始文档往往带着标题、章节、页码、来源、更新时间这些信息,如果加载的时候只提取正文,把这些元数据扔了,后面检索出来的片段就是"无源之水",用户看到答案也不知道出自哪份文件的哪一页,可信度大打折扣。所以我在加载阶段一定会保留:来源文件名、章节路径、页码、更新时间这几项,后面入库时一起存进去。
提示:加载阶段一定要做"抽样人工校验"。随机抽 10 到 20 个文档,把解析出来的文本和原文对照,看看有没有丢内容、串行、乱码。这一步花半小时,能省掉后面几天的排查。
2.2 文本切分:RAG 效果的分水岭
如果让我选一个对 RAG 效果影响最大的环节,我会毫不犹豫地选文本切分。切分策略选错了,后面所有优化都是白费。
为什么要切分?因为模型和向量模型都有输入长度限制,你不可能把一整本书塞进去。而且从检索精度角度,一个片段最好只讲一件事,这样匹配问题的时候才准。如果一段文本里混了五六个主题,检索出来反而会干扰模型。
切分最核心的参数是块大小(chunk size)和重叠长度(overlap)。块太大,一个片段里信息太杂,检索精度下降;块太小,一个完整的语义被切断,模型拿到的是残句。重叠是为了防止关键信息正好卡在切分边界上被切断,让相邻块之间有一点内容重叠。
我一般会这样起步:块大小 500 到 800 个字符,重叠 100 到 150 个字符,然后根据实际效果微调。但这个数字不是死的,得看你的文档类型:
| 文档类型 | 建议块大小 | 建议重叠 | 说明 |
|---|---|---|---|
| 技术文档、API 说明 | 400-600 | 80-120 | 结构清晰,块可以小一点 |
| 规章制度、合同 | 600-900 | 120-180 | 条款完整性强,块要大些 |
| 对话记录、客服日志 | 300-500 | 60-100 | 单轮对话本身短,块要小 |
| 学术论文、长报告 | 800-1200 | 150-250 | 论证连贯,块大才能保语义 |
比固定长度切分更好的做法是按语义结构切分。Markdown 按标题层级切,HTML 按标签切,代码按函数切,合同按条款切。这样切出来的块天然就是"一个完整的意思",检索精度会明显提升。我现在的习惯是:能用结构切就绝不用固定长度切,实在没有结构(比如纯文本流水账)才退而求其次用固定长度加重叠。
还有一个容易被忽略的点:给每个块加上下文头。比如一个块是从"第三章 报销制度 > 3.2 差旅报销"下面切出来的,那我在这个块的正文前面加上"第三章 报销制度 3.2 差旅报销"这行字。这样即使这个块单独被检索出来,模型也知道它讲的是哪部分内容,不会断章取义。这个技巧成本极低,但效果提升很明显。
2.3 向量化:把文字变成"语义坐标"
切分完的文本块,要变成计算机能比较的东西,就得靠 Embedding 模型把它转成一串数字,也就是向量。你可以把向量想象成"语义坐标系里的一个点"——意思相近的文本,它们的点在空间里离得近;意思差得远的,点就离得远。
这里有个关键认知:Embedding 模型和生成模型是两回事,得分开选。生成模型负责最后写答案,Embedding 模型负责检索阶段的语义匹配。很多人只盯着生成模型选,随便找个 Embedding 模型凑合,结果检索阶段就拉胯了。
选 Embedding 模型我一般看几个维度:
- 语言支持:中文场景一定要选对中文优化过的模型,有些模型英文很强,中文一塌糊涂。
- 维度:向量维度越高,表达能力越强,但存储和计算成本也越高。常见的有 384、768、1024、1536 维。一般 768 到 1024 维够用。
- 最大输入长度:这个要和你的块大小匹配。如果模型最大只支持 512 个 token,你块切了 1000 个字符,超出的部分会被截断,信息就丢了。
- 检索性能:这个得看实际评测榜单,但榜单只能参考,最终还得在自己的数据上测。
注意:离线建库用的 Embedding 模型,必须和在线查询用的完全一致。这是铁律。用 A 模型建的库,用 B 模型去查,向量空间对不上,检索结果基本等于随机。我见过有人换模型的时候只换了查询端,忘了重建库,排查了半天才发现是这个原因。
向量化这一步还有个工程上的考量:批量处理。几十万上百万个块,一个个调接口会慢到怀疑人生。一般 Embedding 服务都支持批量输入,一次传几十上百条,能大幅提速。但要注意别超过服务的单次请求上限,超了会报错或者被截断。
2.4 入库:向量库选型与索引构建
向量化完的向量,得存进向量数据库。选向量库这件事,我的原则是:先看规模,再看运维成本,最后看功能。
小规模场景(几万到几十万向量),用轻量级的方案就够了,甚至用本地文件加内存索引都能跑。中等规模(百万到千万级),可以考虑专门的向量数据库。大规模(亿级以上)就得上分布式方案了。
选型的时候有几个点特别容易踩坑:
- 过滤能力:实际业务里,检索往往不是全库搜,而是"在某个部门、某个时间段、某个类别的文档里搜"。如果向量库不支持带条件的过滤检索,你就得先全量召回再自己过滤,性能会很差。
- 更新和删除:知识是会变的,文档会更新、会作废。如果向量库不支持高效的增量更新和删除,每次改一点内容都要全量重建,运维成本极高。
- 持久化和备份:别用纯内存的方案跑生产,重启一次数据全没了。
入库的时候,除了向量本身,一定要把原始文本、元数据(来源、章节、页码、时间)一起存进去。向量只用来做匹配,真正喂给模型的是原始文本,元数据用来做过滤和展示引用来源。
索引构建这块,如果数据量大,建索引本身可能就要花不少时间。我一般会先把数据入库,再异步建索引,建完之前先用暴力检索顶着,建完再切换。这样不会因为建索引把整个服务卡死。
3. 在线查询:用户按下回车之后发生了什么
离线链路把知识准备好了,在线链路就是用户提问之后的那几秒钟里发生的事。这条链路我拆成四步:问题理解、检索召回、重排序、生成答案。
3.1 问题理解:用户问的和他想要的,往往不是一回事
用户输入的问题,直接拿去检索,效果经常不好。原因很简单:用户说话很随意,可能带着口语、缩写、错别字,甚至问题本身就不完整。比如用户问"这个咋弄",你根本不知道他问的是哪个功能。
所以在线链路的第一步,往往是对问题做改写或扩展。常见做法有几种:
- 查询改写:把口语化的问题改写成更规范的检索语句。比如"报销咋整"改写成"报销流程 报销所需材料"。
- 查询扩展:给原问题补充同义词、相关词。比如"年假"扩展成"年假 带薪年假 休假制度"。
- 多查询生成:让模型基于原问题生成几个不同角度的子问题,分别去检索,再把结果合并。这个对复杂问题特别有效。
- 意图识别:先判断用户问的是哪类问题,是查事实、查流程,还是要对比,不同意图走不同的检索策略。
这一步用不用、用多重,取决于你的场景。简单的事实问答,直接检索可能就够了;复杂的业务咨询,改写和扩展能明显提升召回率。但要注意,改写本身也会引入误差,如果改写跑偏了,反而会把检索带歪。我的经验是:改写要保守,宁可少改,不要乱改。
3.2 检索召回:从海量块里捞出最相关的那些
检索的核心动作,是把用户问题也向量化,然后去向量库里找"距离最近"的那些块。距离度量常见的有余弦相似度、欧氏距离、内积,具体用哪个要看向量库和模型的约定,一般用余弦相似度居多。
这里有几个参数要调:
- Top-K:召回多少个块。K 太小,可能漏掉关键信息;K 太大,会引入噪声,还会增加后面生成的成本。一般从 5 到 10 起步,根据效果调。
- 相似度阈值:低于某个相似度的块直接扔掉,避免把不相关的内容硬塞给模型。这个阈值需要根据实际数据分布来定,没有万能值。
纯向量检索有个短板:它对关键词不敏感。比如用户问一个具体的产品型号"X2000",向量检索可能召回一堆讲产品系列的块,但就是没召回那个精确提到 X2000 的块。这时候就需要混合检索——把向量检索和关键词检索(比如 BM25)结合起来,两路结果融合。向量检索负责语义匹配,关键词检索负责精确匹配,互补之后召回质量会好很多。
融合的方式常见的有加权求和和倒数排名融合(RRF)。RRF 的好处是不需要归一化分数,直接按排名融合,实现简单且效果稳定,我个人比较常用。
3.3 重排序:把最该看的排到最前面
召回阶段追求的是"不漏",所以会捞回来一批候选,里面难免有噪声。重排序(Rerank)阶段追求的是"精准",用一个更精细的模型对候选块重新打分排序,把真正相关的排到最前面。
为什么召回和排序要分开?因为召回要求快,得在毫秒级从百万级数据里筛出几十个候选,所以用的是轻量的向量相似度。而重排序只处理这几十个候选,可以用更重、更准的模型,比如交叉编码器(Cross-Encoder),它会把问题和每个候选块拼在一起过一遍模型,精度高但速度慢。两阶段配合,兼顾了速度和精度。
重排序之后,一般取 Top 3 到 Top 5 个块喂给生成模型就够了。给太多反而会让模型分心,还可能引入矛盾信息。
3.4 生成答案:把材料组织成人话
最后一步,把用户问题和检索到的块一起塞进提示词,让生成模型写出答案。这一步的提示词设计很关键,我一般会明确几件事:
- 只基于给定材料回答,材料里没有的就说不知道,不要自己编。
- 标注引用来源,答案里提到的事实要能对应到具体的块。
- 如果材料之间有冲突,要指出来,而不是随便选一个。
- 回答要简洁,直接回答问题,不要绕圈子。
提示词里我还会把检索到的块编号,让模型在答案里引用编号,这样用户能追溯答案的依据,可信度会高很多。
4. 两条链路是怎么咬合的:几个关键的一致性约束
讲完两条链路,得说说它们之间的咬合关系。这两条链路不是独立的,它们通过几个"契约"绑在一起,任何一个契约被破坏,整个系统就会出问题。
4.1 Embedding 模型必须两端一致
这是最重要的一条。离线用模型 A 建库,在线就必须用模型 A 查询。换模型意味着整个库要重建。我见过有人为了"提升效果"在线换了个更强的 Embedding 模型,结果检索全乱套,因为新旧模型的向量空间根本不可比。
如果确实要换模型,正确做法是:新模型建一个新库,两套并行跑一段时间,对比效果,确认新库更好之后再切换,旧库保留一段时间做回滚。
4.2 切分策略决定了检索的粒度
离线怎么切,在线就怎么召回。如果离线把一段完整流程切成了五块,在线检索时可能只召回其中两块,模型看到的就是残缺的流程。所以切分的时候就要想清楚:用户会怎么问,我希望召回的是多大粒度的内容。
一个实用的技巧是小块检索、大块生成:用小的块做检索保证精度,命中之后把它所在的更大的父块(比如整个章节)取出来喂给模型,保证上下文完整。这个思路在很多实际项目里效果很好。
4.3 元数据是过滤和引用的基础
离线阶段存的元数据,在线阶段要用来做过滤(比如只搜某个部门的文档)和引用展示(告诉用户答案出自哪份文件)。如果离线没存好,在线就没得用。所以元数据的设计要在离线阶段就想清楚,别等到在线要用的时候才发现缺字段。
5. 我踩过的那些坑:真实排查链路复盘
光讲原理不够,下面复盘几个我实际踩过的坑,把排查过程完整写出来,你可以对照自己的项目看看有没有类似问题。
5.1 检索结果总是答非所问
现象:用户问"差旅费报销标准",检索出来的全是"差旅申请流程"。
排查过程:先看检索出来的块,发现这些块里确实频繁出现"差旅"这个词,但讲的是申请不是报销。再一看切分,发现"差旅申请"和"差旅报销"在原文里是相邻的两节,切分的时候块大小设得太大,把两节内容切进了同一个块,导致这个块既包含申请又包含报销,向量表示被"平均"了,跟"报销标准"的匹配度反而不如纯讲申请的块。
解决:改成按标题层级切分,让"申请"和"报销"各自成块,问题立刻解决。这个坑让我彻底认识到:切分策略比模型选型更重要。
5.2 明明库里有,就是检索不出来
现象:某份文档里明确写了答案,但无论怎么问都检索不到。
排查过程:先确认文档确实入库了,查向量库发现有这个块。再手动用这个块的内容去检索,能检索到;但用用户的实际问法去检索,就检索不到。对比发现,用户问法里用的词和文档里的词是同义词但字面不同,而当时用的 Embedding 模型对这类同义词的语义匹配能力较弱。
解决:一是换了个语义能力更强的 Embedding 模型并重建库;二是加了查询扩展,把用户问题里的关键词做同义词扩展后再检索。两个措施叠加,召回率明显提升。
5.3 答案里出现了库里没有的信息
现象:模型生成的答案里有一些细节,但检索到的材料里根本没有。
排查过程:这是典型的幻觉。检查提示词,发现提示词里没有明确约束"只能基于材料回答"。模型在材料信息不足的时候,习惯性地用自己的先验知识补全了。
解决:强化提示词约束,明确要求"如果材料中没有相关信息,直接回答不知道",并且在答案里要求标注引用编号,没有引用编号的句子视为不可信。改完之后幻觉明显减少。
5.4 更新文档后答案还是旧的
现象:文档更新了,但问答系统还是按旧内容回答。
排查过程:查向量库发现旧块还在,新块也进去了,检索时旧块因为某些原因排名更靠前。
解决:建立了文档版本管理,更新文档时先删除对应的旧块再插入新块,并且给块加上更新时间戳,检索时对更新的内容做加权。这个问题的根源是离线链路的更新机制没设计好,属于典型的"离线埋雷、在线爆炸"。
6. 把两条链路跑顺之后,还能往哪走
两条基础链路跑通之后,RAG 还有很多可以深挖的方向。我简单提几个,不作为重点,但值得了解。
GraphRAG:传统 RAG 是把知识切成孤立的块,块与块之间的关系丢了。GraphRAG 的思路是先把知识抽成实体和关系的图谱,检索的时候沿着图谱找相关联的内容。对于需要多跳推理的问题(比如"A 的上级是谁的同事"),效果比传统 RAG 好很多。代价是离线建库阶段要额外做实体抽取和关系构建,成本高不少。
Agentic RAG:让模型自己决定要不要检索、检索几次、用什么查询检索。传统 RAG 是"一问一检索一生成"的固定流程,Agentic RAG 把检索变成模型可以调用的工具,模型根据问题复杂度自主规划。适合复杂任务,但延迟和成本都会上升。
多模态 RAG:知识库里不只有文字,还有图片、表格、图表。多模态 RAG 要解决的是怎么把这些非文本内容也纳入检索。目前比较实用的做法是给图片生成文字描述再向量化,或者用支持多模态的 Embedding 模型直接编码。
这些方向各有适用场景,不建议一上来就上,先把基础的两条链路跑稳,再根据实际瓶颈决定往哪个方向深入。
7. 一些实操层面的经验之谈
最后分享几个我在实际项目里总结的小经验,都是文档里不太会写、但确实管用的。
关于评测:RAG 效果好不好,不能靠感觉,得有评测集。我一般会准备一批"问题-标准答案-相关文档"的三元组,每次改动之后跑一遍,看召回率和答案准确率的变化。没有评测集,优化就是盲人摸象。评测集不用多,几十到上百条就能看出趋势。
关于成本:Embedding 和生成都是要花钱的。离线建库是一次性成本,在线查询是持续性成本。优化在线成本的一个思路是缓存——高频问题的检索结果和答案可以缓存起来,命中缓存直接返回,既快又省。
关于延迟:在线链路的每一步都会累加延迟。检索几十毫秒,重排序可能上百毫秒,生成可能几秒。如果对延迟敏感,可以考虑把重排序做成可选的,或者用更快的生成模型。用户体验上,流式输出能显著改善"等待感",即使总时长不变,用户感觉也快很多。
关于知识库的维护:RAG 不是建完就完事了,知识库是要持续维护的。文档会过期、会新增、会修改,得有一套机制保证库里的内容和实际知识同步。我见过太多项目上线时效果很好,半年后因为没人维护,答案全是过时的,最后被弃用。维护机制的重要性不亚于建库本身。
关于"够用就好":不是所有场景都需要最复杂的方案。如果知识库就几百份文档,问题也比较固定,一个简单的向量检索加生成可能就够了,没必要上混合检索加重排序。方案复杂度要匹配实际需求,过度设计只会增加维护负担。
RAG 这个东西,入门容易精通难。两条链路的框架很简单,但每个环节的细节都够你调很久。我的建议是先把最小可用的版本跑起来,然后拿真实问题去测,哪里不行补哪里,别一上来就追求完美架构。跑起来、用起来、根据反馈迭代,比什么都强。