雨滴.
雨滴.
技术分享

RAG:不是给模型"喂资料"这么简单

刚开始接触 RAG 的时候,我对它的理解其实很简单:模型不知道的东西,就去知识库里查一下,然后把查到的内容塞给模型。

现在回头看,这个理解不能说错,但确实有点太浅了。

大模型有几个绕不开的问题:训练数据有截止时间,不知道你的私有数据,遇到不确定的问题还可能一本正经地给你编一个答案。RAG(Retrieval-Augmented Generation,检索增强生成)解决这些问题的思路并不复杂——回答之前,先去外部知识库找相关资料,再把这些资料交给大模型,让模型基于检索结果生成答案。

说白了,就是把原本只有“模型参数里的知识”,变成:

LLM 自身知识 + 外部知识库 → RAG → 最终回答

但真正开始自己做 RAG 之后,我才发现,难点根本不在“把资料喂给模型”这一步。

你喂什么、怎么切、怎么搜、搜回来以后怎么排、最后给模型多少内容,这些事情任何一个环节出了问题,最后的回答都有可能跟着出问题。


一、先把完整链路搞清楚

RAG 可以简单拆成两条链路:索引链路和查询链路。

索引阶段一般提前完成:

文档加载 → 内容清洗 → 切块 Chunk → Embedding → 写入向量库

查询阶段则是用户每问一次,就重新走一遍:

用户 Query → 检索 → 过滤 / 重排序 → 构造上下文 → LLM → 最终回答

刚看到这条链路的时候,很容易觉得每一步都挺简单。

但真正做起来就会发现,RAG 并没有什么“把 Embedding 模型换好一点,效果就起飞”的万能按钮。它更像一条流水线,前面任何一步留下的问题,都可能在最后的答案里暴露出来。


二、为什么不能直接拿 MySQL 查?

做 Java 项目的时候,我一开始很容易想到一个问题:

既然我已经有 MySQL 了,为什么还要专门搞一个向量数据库?

原因其实就在“查什么”上。

MySQL 最擅长的是结构化数据和精确查询,比如:

SELECT * FROM article WHERE category = 'Java';

但 RAG 经常面对的是另一种问题:

“有没有介绍 Java 虚拟线程的内容?”

用户并不会告诉你数据库里具体存了哪个关键词,它真正想表达的是一个语义。

于是我们可以把文本转换成向量:

“Java 虚拟线程有什么用?” → Embedding → [0.12, -0.37, 0.82, ...]

知识库里的其他文本也转换成向量,然后根据向量之间的距离,找出语义上比较接近的内容。

这就是向量检索。

在数据量比较大的情况下,还会涉及 ANN(Approximate Nearest Neighbor,近似最近邻)以及 HNSW、PQ 等索引和压缩技术。

常见的向量数据库有 Milvus、Qdrant、Weaviate 等。如果项目本身已经使用 PostgreSQL,也可以直接考虑 pgvector。至于到底选哪个,我觉得没必要为了“技术栈看起来高级”强行上一个复杂的向量数据库,数据规模、过滤需求、部署成本这些东西反而更应该先考虑。


三、Chunk 不是随便切几段就完事了

如果让我选一个 RAG 最容易被低估的地方,我可能会选 Chunk。

因为 Embedding 再好,检索再高级,如果一开始切出来的内容就不完整,后面基本都是在补锅。

切得太小:

原文:
Java 21 引入虚拟线程,适合大量 I/O 密集型任务……

切块:
“Java 21 引入虚拟线程”

语义信息可能还在,但上下文已经没了。

切得太大也不行。

一个 Chunk 塞进去几千字,里面真正和 Query 有关的可能只有两句话,Embedding 表达的语义就容易变得不够集中,同时最后还会浪费大量上下文 Token。

所以 Chunk 大小、重叠区域、切分方式,本身就是需要根据数据特点不断调整的。

而这里还有一个更隐蔽的问题:

有些 Chunk 单独拿出来看,其实根本不知道自己在讲什么。

例如:

“该公司二季度营收增长了 3%。”

如果脱离原文,我们甚至不知道“该公司”是谁。

Anthropic 后来提出的 Contextual Retrieval,就是针对这种问题做的。简单来说,在 Chunk 建立 Embedding 或 BM25 索引之前,先给它补一段和原文相关的上下文,让这个 Chunk 即使脱离原文,也能够表达得更加完整。

