RAG 这个词这两年出现的频率太高了,高到很多刚入行的朋友以为它是个新框架或者新工具,其实它更像是一种"给大模型外挂大脑"的工程思路。我最早接触 RAG 是在做一个内部文档问答的需求,当时天真地以为把 PDF 丢给模型就能问出答案,结果被现实按在地上摩擦——模型要么答非所问,要么一本正经地胡说八道。后来才明白,问题的根子不在模型本身,而在于它压根不知道你那些私有文档里写了什么。RAG 要解决的就是这件事:让模型在回答问题之前,先去你的知识库里把相关资料捞出来,再基于这些资料组织答案。
这篇文章我想聊的不是"RAG 是什么"这种概念科普,而是把一个 RAG 应用从零搭起来的过程中,那些真正会卡住你的地方。包括文档怎么切、向量库怎么选、检索为什么不准、召回和生成的边界在哪、以及我踩过的那些坑。适合已经了解 LLM 基本用法、想动手做一个能用的 RAG 应用的开发者,也适合正在被检索效果折磨、想找优化方向的朋友。全文会围绕检索增强生成、向量数据库、LLM 调用这几条主线展开,尽量把每一步的"为什么"讲清楚。
1. 先把 RAG 的数据流想明白再动手
很多人一上来就装 LangChain、连向量库、调 API,代码跑通了但效果一塌糊涂,根本原因是没搞清楚数据在系统里到底是怎么流动的。RAG 的本质是一条流水线,任何一个环节出问题,最终答案都会崩。我习惯在动手前先把这条链路画在纸上,确认每个节点的输入输出。
1.1 一条完整的 RAG 链路到底经过哪些环节
从用户敲下问题到看到答案,中间其实经过了六个阶段。第一阶段是文档加载,把 PDF、Word、Markdown、网页这些异构数据读成纯文本。第二阶段是文本切分,因为模型和向量库都有长度限制,必须把长文档切成小块。第三阶段是向量化,用 Embedding 模型把每个文本块转成一串浮点数。第四阶段是存储与索引,把这些向量存进向量数据库并建立索引结构。第五阶段是检索,用户提问时把问题也向量化,去库里找最相似的若干块。第六阶段是生成,把检索到的文本块拼进 Prompt,交给 LLM 生成最终答案。
这六个阶段里,前四个是离线预处理,通常只在文档更新时跑一次;后两个是在线查询,每次用户提问都要走一遍。区分离线在线很重要,因为优化手段完全不同。离线阶段你可以慢慢跑、可以重试、可以用大模型做增强;在线阶段对延迟敏感,每一步都要抠时间。
我见过不少人把 Embedding 和 LLM 混为一谈,以为用了 GPT 做生成就不需要单独的 Embedding 模型了。这是两码事。Embedding 模型负责把文本映射到向量空间,追求的是语义相似度计算准确;LLM 负责根据上下文生成自然语言,追求的是表达流畅和逻辑合理。它们可以来自不同厂商,甚至本地部署的 Embedding 配云端 LLM 也是常见组合。
1.2 为什么"检索"才是 RAG 的真正瓶颈
刚做 RAG 的时候,我把大量精力花在调 Prompt 和换模型上,觉得答案不好是模型不够聪明。后来做了一组对照实验才发现,把检索结果直接打印出来看,很多时候压根就没召回正确的文档块。模型再强,你给它喂的是错的资料,它也变不出正确答案。这就是所谓的"garbage in, garbage out"。
检索的瓶颈体现在三个层面。第一是语义鸿沟,用户问"怎么退款",文档里写的是"申请售后返还货款",字面完全不重叠,纯关键词匹配就废了,必须靠向量语义匹配。第二是粒度问题,一个文本块太大,里面混了好几个主题,向量就被平均掉了,相似度算不准;块太小,又丢失了上下文,模型拿到手也不知道在说什么。第三是多义与歧义,同一个词在不同语境下含义不同,向量空间里却可能挤在一起。
所以我的经验是,RAG 项目里 70% 的调试时间应该花在检索上,而不是生成上。检索准了,哪怕用个小模型生成,答案也能看;检索不准,上再贵的模型也是白搭。这个认知转变是我做 RAG 的第一个分水岭。
1.3 离线索引和在线查询的边界怎么划
划清离线在线的边界,直接决定了你的系统架构。离线部分我通常做成一个独立的索引构建任务,输入是原始文档目录,输出是向量库里的数据。这个任务可以定时跑,也可以手动触发,关键是它和在线服务解耦。在线部分就是一个查询服务,接收用户问题,走检索加生成,返回答案。
这样划分的好处是,文档更新不会影响在线服务的稳定性,索引重建时可以慢慢跑不怕超时。而且离线阶段可以做很多"重"操作,比如用 LLM 给每个文本块生成摘要、抽取关键词、打标签,这些增强信息能显著提升后续检索质量,但在线阶段根本没时间做。
有个细节容易被忽略:离线索引和在线查询必须用同一个 Embedding 模型。我踩过一次坑,离线用 A 模型建的库,上线时手滑换成了 B 模型,结果检索出来的全是无关内容,因为两个模型的向量空间根本不兼容。这个错误排查起来很费劲,因为代码不报错,只是效果差,很容易误以为是别的问题。
2. 文档切分:决定 RAG 上限的第一道关卡
如果只能给 RAG 新手一条建议,我会说:把文档切分做好,你的 RAG 就成功了一半。切分策略直接决定了检索的最小单元,单元切得不好,后面所有环节都在为这个错误买单。我在这上面交的学费最多,也最有心得。
2.1 固定长度切分为什么经常翻车
最简单的切分方式是按固定字符数切,比如每 500 字一块,块之间留 50 字重叠。这个方法实现简单,但问题很明显。它完全不管语义边界,经常把一句话从中间劈开,或者把一个小节的标题和内容分到两块里。用户问的问题如果正好落在这个边界上,检索就会召回半截内容,模型看了也懵。
重叠窗口是为了缓解这个问题,让相邻块共享一部分内容,保证语义连续性。但重叠比例不好定,太小没效果,太大又造成冗余,检索时可能召回好几个高度相似的块,浪费上下文窗口。我一般把重叠设在块大小的 10% 到 20% 之间,具体看文档的句子平均长度。
固定长度切分不是不能用,它适合那种结构松散、没有明显章节的文本,比如聊天记录、日志。但只要文档有标题层级、有段落结构,就应该优先用结构化切分。
2.2 按语义和结构切分的实操思路
结构化切分的核心思想是顺着文档本身的组织方式来切。Markdown 有#、##、###,HTML 有标签层级,PDF 虽然麻烦但也能通过字体大小和加粗推断标题。我通常先按最高级标题切大块,如果某块还是太长,再按次级标题切,直到每块落在合理长度区间。
这里有个关键参数是目标块大小。太小检索准但上下文不足,太大上下文全但噪声多。我的经验值是中文 300 到 800 字,英文 200 到 500 词。这个区间是权衡的结果:足够容纳一个完整论点,又不至于混入太多无关信息。当然这要看你的文档密度,技术文档信息密度高,可以小一点;叙述性文档可以大一点。
对于代码和技术文档,我还会做特殊处理。代码块尽量保持完整,不要从中间切断,因为半截代码毫无意义。表格也尽量整块保留,或者转成文字描述。这些细节看起来琐碎,但对检索质量影响很大。
2.3 给每个文本块补上"上下文身份证"
这是我个人觉得最有价值的一个技巧:给每个切分后的文本块附加元数据。光有一段文字,向量化后丢失了它在原文中的位置信息。如果我在块前面加上"本文档标题:XXX,所属章节:YYY,本块内容:……",检索时这段前缀也会参与向量计算,能显著提升相关性。
元数据还能用于过滤。比如用户问的是某个产品的问题,我可以在检索时先按产品名过滤,只在相关文档里找,大大缩小搜索范围。这在多产品、多版本的文档库里特别有用。元数据字段我一般会存:文档来源、章节路径、块序号、创建时间、文档类型。这些信息在后续做引用溯源时也用得上,用户能看到答案是从哪份文档的哪一节来的,信任度会高很多。
提示:元数据不要塞太多,否则会稀释正文的向量表达。前缀控制在 50 字以内比较合适,只放最关键的定位信息。
3. 向量数据库选型:别被参数表忽悠
向量数据库这个赛道卷得厉害,各种产品参数表看得人眼花。但选型不是比谁的功能多,而是看你的场景需要什么。我做过几个不同规模的项目,从单机小工具到百万级文档的服务,选型逻辑差别很大。
3.1 从数据规模和部署方式倒推选型
选型第一步是搞清楚你的数据量级和部署环境。如果只是本地跑个小知识库,文档几百上千个,那根本不需要专门的向量数据库,用 FAISS 这种内存索引库就够了,甚至 numpy 手写余弦相似度都能扛。这个阶段追求的是简单,别给自己找麻烦。
数据量到了十万级、需要持久化和并发查询,就该上真正的向量数据库了。这时候要考虑是自建还是用云服务。自建的话 Milvus、Qdrant、Weaviate 都是成熟选择,云服务则省去了运维但成本高。我的判断标准是:如果团队没有专职运维,且数据量不是特别大,优先考虑云服务或者轻量级自建方案。
百万级以上、对延迟和可用性有硬要求,才需要认真对比分布式能力和索引算法。这个阶段 HNSW、IVF、PQ 这些索引结构的取舍就变得重要了,因为它们直接决定召回率和查询速度的平衡。
3.2 几个主流方案的实测对比
我把用过的几个方案列个表,说说真实感受,参数表上查不到的那种。
| 方案 | 适合场景 | 实测优点 | 实测坑点 |
|---|---|---|---|
| FAISS | 本地小规模、离线实验 | 快、零依赖、纯内存 | 无持久化、无并发、重启即丢 |
| Qdrant | 中小规模自建 | 部署简单、过滤功能强 | 大规模下内存占用偏高 |
| Milvus | 大规模生产 | 生态全、扩展性好 | 组件多、运维门槛高 |
| pgvector | 已有 Postgres 的团队 | 复用现有数据库、事务一致 | 超大规模性能不如专用库 |
| 云托管服务 | 无运维团队 | 开箱即用、弹性扩容 | 成本随规模线性上涨 |
这张表里我最想强调的是 pgvector。很多团队本来就在用 Postgres,加个扩展就能存向量,省去了引入新组件的麻烦,数据一致性也好保证。对于中小规模应用,这是被低估的选择。我有个项目就是 pgvector 扛的,几十万条向量查询延迟完全可接受。
3.3 索引参数调优:召回率和速度的跷跷板
向量索引本质上是在做近似最近邻搜索,用一点点召回率的损失换取查询速度的大幅提升。HNSW 是目前最常用的索引,它有两个关键参数:M控制每个节点的连接数,ef_construction控制建索引时的搜索范围。M 越大、ef_construction 越大,索引质量越高但建索引越慢、内存占用越大。
查询时还有个ef_search参数,它控制查询时的搜索范围。这个参数可以在运行时调,是调节召回率和延迟最直接的旋钮。我的做法是先设一个较高的 ef_search 保证召回,测出延迟,再逐步往下调,找到满足延迟要求的最低值。
这里有个反直觉的点:索引不是越精确越好。有时候稍微降低召回率,把省下来的时间用于召回更多候选块再重排,整体效果反而更好。这就是后面要讲的"召回加精排"两阶段检索的思路。
4. 检索质量优化:从"能查到"到"查得准"
检索是 RAG 的心脏。前面把数据和索引准备好了,这一节聊怎么让检索真正准起来。我把它分成三个层次:查询改写、混合检索、重排序。这三招叠加使用,效果提升非常明显。
4.1 查询改写:用户的问题往往不是好的检索词
用户提问的方式和文档写作的方式经常对不上。用户问"这个功能怎么收费",文档里可能写的是"计费规则说明"。直接拿用户原话去检索,命中率堪忧。解决办法是在检索前先对查询做改写或扩展。
最简单的是同义词扩展,把"收费"扩展成"收费、计费、价格、费用"。稍微高级点的是用 LLM 做查询改写,让模型把口语化的问题改写成更适合检索的关键词组合,或者生成多个不同角度的查询。我常用的一招是让 LLM 基于用户问题生成三个改写版本,分别检索后合并结果,召回率提升立竿见影。
还有个技巧叫HyDE,思路是先让 LLM 根据问题"编"一个假想的答案,然后用这个假想答案去检索。因为假想答案的措辞更接近文档风格,检索效果往往比原问题好。这个方法听起来有点玄,但实测确实有效,尤其适合问答式文档。
4.2 混合检索:向量加关键词才是稳的
纯向量检索擅长语义匹配,但对精确匹配不敏感。比如用户搜一个特定的错误码"ERR_5023",向量检索可能召回一堆语义相近但错误码不同的内容。这时候关键词检索(BM25)就派上用场了,它能精确匹配这些专有名词。
混合检索就是把向量检索和关键词检索的结果融合。融合方式有加权的,也有用 RRF(倒数排名融合)的。RRF 不需要调权重,对两路结果按排名做融合,鲁棒性好,我一般优先用它。实测下来,混合检索在专有名词、代码、编号这类查询上,比纯向量检索强太多。
实现上,很多向量数据库已经内置了混合检索能力,比如同时支持稠密向量和稀疏向量。如果没有,也可以分别查两个库再在应用层融合,只是多一次查询开销。
4.3 重排序:把最相关的顶到最前面
检索召回 Top-K 之后,顺序其实不一定对。向量相似度高不代表真的相关,因为 Embedding 模型的能力有限。这时候引入一个重排序模型(Reranker),对召回的候选块做精细打分,重新排序,把真正相关的顶上来。
Reranker 通常是交叉编码器结构,它把问题和候选块拼在一起过一遍模型,输出相关性分数。它比向量点积准得多,但慢得多,所以只适合对少量候选做精排。典型流程是:向量检索召回 50 个,Reranker 精排出 Top 5,再交给 LLM。这个"粗召回加精排序"的两阶段结构,是我目前认为性价比最高的检索方案。
选 Reranker 时要注意它和你的语言、领域是否匹配。通用 Reranker 在专业领域可能表现一般,有条件的话可以用领域数据微调。不过对大多数应用,现成的开源 Reranker 已经够用。
5. 生成环节:Prompt 设计和上下文管理
检索把资料找齐了,最后一步是让 LLM 基于资料生成答案。这一步看似简单,其实 Prompt 的写法直接决定答案质量。我见过太多人随便拼个 Prompt 就上线,然后抱怨模型不听话。
5.1 把检索结果"喂"进 Prompt 的正确姿势
拼接上下文时,我习惯给每个文本块加上编号和来源,比如"[1] 来源:产品手册第三章……"。这样模型在生成时可以引用编号,用户也能追溯。块与块之间用明确的分隔符隔开,避免模型把它们混在一起理解。
Prompt 里必须明确告诉模型:只根据提供的资料回答,资料里没有的信息就说不知道。这句话能大幅减少幻觉。如果不加这句,模型很容易用自己的先验知识补充,而这些补充可能是错的。我还会要求模型在答案里标注引用了哪几块资料,方便验证。
上下文长度要控制。虽然现在模型支持很长的上下文,但塞太多无关内容反而会干扰模型。我的做法是精排后只取 Top 3 到 Top 5,宁可少而精,不要多而杂。如果确实需要更多信息,就分多轮检索,而不是一次性全塞进去。
5.2 处理"检索不到"和"资料冲突"的情况
真实场景里,检索不到相关资料是常态。这时候系统应该明确告诉用户"没有找到相关信息",而不是让模型硬编一个答案。我在 Prompt 里会设定一个兜底逻辑:如果资料与问题无关,直接回复未找到,并建议用户换个问法或联系人工。
资料冲突也很常见,尤其是文档有多个版本时。我的处理方式是让模型指出存在冲突,并分别列出不同来源的说法,把判断权交给用户。这比模型自作主张选一个要诚实得多。元数据里的时间戳在这里就有用了,可以优先采用较新的版本,但要明确告知用户。
5.3 引用溯源:让答案可验证
引用溯源是我认为 RAG 相比纯 LLM 最大的优势之一。答案不是凭空来的,而是有据可查。实现上,我在生成时要求模型输出引用的块编号,前端再把编号映射回原文位置,用户点击就能跳转。这个功能对内部知识库、客服系统特别重要,因为用户需要确认答案的权威性。
溯源做得好,还能反过来帮你调试。当用户反馈答案错误时,你能快速定位是检索错了还是生成错了。如果引用的是无关块,那是检索问题;如果引用正确但答案错,那是生成问题。这个区分能省下大量排查时间。
6. 那些文档里不会写的踩坑记录
前面讲的都是方法论,这一节聊点实在的,都是我实际踩过的坑,希望能帮你少走弯路。
6.1 中文分块和英文分块的差异
中文没有空格分词,按字符数切和按词切效果差别很大。我一开始用英文那套按空格切词的方法处理中文,结果切出来的块惨不忍睹。中文切分要么用专门的分词工具,要么按标点符号切句子再组合。标点切分简单有效,句号、问号、感叹号、分号都是天然边界。
还有个坑是中文的标点全角和半角混用,正则匹配时容易漏。我写切分逻辑时会把全角半角都考虑进去,否则会出现句子切不断的情况。这些细节很小,但不处理就会影响切分质量。
6.2 Embedding 模型的维度陷阱
不同 Embedding 模型输出的向量维度不同,从 384 维到 1536 维甚至更高都有。维度高不代表效果好,有些高维模型在特定领域反而不如低维的。选模型要看它在你的语言和领域上的表现,最好用一批真实查询做评测,别只看榜单。
另一个坑是归一化。有些模型输出的向量已经归一化了,有些没有。如果混用,余弦相似度计算会出错。我一般统一做一次归一化,确保一致性。这个错误很隐蔽,因为不报错,只是相似度算出来偏大或偏小。
6.3 增量更新时的一致性维护
文档会更新,索引也得跟着更新。最简单的做法是删掉旧块、插入新块,但这里有个坑:如果文档只是小改,重新切分可能导致块边界变化,旧块的 ID 和新块对不上,删除时容易漏。我的做法是给每个块一个基于文档 ID 加内容哈希的稳定 ID,更新时按文档 ID 批量删除再重建,保证不残留。
还有个问题是更新期间的查询一致性。如果一边删一边插,用户可能查到半新半旧的结果。生产环境我会用双索引切换:新索引建好后再原子切换,避免中间态。这个成本高一点,但对用户体验值得。
6.4 评测:没有评测就没有优化
最后强调一个最容易被忽略的环节:评测。没有评测,你所有的优化都是凭感觉,改了半天不知道有没有变好。我建议一开始就建一个小规模的评测集,几十条真实问题和对应的标准答案,每次改动后跑一遍,看召回率和答案准确率的变化。
评测指标我主要看两个:检索的召回率(正确文档块有没有被召回)和生成的准确率(答案对不对)。这两个指标分开看,才能定位问题出在哪一环。评测集不用大,但要覆盖典型场景和边界情况。随着项目迭代,评测集也要不断补充,把线上发现的 bad case 加进去。
注意:评测集里的标准答案要人工确认,不能直接用模型生成,否则就是拿模型评模型,没有意义。
7. 从 Demo 到生产还差哪些工程化工作
跑通一个 Demo 可能只要一下午,但要让它在生产环境稳定服务,还有不少工程活要干。这一节聊聊那些容易被低估的部分。
7.1 缓存策略:省下的是真金白银
RAG 每次查询都要调 Embedding 和 LLM,成本不低。缓存能省很多钱。我一般做两层缓存:一层是查询结果的缓存,相同问题直接返回;另一层是 Embedding 的缓存,相同文本不重复计算。查询缓存要注意失效策略,文档更新后相关缓存要清掉。
语义缓存是个进阶玩法,不是精确匹配问题,而是判断新问题和缓存问题语义是否相近,相近就复用答案。这能提高命中率,但要用向量相似度判断,阈值设不好会返回错误答案。我一般把阈值设得保守一点,宁可多算一次也不返回错答案。
7.2 延迟优化:用户等不了太久
RAG 的延迟主要来自三块:Embedding 计算、向量检索、LLM 生成。Embedding 和检索通常很快,几十毫秒级别,大头在 LLM 生成。优化生成延迟可以用流式输出,让用户先看到部分答案,感知上快很多。
检索阶段如果用了 Reranker,也会增加延迟。我的做法是给 Reranker 设个超时,超时就跳过精排直接用粗排结果,保证响应时间可控。这种降级策略在生产环境很重要,宁可效果差一点也不能让用户干等。
7.3 监控与反馈闭环
上线不是终点。我会监控几个关键指标:查询量、平均延迟、检索召回率、用户反馈。用户反馈是最宝贵的信号,点赞点踩都要记录,定期分析踩的案例,找出共性问题。
我还会记录每次查询的完整链路:原始问题、改写后的问题、召回的块、重排后的块、最终答案。这些日志在排查问题时价值巨大。有了这些数据,你就能持续迭代,让系统越用越准。
8. 关于 RAG 能力边界的一些实话
做了这么多 RAG 项目,我对它的能力边界有了比较清醒的认识,这里说几句实话,可能不太中听但有用。
RAG 不是万能的。它擅长的是"从已有资料里找答案",如果你的需求是推理、计算、创意生成,RAG 帮不上太多忙。有些团队把 RAG 当成解决一切问题的银弹,结果发现效果不达预期,其实是场景选错了。
RAG 的效果高度依赖文档质量。如果原始文档本身就混乱、过时、自相矛盾,RAG 只会把这些混乱放大。我见过一个项目,文档是十年前写的,里面很多信息已经失效,RAG 检索出来照样喂给模型,答案自然错得离谱。这种情况下,先整理文档比优化 RAG 更有效。
还有一个现实是,RAG 的维护成本不低。文档在变、模型在更新、用户需求在变,系统需要持续投入。它不是搭好就一劳永逸的东西。如果团队没有持续维护的准备,不如先用简单方案顶着。
我个人在实际操作中的体会是,RAG 项目成功的关键不在技术选型多先进,而在对业务场景的理解有多深。你得知道用户真正会问什么问题,文档里真正有什么内容,两者之间的鸿沟在哪。把这些想清楚了,技术方案自然就出来了。反过来,如果只是堆技术,再花哨的架构也解决不了实际问题。