Published on

RAG 不只是接个向量库:从最小系统到工程判断

Authors
  • Name
    Twitter

RAG 不只是接个向量库

很多人第一次做 RAG,会把它想得过于乐观。

做法通常也差不多:找一批文档,切块,做 embedding,塞进向量库,用户提问时检索几个 chunk,把结果连同问题一起丢给模型。跑通以后,页面上真的能吐出答案,于是大家很容易得出一个结论: “RAG 已经做完了,后面无非是调调参数。”

这通常就是第一层错觉。

RAG 真正难的地方,从来不是“把检索接到模型前面”。难点在于另一件更不体面的事:你要让整条链路在脏数据、模糊问题、不稳定语义边界和有限上下文里,仍然尽可能稳定地工作。

如果这件事没有做好,系统表面上看起来会很聪明,实际上只是把“像真的”伪装成“是真的”。

这也是我现在看 RAG 的基本立场:

  • 它不是一个模型技巧,而是一条系统链路
  • 它不是“有没有接检索”的问题,而是“每一层有没有失真”的问题
  • 它最怕的不是完全答不出来,而是答得像对,但依据是错的

所以这篇文章不打算做一份“RAG 全景科普”,也不想列一串框架名词。更现实一点,我想把它拆回工程问题:

  1. 一个最小可工作的 RAG,到底最少要包含什么
  2. 真正决定效果的地方在哪里
  3. 第一版系统通常会死在哪
  4. 没有评估时,为什么大多数“优化”其实都不可信
  5. 什么时候才值得上 Hybrid Search、Rerank、Query Rewrite 这些更复杂的东西

如果你已经写过一点 LLM 应用,但还没完整做过 RAG,希望这篇能帮你少踩一些很常见、但又很少有人认真拆开的坑。

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,似乎引用更稳了。

但如果没有统一问题集和前后对照,这些感觉没有太大价值。

工程不是靠“看起来更像了”推进的。

RAG 常见失败模式图,区分未召回、召回错误、上下文污染和生成漂移

怎么做一个最小但可验证的版本

我现在更认同一种比较克制的路线:先做一个土一点、但能解释问题的版本。

第一版完全可以只做这些:

  • 文档源先只支持 Markdown
  • 手动清洗掉明显无关内容
  • 按段落或小节做切块,再补少量 overlap
  • 用一个成熟 embedding 模型做索引
  • 检索先只做 top-k 语义召回
  • 把召回结果和来源一起传给模型
  • 明确要求模型“只基于给定材料回答,不足时直说”

这个版本不需要花哨,但必须回答三个问题:

  1. 系统有没有把真正相关的证据找回来
  2. 模型有没有依据这些证据回答
  3. 回答错时,我能不能定位错在检索还是生成

只要这三个问题还回答不清楚,就没必要急着往上叠复杂能力。

为了让最小版更容易调试,我建议一开始就保留这些日志:

  • 原始问题
  • 检索 query
  • 召回的 chunk 列表和分数
  • 每个 chunk 的来源文档
  • 最终送给模型的上下文
  • 最终回答

很多“神秘 bug”,你把这几项打印出来以后,就没那么神秘了。

没有评估,就别太认真谈优化

RAG 很容易给人一种危险的错觉:你问几个顺手的问题,都答对了,于是系统看起来已经“八九不离十”。

这对 demo 也许够,对系统完全不够。

我认为第一版能跑起来以后,下一步不是立刻上高级特性,而是先建评估。

至少要把评估拆成两层:

检索层评估

核心问题:

  • 应该命中的证据,有没有被召回
  • 命中的证据,排位是否足够靠前
  • 召回结果里噪音比例高不高

如果这一层都不稳,后面生成回答再漂亮,也只是建立在摇晃地基上。

回答层评估

核心问题:

  • 回答是否真正使用了给定上下文
  • 回答有没有超出材料范围的扩写
  • 面对资料缺失时,系统会不会老实承认不知道
  • 当多个证据冲突时,系统有没有把冲突说出来

这一层最值得警惕的不是明显错误,而是“半对”。

完全错误比较好发现,半对最危险,因为它最像可用系统。

先做小而硬的评估集

别一开始就追大规模自动评估。第一套评估集可以很小,但要足够尖。

我更建议手工整理一批问题,覆盖至少这几类:

  • 文档里明确有标准答案的问题
  • 文档里有相关信息,但需要跨段整合的问题
  • 文档里根本没有答案、系统应该拒答的问题
  • 很容易被相似术语误导的问题
  • 多个文档可能给出不同说法的问题

只要这批问题足够有代表性,哪怕只有几十条,也比“随便问几个感觉一下”强得多。

什么时候才值得上更复杂的优化

Hybrid Search、Rerank、Query Rewrite、Metadata Filtering、Multi-turn Memory,这些都不是没用。问题在于,很多人加它们的时机太早了。

如果基础链路还没看清,复杂特性通常只会带来两件事:

  • 让系统看起来更高级
  • 让问题更难定位

下面是我对几个常见优化点的判断。

当你的语义检索容易漏掉精确术语、版本号、错误码、配置项名称时,关键词检索通常值得加。

它尤其适合这些场景:

  • 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 的预期就这么简单:

先别急着把它做复杂。

先把它做诚实。