Anthropic 公布的实验中,Contextual Embeddings + Contextual BM25 能够明显降低检索失败率,再结合 Rerank 后效果进一步提升。

这个事情给我的一个感受是:

RAG 的优化并不只是换模型。

很多时候,先看看自己切出来的 Chunk 到底长什么样,可能比盯着 Embedding 模型排行榜更有意义。


四、Embedding Model 也不是排行榜第一就一定适合

Embedding 模型负责把文本变成向量,所以它当然很重要。

但真正选模型的时候,我觉得不能只看 MTEB 排名。

如果你的知识库主要是中文内容,却拿一个主要针对英文场景优化的模型直接上生产,排行榜再漂亮也不一定适合你的业务。

实际需要关注的东西还有:

  • 中文或者其他目标语言的表现

  • 是否适合自己的业务领域

  • 向量维度

  • 推理速度

  • 部署方式

  • 模型成本

最靠谱的方式还是自己准备一批 Query-Document 数据,然后跑实际评测。

比如:

Query → Embedding → Top-K 检索 → Recall@K

如果你发现自己的 Query 在 Top-5 里经常找不到真正相关的文档,那么继续换 Rerank 模型可能意义也不大。

先确定问题到底出在召回,还是出在排序。


五、只做向量检索,很多时候还不够

向量检索很擅长理解“意思差不多”的内容,但它也有自己的盲区。

比如:

Redis 6379
Spring Boot 3.4.7
Exception 500
SKU-2026-001

这些东西有时候反而更适合关键词精确匹配。

你问:

“Redis 6379 端口连接失败怎么办?”

这时候“6379”这个数字本身就非常重要,纯语义检索未必能够很好地利用它。

所以实际项目里经常会把两种检索放在一起:

                Query
                  ↓
          ┌───────┴───────┐
          ↓               ↓
      Vector Search     BM25
          ↓               ↓
          └───────┬───────┘
                  ↓
             结果融合
                  ↓
               Rerank

这就是 Hybrid Search。

两路结果可以通过加权或者 RRF(Reciprocal Rank Fusion,倒数排序融合)进行合并。

这样做的思路其实很朴素:

既让模型理解“意思”,又别把关键词本身丢了。


六、用户的问题,有时候也需要“加工”一下

还有一个经常被忽略的问题:

用户说出来的话,不一定是一个适合直接检索的 Query。

例如连续对话:

“Redis 怎么解决缓存击穿?”

过了一会儿又问:

“那它和雪崩有什么区别?”

如果直接拿第二句话去检索,效果很可能不太好——脱离上下文以后,连“它”指什么都不知道。

所以 RAG 里面经常会出现 Query Rewriting:

历史对话 + 当前问题 → Query Rewriting → “Redis 缓存击穿与缓存雪崩有什么区别?” → 检索

除此之外,还有 Query Expansion,可以给一个问题补充相关词汇;也可以让模型根据 Query 生成 Metadata Filter,比如:

用户:
“2025 年以后发布的 Java 教程”

        ↓

语义 Query:
Java 教程

Metadata Filter:
publish_time > 2025

这些东西看起来是在“折腾 Query”,实际上目的只有一个:

别让检索系统拿着一句人话,稀里糊涂地去知识库里找东西。


七、Rerank:第一次召回只是“海选”

向量检索或者 BM25 找出来的 Top-K,并不意味着前几个一定就是最相关的。

所以很多 RAG 会再增加一层 Rerank。

可以把它理解成:

Query → 第一次召回 → Top 20 → Rerank → Top 5 → LLM

第一次召回更像是“海选”,尽可能把相关内容找回来。

Rerank 则负责在这些候选里面重新判断:

到底谁和当前问题最相关?

常见做法是使用 Cross-Encoder,对 Query 和 Document 进行联合打分。

当然,Rerank 不是免费的。多一次模型推理,就意味着多一点延迟和计算成本。如果你的场景对准确率要求比较高,而且初次召回范围比较大,这一层往往很有价值。


八、检索出来的东西,也不是越多越好

这是我觉得做 RAG 时特别容易犯的一个错误:

“既然都检索出来了,那就全部给模型呗。”

其实不是。

假设一次检索出了 20 个 Chunk,全部塞进去,模型当然能看到,但问题也来了:

  • Prompt 变长

  • Token 成本增加

  • 推理延迟增加

  • 无关信息变多

  • 重要信息可能被大量内容淹没

