1. 为什么RAG是当前大模型落地最务实的路径
如果你最近在关注大模型应用开发,大概率已经被各种"微调""Agent""长上下文"的概念轮番轰炸过。但真正在企业项目里落地过的人都有一个共识:RAG(Retrieval-Augmented Generation,检索增强生成)是目前性价比最高、工程风险最低、迭代速度最快的方案,没有之一。
我先把结论摆在前面。RAG的本质,是给一个已经训练好的语言模型外挂一个"可随时更新的知识库"。模型本身负责语言理解和生成,知识库负责提供事实依据。这样做的好处非常直接:你不需要重新训练模型,不需要准备成千上万条标注数据,不需要昂贵的GPU集群,只要把文档整理好、切分好、向量化存进去,模型就能"知道"你私有的那部分内容。
很多人第一次听到RAG会觉得这不就是"搜索+GPT"吗?表面上看确实像,但工程上的差别非常大。传统搜索返回的是文档列表,用户还得自己读;RAG返回的是模型基于检索结果重新组织的自然语言答案,而且能标注来源。这个差别决定了RAG可以直接嵌入到客服、内部知识助手、文档问答、代码助手等场景里,用户体验是质的飞跃。
这一讲我会把RAG从"为什么需要"到"怎么搭起来"再到"怎么调优"完整拆一遍。不管你是刚接触大模型应用开发的新手,还是已经做过一两个Demo但效果不理想的开发者,都能从中找到可以直接抄作业的部分。核心关键词我会反复提到:检索增强生成、向量数据库、文本切分、Embedding、召回率、重排序、上下文窗口。这些词你理解了,RAG的骨架就立起来了。
2. 大模型的三条知识注入路线,为什么RAG胜出
2.1 提示工程、微调、RAG的适用边界
在动手写代码之前,有必要先把"给模型注入知识"这件事的几条路线理清楚。很多人一上来就问"我该用RAG还是微调",其实这个问题本身就问错了——它们解决的不是同一类问题。
提示工程(Prompt Engineering)是最轻量的方式。你把知识直接写在提示词里,模型读完再回答。优点是零成本、即时生效;缺点是受限于上下文窗口长度,知识一多就塞不下,而且每次调用都要重复传一遍,token成本高。
微调(Fine-tuning)是把知识"烧"进模型权重里。优点是推理时不需要额外检索,速度快;缺点是成本高、周期长、知识更新困难——你今天微调完,明天公司政策变了,又得重新来一遍。而且微调容易导致"灾难性遗忘",模型可能把通用能力也弄丢了。
RAG走的是第三条路:模型权重不动,知识放在外部存储里,推理时实时检索。它的核心优势可以用三个词概括:可更新、可溯源、可控制。知识库改了,下一次查询立刻生效;每条答案都能追溯到原文出处;检索不到相关内容时,可以明确告诉用户"我不知道",而不是让模型胡编。
下面这张表是我在实际选型时常用的判断依据:
| 维度 | 提示工程 | 微调 | RAG |
|---|---|---|---|
| 知识更新速度 | 即时 | 慢(需重训) | 即时 |
| 初始成本 | 极低 | 高 | 中 |
| 推理成本 | 高(长提示) | 低 | 中(检索+生成) |
| 可溯源 | 弱 | 无 | 强 |
| 适合场景 | 少量固定知识 | 风格/格式定制 | 大规模动态知识 |
| 幻觉控制 | 一般 | 一般 | 较好 |
2.2 RAG真正解决的三个痛点
第一个痛点是知识时效性。任何预训练模型都有知识截止日期,昨天发生的事情它不可能知道。RAG把知识外置之后,这个问题自然消解。
第二个痛点是私有数据无法进入训练。企业内部文档、客户资料、代码库,这些东西根本不可能拿去训练公开模型。RAG让这些数据留在本地,只把检索到的片段送给模型,数据安全边界清晰。
第三个痛点是幻觉。模型在没有依据时会编造答案,这是自回归生成的固有特性。RAG通过"先检索、后生成"的流程,把模型的回答锚定在真实文档上,幻觉率会显著下降。注意我说的是"下降"而不是"消除",检索不到或者检索错了,模型照样会编,这一点后面会专门讲。
提示:如果你的场景是"知识相对固定、但需要模型改变说话风格或输出格式",那微调更合适;如果是"知识频繁变化、需要引用来源",RAG是唯一选择。两者也可以叠加使用。
3. RAG流水线的五个核心环节拆解
3.1 文档加载与清洗:最容易被低估的一步
很多人搭RAG的第一个坑就栽在这里。他们拿到一堆PDF、Word、网页,直接丢进切分器,结果检索出来的内容全是页眉页脚、乱码、表格错位。文档清洗的质量,直接决定了RAG效果的上限。
我在实际项目里的做法是分三步走。第一步是格式统一,把所有文档转成纯文本或Markdown,保留标题层级。PDF用解析库提取时要注意,扫描件需要先做OCR,双栏排版要按栏读取,否则文字顺序会乱。第二步是噪声去除,把页眉、页脚、页码、水印、重复的免责声明全部删掉。第三步是结构保留,标题、列表、代码块这些结构信息要尽量保留,因为它们对后续切分和检索都有帮助。
这里有个经验:不要试图用一套清洗规则处理所有格式。PDF、HTML、Markdown的噪声模式完全不同,分开处理反而更省事。我通常会为每种格式写一个独立的加载器,输出统一的中间结构(比如带层级信息的字典),再进入下一步。
3.2 文本切分:粒度决定召回质量
切分是RAG里最需要"手感"的环节。切得太粗,一个片段里混了好几个主题,检索时噪声大;切得太细,一个完整的意思被拆散,模型拿到手里拼不起来。
常见的切分策略有三种。固定长度切分最简单,按token数或字符数硬切,配合一定的重叠(overlap)避免语义断裂。递归切分会优先按段落、句子等自然边界切,实在超长再硬切,效果通常更好。语义切分用Embedding判断相邻句子的相似度,相似度骤降的地方就是切分点,效果最好但成本最高。
我在大多数项目里用的是递归切分,参数大致是这样:块大小500到800个token,重叠100到150个token。为什么是这个范围?因为太小(比如200)会导致单个块信息不足,检索出来模型看不懂;太大(比如2000)会稀释关键信息,向量表示变得模糊。重叠的作用是防止一个完整句子正好被切在边界上,导致两边都缺半句。
还有一个细节:切分时要带上元数据。每个块除了文本内容,还要记录它来自哪个文档、哪个章节、第几页。这些元数据在检索时可以用于过滤,在生成时可以用于引用来源,非常关键。
3.3 向量化与存储:Embedding模型怎么选
切分完的文本块要转成向量才能做语义检索,这一步靠的是Embedding模型。选型时主要看三个指标:检索质量、维度、推理速度。
检索质量看的是模型在语义相似度任务上的表现,一般用MTEB之类的榜单做参考。维度方面,常见的有384、768、1024、1536等,维度越高表达能力越强,但存储和计算成本也越高。推理速度则取决于模型大小和硬件,本地部署和API调用的取舍要结合数据量来定。
我的建议是:中小规模(百万级块以内)优先用成熟的API Embedding服务,省心且质量稳定;数据敏感或规模很大时再考虑本地部署开源模型。本地部署时要注意,不同模型对输入长度有限制,超长文本会被截断,切分时要提前考虑。
向量存到哪里?小规模可以用FAISS这种内存索引,简单直接;中大规模建议用专门的向量数据库,比如Milvus、Qdrant、Weaviate这类,它们支持持久化、分布式、元数据过滤。选型时重点看是否支持混合检索(向量+关键词)和元数据过滤,这两个功能在实际项目里非常有用。
3.4 检索:从"能查到"到"查得准"
检索环节的目标是把用户问题最相关的几个块找出来。最基础的是向量相似度检索,把问题也转成向量,在库里找最相近的Top-K个块。但纯向量检索有个问题:它对关键词不敏感。比如用户问一个具体的产品型号,向量检索可能返回一堆语义相近但型号不对的内容。
解决办法是混合检索:向量检索负责语义匹配,关键词检索(比如BM25)负责精确匹配,两路结果融合后排序。融合算法常用RRF(Reciprocal Rank Fusion),它对不同检索器的分数尺度不敏感,实现简单效果好。
Top-K取多少也有讲究。取太少(比如3),可能漏掉关键信息;取太多(比如20),噪声增加,还会挤占上下文窗口。我一般先用10到15做初筛,再通过重排序压缩到3到5个送给模型。
3.5 重排序:把最相关的顶到最前面
重排序(Rerank)是RAG效果提升的"隐藏大招"。它的原理是:初筛用的向量检索是"粗排",速度快但精度有限;重排序用一个更精细的模型(通常是Cross-Encoder),把问题和每个候选块拼在一起打分,精度高但速度慢,所以只对少量候选做。
实际效果上,加了重排序之后,最相关的块被顶到前面的概率会明显提升,模型拿到的上下文质量直接改善。代价是增加一次模型推理,延迟会上升几百毫秒。对于延迟敏感的场景,可以只对Top-20做重排取Top-5;对于质量优先的场景,可以扩大候选集。
注意:重排序模型和Embedding模型是两回事,不要混用。Embedding是双塔结构,问题和文档分别编码;重排序是单塔结构,问题和文档一起编码。前者快,后者准。
4. 从零搭一个可用的RAG问答系统
4.1 技术栈选型与目录结构
理论讲完,我们动手搭一个。为了让你能直接复现,我选一套最通用的技术栈:Python + 一个主流Embedding API + FAISS做向量存储 + 一个开源重排序模型 + 一个大模型API做生成。这套组合的好处是每一层都可以替换,你换成别的向量库或别的模型,整体逻辑不变。
目录结构我习惯这样组织:
rag_demo/ ├── data/ # 原始文档 ├── index/ # 向量索引持久化 ├── src/ │ ├── loader.py # 文档加载与清洗 │ ├── splitter.py # 文本切分 │ ├── embedder.py # 向量化 │ ├── retriever.py # 检索与重排序 │ └── generator.py # 答案生成 ├── config.yaml # 参数配置 └── main.py # 入口把参数抽到配置文件里是个好习惯,切分大小、Top-K、模型名称这些经常要调的东西,改配置比改代码方便得多。
4.2 核心代码:切分、向量化、检索三段式
先看切分。我用递归切分,优先按标题和段落切,超长再按句子切:
def split_document(text, chunk_size=600, overlap=120): separators = ["\n## ", "\n### ", "\n\n", "\n", "。", "!", "?"] chunks = [] # 递归按分隔符切分,直到每块小于chunk_size def _split(segment, seps): if len(segment) <= chunk_size or not seps: return [segment] sep = seps[0] parts = segment.split(sep) result = [] for i, part in enumerate(parts): if i > 0: part = sep + part result.extend(_split(part, seps[1:])) return result raw_chunks = _split(text, separators) # 合并过短的块,并添加重叠 for i, chunk in enumerate(raw_chunks): if i > 0: prev_tail = raw_chunks[i-1][-overlap:] chunk = prev_tail + chunk chunks.append(chunk.strip()) return [c for c in chunks if len(c) > 50]这段代码的关键点是:分隔符列表从粗到细排列,递归时优先用粗粒度分隔符,保证语义完整性;重叠部分取上一块的尾部,避免边界信息丢失;最后过滤掉过短的块,它们通常是噪声。
向量化和检索部分:
import faiss import numpy as np def build_index(chunks, embed_fn): vectors = [embed_fn(c) for c in chunks] matrix = np.array(vectors).astype("float32") faiss.normalize_L2(matrix) # 归一化后用内积等价余弦相似度 index = faiss.IndexFlatIP(matrix.shape[1]) index.add(matrix) return index def search(query, index, chunks, embed_fn, top_k=10): q_vec = np.array([embed_fn(query)]).astype("float32") faiss.normalize_L2(q_vec) scores, ids = index.search(q_vec, top_k) return [(chunks[i], s) for i, s in zip(ids[0], scores[0])]注意归一化这一步。FAISS的IndexFlatIP算的是内积,向量归一化之后内积就等于余弦相似度,这是标准做法。如果你忘了归一化,检索结果会偏向向量模长大的块,效果会明显变差。
4.3 生成环节的提示词设计
检索到相关块之后,要把它们和用户问题一起组装成提示词送给模型。这一步的提示词设计直接决定答案质量。我的模板大致是这样:
你是一个严谨的知识助手。请仅根据下面提供的资料回答问题。 如果资料中没有相关信息,请明确回答"根据现有资料无法回答",不要编造。 资料: {context} 问题:{question} 回答要求: 1. 只使用资料中的信息 2. 如果引用了具体内容,标注来源编号 3. 回答简洁,不要重复资料原文这个模板有三个关键约束:限定知识来源(只用资料)、允许拒答(无法回答时明说)、要求引用(标注来源)。第一条控制幻觉,第二条避免硬编,第三条方便用户核查。实测下来,这三条约束能显著提升答案的可信度。
组装context时,我会给每个块加上编号和来源信息,比如[1] 来源:产品手册第3章\n内容...。这样模型引用时可以直接说"根据[1]",用户也能对应到原文。
5. 效果调优:那些文档里不会写的实战经验
5.1 召回率上不去,先查这三个地方
RAG效果差,八成问题出在检索环节。我排查时按这个顺序来:
第一,看切分是否合理。把检索到的块打印出来读一遍,如果发现块里混了多个主题,或者一个完整意思被切断,那就是切分参数不对。调整块大小和重叠,重新建索引。
第二,看Embedding是否匹配语言和领域。有些Embedding模型对中文支持一般,有些对专业术语不敏感。换一个在中文或你所在领域表现更好的模型,效果可能立竿见影。
第三,看是否需要混合检索。如果用户查询里经常出现专有名词、型号、编号,纯向量检索会漏,加上关键词检索做融合。
我遇到过一个典型案例:某内部知识库问答,用户问"XX流程的第三步是什么",纯向量检索总是返回整个流程的概述,而不是第三步的具体内容。后来加了关键词检索,并且把切分粒度调细,让每个步骤独立成块,问题就解决了。
5.2 上下文窗口不是越大越好
很多人觉得既然模型支持长上下文,那就把所有检索结果都塞进去。这是个误区。上下文越长,模型对中间部分的注意力越弱(所谓的"lost in the middle"现象),而且token成本直线上升。
我的做法是:检索多、生成少。初筛拿10到15个候选,重排序后只取3到5个最相关的送给模型。这样既保证了召回,又控制了上下文长度。如果3到5个还不够,说明问题本身需要跨多个文档综合,这时候可以考虑多轮检索或者让模型先规划再检索。
5.3 拒答机制:让模型学会说"我不知道"
RAG系统最怕的不是答不出来,而是答错了还一本正经。所以拒答机制必须做。除了在提示词里明确要求,还可以在检索环节加一个相似度阈值:如果最高相似度低于某个值,直接返回"未找到相关资料",根本不调用生成模型。
阈值怎么定?没有统一标准,取决于你的Embedding模型和数据类型。我的经验是拿一批已知答案的问题跑一遍,看正确命中的相似度分布,取一个能过滤掉大部分无关查询的值。这个值需要根据实际badcase反复微调。
提示:拒答率太高会影响体验,太低会放进来噪声。建议先宽松后收紧,观察用户反馈再调整。
5.4 增量更新与索引维护
知识库不是一成不变的。新文档要加进来,旧文档要删除,这涉及到索引的增量更新。FAISS的IndexFlatIP不支持删除,只能重建;如果更新频繁,建议用支持增删的向量数据库。
我的做法是给每个块打上文档ID和时间戳,更新时先按文档ID删除旧块,再插入新块。对于大规模知识库,可以做成定时任务,每天凌晨重建一次索引,白天用旧索引服务,切换时做原子替换。
6. RAG的边界与常见误区
6.1 RAG解决不了的问题
RAG不是万能的。它擅长"基于已有文档回答问题",但不擅长需要推理和计算的任务。比如"根据这三份财报算出同比增长率",RAG只能把财报检索出来,计算还得靠模型或者外部工具。这类场景需要RAG配合工具调用(Function Calling)来做。
它也不擅长需要全局理解的任务。比如"总结这100份文档的共同主题",检索只能拿到局部片段,模型看不到全貌。这种场景要么用长上下文模型,要么先做聚类再总结。
还有一个常见误区是把RAG当搜索引擎用。RAG的输出是生成式答案,不是文档列表。如果用户需要的是"给我看相关文档",那应该做检索而不是RAG。
6.2 评估:怎么知道你的RAG好不好
没有评估的调优都是瞎调。RAG的评估分两个层面:检索质量和生成质量。
检索质量看召回率(相关文档有没有被检索到)和准确率(检索到的有多少是相关的)。生成质量看答案是否忠实于检索内容(faithfulness)、是否回答了问题(relevance)。
实操上,我会准备一个几十到几百条的问题集,每条标注标准答案和应该检索到的文档。每次调整参数后跑一遍,看指标变化。这个工作前期投入大,但后期调优效率会高很多。没有这个集合,你只能靠感觉,很容易越调越乱。
6.3 一个容易被忽略的细节:查询改写
用户的问题往往很口语化、很模糊,直接拿去检索效果不好。查询改写(Query Rewriting)就是把用户问题转成更适合检索的形式。简单做法是用模型把问题扩写成几个关键词或几个子问题;复杂做法是做多轮检索,每轮基于上一轮结果调整查询。
我常用的一个技巧是HyDE(Hypothetical Document Embeddings):先让模型根据问题"编"一个假设性的答案,再用这个答案去检索。原理是答案和文档的语义空间更接近,比问题本身检索效果更好。这个方法在问题很短、很模糊时特别有效。
7. 我踩过的坑和几条实用建议
第一个坑是切分时把表格切碎了。表格在纯文本里是一堆用空格或竖线分隔的行,按字符切分很容易把表头和表体分开,检索出来完全看不懂。后来我改成表格整体保留,超长表格按行切但每块都带上表头。
第二个坑是Embedding模型换了但没重建索引。不同模型的向量空间不兼容,换了模型必须重新向量化所有块,否则检索结果完全是乱的。这个错误我犯过一次,排查了半天才发现。
第三个坑是忽略了元数据过滤。有些查询天然带过滤条件,比如"2024年的政策",如果检索时不按年份过滤,会混进来一堆旧文档。加上元数据过滤之后,准确率提升很明显。
几条建议:先把数据质量做好再谈模型调优,脏数据上跑再好的模型也白搭;保留完整的检索日志,出问题时能回溯是哪一步错了;小步快跑,每次只改一个变量,改完立刻评估,不要一次调一堆参数然后不知道是哪个起了作用。
RAG这个方向还在快速演进,从最初的朴素检索到现在的混合检索、重排序、查询改写、多轮检索,工程手段越来越丰富。但核心逻辑没变:把对的知识在对的时刻送给模型。把这条主线抓住,具体技术怎么变都能应对。