- Published on
RAG 不只是接个向量库:从最小系统到工程判断
- Authors
- Name
RAG 不只是接个向量库
很多人第一次做 RAG,会把它想得过于乐观。
做法通常也差不多:找一批文档,切块,做 embedding,塞进向量库,用户提问时检索几个 chunk,把结果连同问题一起丢给模型。跑通以后,页面上真的能吐出答案,于是大家很容易得出一个结论: “RAG 已经做完了,后面无非是调调参数。”
这通常就是第一层错觉。
RAG 真正难的地方,从来不是“把检索接到模型前面”。难点在于另一件更不体面的事:你要让整条链路在脏数据、模糊问题、不稳定语义边界和有限上下文里,仍然尽可能稳定地工作。
如果这件事没有做好,系统表面上看起来会很聪明,实际上只是把“像真的”伪装成“是真的”。
这也是我现在看 RAG 的基本立场:
- 它不是一个模型技巧,而是一条系统链路
- 它不是“有没有接检索”的问题,而是“每一层有没有失真”的问题
- 它最怕的不是完全答不出来,而是答得像对,但依据是错的
所以这篇文章不打算做一份“RAG 全景科普”,也不想列一串框架名词。更现实一点,我想把它拆回工程问题:
- 一个最小可工作的 RAG,到底最少要包含什么
- 真正决定效果的地方在哪里
- 第一版系统通常会死在哪
- 没有评估时,为什么大多数“优化”其实都不可信
- 什么时候才值得上 Hybrid Search、Rerank、Query Rewrite 这些更复杂的东西
如果你已经写过一点 LLM 应用,但还没完整做过 RAG,希望这篇能帮你少踩一些很常见、但又很少有人认真拆开的坑。
RAG 到底在解决什么问题
先把问题说得朴素一点。
语言模型擅长的是压缩、归纳、补全和生成。它知道很多模式,但它对“你这次真正允许它依据什么回答”并没有天然约束。训练语料不是你的私有知识库,参数记忆也不是可靠索引。于是只要问题稍微具体一点,尤其是涉及私有文档、内部流程、时效信息或长尾细节,模型就会很自然地滑向两个方向:
- 用旧知识补空白
- 用最像答案的话把空白补平
RAG 的价值,本质上就是给模型加一条外部证据链。
它想解决的不是“让模型更聪明”,而是“让回答尽量建立在这次可见、可控、可追溯的资料上”。换句话说,RAG 要做的是把生成问题改写成一个受检索约束的问题:
用户问题
-> 去外部知识源里找相关证据
-> 把证据按一定形式送进上下文
-> 要求模型尽量依据这些证据回答
这听起来不复杂,但麻烦在于每一步都可能失真。
用户表达可能很模糊,文档本身可能很脏,切块可能切断语义,向量检索可能召回到“看起来相关、实际上不关键”的内容,模型也可能拿着半对不对的上下文,生成一段极有说服力的废话。
所以 RAG 的核心并不是“检索 + 生成”这四个字,而是下面这条链:
原始文档 -> 清洗/切块 -> 索引 -> 检索 -> 重排 -> 上下文组装 -> 生成 -> 评估
你不控制这条链,就别太相信最后那个答案。
一个能工作的 RAG,最小也得包含什么
如果只是想做第一个版本,我认为别上来就追“全功能架构”。先做最小闭环,能解释问题比能堆特性重要得多。
一个够用的最小 RAG,至少要有这几层:
1. 文档导入
第一步不是模型,是输入。
你得有办法把 Markdown、网页、PDF 或内部文档转成统一、可处理的文本结构。这里最容易被忽略的一点是:原始文档通常并不天然适合拿来检索。
常见问题包括:
- 导航、页眉、版权、目录一起混进正文
- PDF 解析后段落顺序乱掉
- 表格和列表被打平成难以理解的碎片
- 多个主题被塞进一个很长的段落里
如果输入本身就脏,后面做得再认真,也只是更精准地检索垃圾。
2. 切块
切块不是预处理细节,它往往就是第一大效果分水岭。
检索系统从来不是在检索“文档”,而是在检索“被切出来、可以被索引的最小单位”。这个单位如果边界不合理,整个系统从一开始就偏了。
切块太大,会发生两件事:
- 语义太混,真正有用的信息被大量无关上下文包住
- 即使召回命中,送给模型的证据也不够锐利
切块太小,也会发生两件事:
- 语义断裂,关键前提和结论被拆散
- 检索结果变得像关键词碰撞,而不是完整语义片段
所以别把 chunk size 当成一个“调调 512 还是 1024”的机械参数。它本质上是在回答一个更重要的问题:你的知识库里,什么才是最小有效知识单元?
对 API 文档,这个单位可能是“一个 endpoint 的说明 + 参数 + 限制”。
对技术博客,这个单位可能是“一个完整小节”。
对排障手册,这个单位可能是“一个故障现象 + 原因 + 处理步骤”。
如果这个边界没想清楚,后面很多问题都会被你误判成 embedding、模型或者向量库的问题。
3. 索引和检索
最小版完全可以先用最朴素的语义检索。
你不需要一上来就上复杂向量架构,也不需要先做多路召回。第一版的目标很简单:
- 对相关问题,能召回大体正确的内容
- 对不相关问题,不要把明显噪音排到前面
- 出错时,能够定位到底是“没召回”还是“召回错了”
这一步最重要的不是 fancy,而是可解释。
如果检索层自己都讲不清为什么拿回这几个 chunk,后面的生成层其实没法认真 debug。
4. 上下文组装
很多人做 RAG,只关注“召回了哪些 chunk”,但忽略了另一个关键动作:你最后是怎么把它们交给模型的。
这一步至少包括:
- 选多少个 chunk
- 用什么顺序拼接
- 是否附带标题、来源、时间、文档类型
- 是否要求模型只基于给定资料回答
- 当资料不足时,是否允许明确说不知道
这里的失误非常常见。比如你召回到了相关内容,但把五六段互相弱相关的文本一股脑塞进去,模型看到的是一个噪音浓度很高的上下文包。它未必完全瞎说,但很可能会挑看起来最像答案的句子,自行缝出一段“合理”的输出。
这类错误,表面看像模型理解差,实际上是上下文污染。
5. 生成和回答约束
RAG 不是“把检索结果贴给模型,然后祈祷它守规矩”。
你至少得明确回答策略:
- 没有足够证据时,该不该拒答
- 有多个来源冲突时,能不能指出冲突
- 是要直接给结论,还是先给依据再下结论
- 是否要求引用出处
如果这一层没有清晰约束,系统会自动往“更像有用助手”的方向滑,而不是往“更像可靠系统”的方向滑。
前者用户一开始会觉得顺手,后者才是真的能上线。
真正决定效果的,通常不是模型,而是检索链路
外面聊 RAG,讨论很容易歪到模型选择上:用哪个 embedding,哪个大模型,哪个框架,哪个推理供应商。
这些当然有影响,但很多项目里,它们并不是主导因素。
更常见的现实是:系统效果的上限,在模型出场之前就已经被前面几步决定掉了。
文档边界决定你能不能找到“完整证据”
如果一个有效答案需要三段连续说明,而你把它切成三个彼此独立的小块,检索阶段很可能只拿回其中一段。模型看到残片以后,最擅长做的事不是承认上下文不足,而是补。
这时你看到的是一个语言上流畅、逻辑上也不完全离谱的回答,但依据已经断了。
召回质量决定你是在“找证据”还是“找相似句子”
语义检索很容易出现一种看起来合理的误命中:词义相近,主题相关,但不是用户真正需要的那个片段。
比如用户问“为什么导入后的 PDF 片段顺序会错”,系统可能召回“PDF 支持说明”“导入流程介绍”“文本清洗配置”,它们都相关,但没有回答真正的问题。
这类结果如果排在前面,模型照样会给你一个语气坚定的回答。
上下文噪音决定模型还能不能做正确压缩
模型并不是看到更多信息就一定更准。上下文越长,越要求输入有组织。
如果你把相关内容和一堆边缘内容混在一起,模型要先做一次“上下文内检索”,再做总结。这个过程本身就会掉信息,而且掉的往往不是废话,而是那些位置不起眼、但真正起决定作用的句子。
所以很多人以为自己在做“增强生成”,其实是在做“增强干扰”。
第一版系统通常死在哪
如果你第一次做 RAG,我建议优先怀疑下面这几个地方。它们比“模型不够强”更常见,也更值得先查。
1. 文档导入质量很差,但没人承认
这类问题最讨厌,因为它不像报错那样直白。系统能跑,检索也有结果,但结果就是不稳定。
常见症状:
- 同样的文档,不同问题表现差异非常大
- 明明文档里有答案,但总是召回不到关键段
- 检索结果里混入大量目录、页脚、模板句
这通常不是“检索玄学”,而是你喂进去的文本结构已经坏了。
2. chunking 是默认参数,没人真的看过样本
很多项目的 chunk 策略是这样定的:
“先切 500 个 token,overlap 设 50,大家都这么干。”
这不是策略,这只是把责任推给默认值。
正确做法很土,但有效:抽样看几十个 chunk,直接用人眼判断这些片段是不是一个完整可检索单位。你会很快发现很多配置根本不适合你的文档类型。
3. 检索命中了,但命中的不是决定性内容
这是 RAG 里特别常见的一种误判。
系统返回了一堆相关文本,于是大家以为“检索没问题,问题在生成”。实际上,模型拿到的可能只是背景信息,不是能支撑答案的关键证据。
相关,不等于够用。
4. 没有把“检索错误”和“生成错误”分开
只要你没把这两类问题拆开,后面的所有优化几乎都会变成凭感觉。
检索错误是:该找的没找回来,或者顺序明显不对。
生成错误是:证据已经给到了,但模型还是理解错、总结错、扩写过头或者编造了证据之外的内容。
这两类问题的解决手段完全不同。前者要看索引、chunking、召回和重排;后者才需要看 prompt、回答格式、模型能力和上下文组织。
把它们混为一谈,是很多团队一直在“调”,却一直调不明白的根本原因。
5. 评估缺位,导致每次改动都像在猜
这是最根本的一条。
没有评估集、没有失败样本、没有分层指标时,所谓优化大概率只是错觉管理。今天你换个 embedding,好像顺了一点;明天你加个 reranker,又觉得专业了不少;后天换个 prompt,似乎引用更稳了。
但如果没有统一问题集和前后对照,这些感觉没有太大价值。
工程不是靠“看起来更像了”推进的。
怎么做一个最小但可验证的版本
我现在更认同一种比较克制的路线:先做一个土一点、但能解释问题的版本。
第一版完全可以只做这些:
- 文档源先只支持 Markdown
- 手动清洗掉明显无关内容
- 按段落或小节做切块,再补少量 overlap
- 用一个成熟 embedding 模型做索引
- 检索先只做 top-k 语义召回
- 把召回结果和来源一起传给模型
- 明确要求模型“只基于给定材料回答,不足时直说”
这个版本不需要花哨,但必须回答三个问题:
- 系统有没有把真正相关的证据找回来
- 模型有没有依据这些证据回答
- 回答错时,我能不能定位错在检索还是生成
只要这三个问题还回答不清楚,就没必要急着往上叠复杂能力。
为了让最小版更容易调试,我建议一开始就保留这些日志:
- 原始问题
- 检索 query
- 召回的 chunk 列表和分数
- 每个 chunk 的来源文档
- 最终送给模型的上下文
- 最终回答
很多“神秘 bug”,你把这几项打印出来以后,就没那么神秘了。
没有评估,就别太认真谈优化
RAG 很容易给人一种危险的错觉:你问几个顺手的问题,都答对了,于是系统看起来已经“八九不离十”。
这对 demo 也许够,对系统完全不够。
我认为第一版能跑起来以后,下一步不是立刻上高级特性,而是先建评估。
至少要把评估拆成两层:
检索层评估
核心问题:
- 应该命中的证据,有没有被召回
- 命中的证据,排位是否足够靠前
- 召回结果里噪音比例高不高
如果这一层都不稳,后面生成回答再漂亮,也只是建立在摇晃地基上。
回答层评估
核心问题:
- 回答是否真正使用了给定上下文
- 回答有没有超出材料范围的扩写
- 面对资料缺失时,系统会不会老实承认不知道
- 当多个证据冲突时,系统有没有把冲突说出来
这一层最值得警惕的不是明显错误,而是“半对”。
完全错误比较好发现,半对最危险,因为它最像可用系统。
先做小而硬的评估集
别一开始就追大规模自动评估。第一套评估集可以很小,但要足够尖。
我更建议手工整理一批问题,覆盖至少这几类:
- 文档里明确有标准答案的问题
- 文档里有相关信息,但需要跨段整合的问题
- 文档里根本没有答案、系统应该拒答的问题
- 很容易被相似术语误导的问题
- 多个文档可能给出不同说法的问题
只要这批问题足够有代表性,哪怕只有几十条,也比“随便问几个感觉一下”强得多。
什么时候才值得上更复杂的优化
Hybrid Search、Rerank、Query Rewrite、Metadata Filtering、Multi-turn Memory,这些都不是没用。问题在于,很多人加它们的时机太早了。
如果基础链路还没看清,复杂特性通常只会带来两件事:
- 让系统看起来更高级
- 让问题更难定位
下面是我对几个常见优化点的判断。
Hybrid Search
当你的语义检索容易漏掉精确术语、版本号、错误码、配置项名称时,关键词检索通常值得加。
它尤其适合这些场景:
- API 文档
- 运维手册
- 带大量专有名词的内部知识库
如果你的知识源里这种精确锚点很多,纯向量检索往往不够稳。
Rerank
当你能召回“大致相关”的候选,但前几名排序总不够准时,rerank 才有明确价值。
它擅长解决的是“粗召回够了,但精排不行”。
如果你的问题是根本没召回来,那 rerank 帮不上忙。别拿它修补索引和切块阶段的问题。
Query Rewrite
当用户问题很口语化、上下文省略很多,或者强依赖前文指代时,query rewrite 有帮助。
但它本身也是一次变换,一次变换就多一层偏差。第一版没必要默认打开,尤其是在你还没搞清原始 query 为什么失效的时候。
Metadata Filtering
当知识库来源很多、文档类型差异大、时间敏感性强时,元数据过滤通常很值。
它不是锦上添花,而是降低噪音的重要手段。特别是在内部知识库里,新旧版本、团队边界、产品线边界经常比语义本身更能决定答案对不对。
一条更现实的学习和构建路线
如果现在让我重新规划 RAG 的学习顺序,我会更克制一点。
第一阶段:只补和系统直接相关的基础
重点不是推公式,而是搞清楚几个问题:
- embedding 到底在帮助什么
- 语义相似和任务相关不是一回事
- chunking 为什么经常决定上限
- top-k、上下文窗口和噪音之间是什么关系
会这些以后,你看很多 RAG 方案就不会再只看“组件名单”了。
第二阶段:做一个最小闭环
目标不是追求效果极致,而是让链路完整、日志可看、问题可定位。
这时最重要的产物不是 demo 页面,而是一套你能调试的流程。
第三阶段:建立评估和失败样本
这一步很枯燥,但基本不能跳。
因为真正让系统变好的,不是你又加了一个新模块,而是你终于知道它一直在哪些问题上失败。
第四阶段:只选一两个优化点做对照实验
别同时改 embedding、chunking、rerank 和 prompt。那样最后你只能得到一句没什么价值的话:
“感觉好像比以前强了。”
工程上最有用的结论通常更朴素:
- 这个文档类型更适合按小节切
- 这个知识库需要关键词召回补精确术语
- top-k 从 5 提到 12 没有明显收益,噪音反而更高
- 某个 reranker 确实改善了排序,但只对一类问题明显有效
这种结论不花哨,但它们能累积成真正稳定的系统判断。
最后
我现在越来越不觉得 RAG 是一个“接上去就会更聪明”的功能。
它更像一套约束系统:你试图让模型在有限、外部、可追溯的证据上工作,而不是在自己脑子里自由发挥。这个目标本身没有问题,但它要求你接受一个事实:
真正决定结果的,通常不是最显眼的那一层。
不是首页 demo,不是模型名字,也不是框架 logo。
而是那些看起来不那么性感的细节:文档是否干净,chunk 边界是否合理,召回是不是拿到了关键证据,上下文有没有被噪音污染,系统有没有一套能让你承认自己其实还没做对的评估方法。
如果这些东西不扎实,RAG 很容易变成一个昂贵的错觉制造器。
反过来,如果这些基础问题被认真对待,那么哪怕第一版系统很朴素,它也会是一个真的能继续长出来的系统。
我现在对 RAG 的预期就这么简单:
先别急着把它做复杂。
先把它做诚实。