这篇是我学 RAG 时的白板笔记整理。RAG(Retrieval-Augmented Generation,检索增强生成)是现在几乎所有"让 AI 读自己资料"产品的底层套路,比如企业知识库问答、文档助手、客服机器人。

一句话总结它的本质:

在模型回答之前,先检索资料,再基于资料生成回答。

下面按"为什么需要它 → 它怎么工作"的顺序展开。

一、为什么需要 RAG

1. 直接把所有资料喂给模型,为什么不可行?

最朴素的想法是:把全部文档塞进 prompt,让模型自己看。这条路有三个问题:

  1. 上下文窗口有限。模型一次能读的 token 数是有上限的,几百页的文档根本塞不下。
  2. 成本。就算塞得下,每次提问都把全部资料发一遍,token 费用会爆炸。
  3. 响应速度。输入越长,模型处理越慢,用户体验直线下降。

2. 模型本身也有短板

就算不考虑塞资料的问题,纯靠模型自己回答也有硬伤:

  1. 知识过时。模型的知识停留在训练截止日期,问它昨天发生的事它不知道。
  2. 私有数据。公司内部文档、个人笔记,模型训练时根本没见过。
  3. 幻觉。不知道的事情它可能一本正经地编。

RAG 的思路就是绕开这些限制:资料放在模型外面,每次只把和问题相关的一小段喂进去。

二、核心流程

整个 RAG 分两个阶段:数据准备(离线,只做一次)和用户提问(在线,每次提问都执行)。

flowchart LR subgraph prep["① 数据准备(离线,只做一次)"] direction LR A["📄 原始文档"] --> B["切分 Chunking
按章节 / 段落 / 字数……"] B --> C["🧩 Chunks
每一块语义相对完整"] C --> D["Embedding
(向量化)"] D --> E[("向量数据库")] end
flowchart LR subgraph query["② 用户提问(在线,每次提问都执行)"] direction LR Q["❓ 用户问题"] --> QE["问题也做
Embedding"] QE --> S["相似度检索
找距离最近的 top-k 块"] V[("向量数据库")] --> S S --> P["相关资料 + 问题
拼成 Prompt"] P --> L["🤖 LLM 生成回答"] end

第一步:数据准备

切分(Chunking)

文档不能整篇存,要先切成一块块的 chunk。切分的依据可以是章节、段落、字数……没有标准答案,但有一条核心原则:

每一块语义相对完整。

切太碎,一句话被腰斩,检索回来读不懂;切太大,一块里混进好几个主题,检索就不精准。常见做法是几百字一块,相邻块之间留一点重叠(overlap),避免关键信息正好卡在边界上。

向量化(Embedding)

切好的 chunk 没法直接比较"语义像不像",所以要用 embedding 模型把每块文字变成一个向量(一串数字)。它的神奇之处在于:

意思相近的文字,向量在空间里的距离会更近。

用一个二维的例子直观感受一下(白板上画的就是这张图):

xy121−1「我是男的」(1, 1)「我是一个男生」(2, 1)距离近 = 意思相近「今天天气不错」(1, −1)语义无关,离得远

「我是男的」和「我是一个男生」用词不同,但意思几乎一样,所以它们的向量靠在一起;「今天天气不错」和它们没什么关系,就落在远处。

图里为了好画只用了二维,实际的 embedding 是几百到几千维(比如常见的 768 维、1536 维),原理完全一样——只是空间更高维,“距离近 = 语义近"这条规律不变。

向量算好之后,连同原文一起存进向量数据库(Chroma、FAISS、Pinecone 等),数据准备阶段就结束了。

第二步:用户提问

在线阶段每次提问都走一遍这四步:

  1. 问题也做 embedding。用和数据准备同一个 embedding 模型,把用户的问题变成向量。

  2. 相似度检索。在向量数据库里找和问题向量距离最近的 top-k 个 chunk(比如取最相近的 3~5 块)。

  3. 拼 prompt。把检索到的资料和用户问题拼在一起,例如:

    请根据以下资料回答问题。如果资料里没有答案,请直接说不知道。
    
    资料:
    {检索到的 top-k chunks}
    
    问题:{用户的问题}
    
  4. 生成回答。LLM 基于资料作答,而不是凭"记忆"瞎猜。

衡量"距离近不近"最常用的是余弦相似度——看两个向量的夹角:

$$\text{cosine_sim}(\vec{a}, \vec{b}) = \frac{\vec{a} \cdot \vec{b}}{|\vec{a}| , |\vec{b}|}$$

夹角越小、值越接近 1,语义越相近。

三、小结

把整条链路压缩成一句话:

离线:文档 → 切分 → 向量化 → 入库; 在线:问题 → 向量化 → 找最近的块 → 带着资料让模型回答。

这样一来,开头的几个问题全部有了着落:

问题RAG 怎么解决
上下文窗口塞不下每次只取 top-k 个相关块
成本高、响应慢输入短了,又快又便宜
知识过时资料库随时更新,不用重训模型
私有数据自己的文档自己入库
幻觉要求模型"只根据资料回答”

后面打算自己手搓一个最小 RAG(不用 LangChain,直接写 chunking + embedding + 检索),到时候再写一篇实战记录。