也就是经常被提到的 Lost in the Middle 问题。

所以实际做 RAG 时,更合理的思路通常是:

召回 20 个 → 过滤 / Rerank → 留下 5 个 → 必要时进一步压缩 → 交给 LLM

Prompt Compression 也是因此出现的。核心不是“我检索到了多少”,而是“这些内容里,有多少是真正回答这个问题需要的”。


九、怎么判断自己的 RAG 到底好不好?

RAG 最麻烦的地方之一,就是你不能只看最后一句回答。因为一个回答错了,到底是谁的问题?可能是 Chunk 切错了、Embedding 没召回、BM25 也没召回、Rerank 排错、Context 构造有问题,也可能是 LLM 最后就是答错了。

所以评估的时候,最好把检索和生成拆开看。

检索侧可以关注:Recall、Precision、MRR、NDCG。简单理解就是:该找到的有没有找到?找到的东西是不是相关?相关内容到底排在第几?

生成侧则可以看:Faithfulness(回答是否忠实于检索到的内容)、Relevance(是否真正回答了问题)、Completeness(回答是否完整)、Harmfulness(是否产生有害内容)。

RAGAS 这类框架可以帮助做自动化评估,但我个人更倾向于把它当成“筛选工具”,而不是最终答案。因为模型评模型,本身也可能判断错。比较现实的方式还是:自动评估先筛掉明显有问题的样本,再人工抽查关键样本,最后回到线上真实数据继续观察。

最终到底好不好,还是得回到真实用户的问题上。


十、RAG 能不能解决幻觉?

能降低,但别把 RAG 理解成“装上就不会幻觉”。

RAG 提供的是一个额外的知识来源。如果检索召回了一堆错误内容,模型依然可能一本正经地回答错误信息。所以真正有效的方式,往往是多层一起做:更好的知识、更好的召回、更好的排序、更干净的 Context、明确的 Prompt 约束,再加上输出校验和引用。

也就是说:RAG 不是把幻觉“关掉”,而是在尽可能减少模型胡编乱造的空间。


十一、现在再回头看 Agentic RAG、GraphRAG

如果做到这里,传统 RAG 的思路基本就清楚了。但继续往下做复杂一点的项目,就会发现“检索一次、生成一次”并不是所有问题都适合。

比如:“帮我分析这家公司近三年的财报变化,并比较它和竞争对手的业务情况。”这种问题可能需要查多个文档、多个时间段,甚至需要根据第一次检索到的结果决定下一步应该查什么。

这时候就出现了 Agentic RAG。它不再规定固定的“Query → Search → Answer”流程,而是允许模型自己判断:

用户问题
   ↓
模型判断
   ↓
需要什么资料?
   ↓
检索
   ↓
结果够不够?
   ↓
不够 → 再检索
   ↓
够了
   ↓
生成答案

GraphRAG 则是另一种思路。向量检索更擅长找“相似内容”,但对于“A 和 B 是什么关系”“这几家公司之间有什么关联”这种需要跨文档理解实体关系的问题,单纯依靠向量检索可能并不理想。于是可以引入知识图谱,把公司之间的投资、收购这类关系显式表达出来,再结合检索完成分析。

至于 Adaptive RAG,则更像是在做“分流”:简单问题走普通 RAG,复杂问题才升级到 Agentic RAG / GraphRAG,避免所有问题都走最重的一套流程。

很多项目的问题,其实还没有严重到需要 Agentic RAG。

如果连 Chunk、Embedding、Hybrid Search、Rerank、Context 这一套最基础的链路都没有调明白,上来就 GraphRAG、Agentic RAG,最后很容易变成问题没解决、系统反而更复杂、Debug 也更困难。

所以如果让我现在重新做一个 RAG,我可能不会先去追那些看起来很新的方案。

我会先把自己的数据拿出来,看看 Chunk 到底切得怎么样,Query 能不能召回正确内容,Rerank 有没有把真正相关的东西排上来,最后给模型的 Context 是不是干净。

这些最基础的东西跑顺了,再去看业务到底有没有必要引入更复杂的检索方式。

毕竟 RAG 最终还是为了回答问题。

技术方案可以越来越复杂,但最好别让问题本身也跟着变复杂。

豫ICP备2026047996号 公安备案 豫公网安备41072702000373号