🔥 RAG 到底是怎么工作的?掰开揉碎了给你讲明白!
摘要:面试被问 RAG 支支吾吾?看完这篇,你能把 RAG 的每一步流程画出来讲给面试官听。本文用一个完整 Demo,从零带你走通 RAG 全流程。
📌 前言
最近 AI 领域有个词出现频率极高------RAG(Retrieval-Augmented Generation,检索增强生成)。
不管是面试还是实际项目,RAG 已经成了大模型应用的标配方案。但很多同学对 RAG 的理解还停留在"好像跟向量数据库有关"这个层面。
我自己在项目中用 LangChain + RAG 搭建过知识问答系统,过程中踩了不少坑------chunk size 选错导致检索质量断崖下降、Embedding 模型换了一个效果完全不同、Top-K 设太大反而干扰了 LLM 判断......这些实战经验让我对 RAG 每一步的"为什么"有了更深的理解。
今天这篇文章,我会用一个完整可运行的 Demo,把 RAG 的每一步掰开揉碎了讲清楚。看完之后,你不仅能回答"RAG 是什么",还能回答"RAG 内部到底经历了哪些步骤"。
RAG picture

🎯 本文适合谁
- 想理解 RAG 底层流程的开发者
- 面试前需要梳理 RAG 知识点的同学
- 正在做或准备做大模型应用的工程师
- 对 LangChain 感兴趣想快速上手的同学
📚 先搞懂:RAG 到底是什么?
一句话解释
RAG = 先从知识库中检索相关内容,再把检索结果喂给大模型生成回答。
为什么要用 RAG?
大模型有两个致命短板:
| 问题 | 说明 | RAG 怎么解决 |
|---|---|---|
| 🧠 知识有截止日期 | GPT 的训练数据有时间边界 | 实时检索最新知识库 |
| 🤥 会一本正经胡说八道 | "幻觉"问题 | 给它真实文档作为依据 |
| 📄 无法访问私有数据 | 不知道你公司的内部文档 | 把私有数据建成知识库 |
一句话:RAG 让大模型"开卷考试",而不是"闭卷瞎编"。
🔍 RAG 全流程拆解(重点!)
一张图理解 RAG 的核心流程:
scss
┌─────────────────────────────────────────────────────────┐
│ RAG 完整流程 │
│ │
│ 📄 知识库文档 │
│ │ │
│ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ Document │───▶│Embedding │───▶│ 向量数据库 │ │
│ │ 构建 │ │ 向量化 │ │ (VectorStore) │ │
│ └──────────┘ └──────────┘ └──────┬───────┘ │
│ │ │
│ │ ② 检索 │
│ 用户提问 ──── Embedding ───────────────▶│ │
│ "东东和光光 │ │
│ 怎么成为朋友的?" ▼ │
│ ┌──────────┐ │
│ │ Top-K │ │
│ │ 相关文档 │ │
│ └────┬─────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ 组装 Prompt │ │
│ │ = 上下文 + 问题 │ │
│ └────────┬─────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ LLM 生成回答 │ │
│ └──────────────────┘ │
└─────────────────────────────────────────────────────────┘
整个流程可以拆成 3 大阶段、6 个步骤:
| 阶段 | 步骤 | 说明 |
|---|---|---|
| 📥 索引阶段 | ① 构建 Document | 把原始数据切成文档片段 |
| ② Embedding 向量化 | 用模型把文本转成向量 | |
| ③ 存入向量数据库 | 把向量存起来供后续检索 | |
| 🔍 检索阶段 | ④ 用户提问向量化 | 把问题也转成向量 |
| ⑤ 相似度检索 | 在向量库中找最相关的 Top-K 文档 | |
| ✍️ 生成阶段 | ⑥ Augmented Prompt + LLM 生成 | 把检索结果塞进 Prompt,让 LLM 回答 |
接下来我们每一步都用代码讲清楚。
🛠️ 技术栈
| 技术 | 用途 |
|---|---|
| LangChain | LLM 开发框架,提供 RAG 全套组件 |
| @langchain/openai | ChatOpenAI + OpenAIEmbeddings |
| @langchain/core | Document 等核心抽象 |
| @langchain/classic | MemoryVectorStore 内存向量存储 |
安装命令:
bash
pnpm add @langchain/openai @langchain/core @langchain/classic dotenv
📝 代码实现:一步步走通 RAG
第一步:构建 Document(知识库的最小单元)
RAG 的起点是知识库 。知识库由一个个 Document 组成,每个 Document 包含两部分:
pageContent:要被向量化的文本内容(参与语义搜索)metadata:元数据,不参与向量化,但可以用于过滤和溯源
javascript
import { Document } from '@langchain/core/documents';
const documents = [
new Document({
pageContent: `光光是一个活泼开朗的小男孩,他有一双明亮的大眼睛,
总是带着灿烂的笑容。光光最喜欢的事情就是和朋友们一起玩耍,
他特别擅长踢足球,每次在球场上奔跑时,就像一道阳光一样充满活力。`,
metadata: {
chapter: 1,
character: "光光",
type: "角色介绍",
mood: "活泼"
},
}),
new Document({
pageContent: `东东是光光最好的朋友,他是一个安静而聪明的男孩。
东东喜欢读书和画画,他的画总是充满了想象力。虽然性格不同,
但东东和光光从幼儿园就认识了,他们一起度过了无数个快乐的时光。`,
metadata: {
chapter: 2,
character: "东东",
type: "角色介绍",
mood: "温馨"
},
}),
// ... 更多 Document
];
💡 关键理解 :
pageContent是会被 Embedding 模型转成向量的,而metadata不会。metadata 的作用是在检索之后做过滤、排序、溯源。
在真实项目中,知识库可能来自:
- PDF 文件 → 按段落切分
- 网页内容 → 按章节切分
- 数据库记录 → 按条目构建
- 音视频 → 先转文字再切分
第二步:Embedding 向量化
Embedding 是 RAG 的核心中的核心。
它的作用是把人类的自然语言文本,转换成一组高维浮点数向量。语义相近的文本,向量在空间中的距离就近;语义无关的文本,距离就远。
javascript
import { OpenAIEmbeddings } from '@langchain/openai';
const embedding = new OpenAIEmbeddings({
model: process.env.EMBEDDINGS_MODEL_NAME, // 如 text-embedding-3-small
apiKey: process.env.OPENAI_API_KEY,
});
举个直观的例子:
scss
"光光喜欢踢足球" → [0.12, -0.34, 0.56, ..., 0.78] (1536维)
"光光擅长运动" → [0.11, -0.32, 0.55, ..., 0.80] (距离很近 ✅)
"今天天气不错" → [0.89, 0.12, -0.67, ..., 0.23] (距离很远 ❌)
💡 为什么要用向量? 因为传统的关键词搜索搜不到语义。你搜"光光擅长什么运动",关键词匹配可能找不到结果,但向量搜索能理解"踢足球"就是一种"运动"。
第三步:存入向量数据库(VectorStore)
有了 Embedding 模型,我们把所有 Document 的 pageContent 向量化后存入向量数据库。
这个 Demo 用的是 MemoryVectorStore------最轻量的内存向量存储,适合学习和原型验证。生产环境一般用 Pinecone、Milvus、Chroma 等。
javascript
import { MemoryVectorStore } from '@langchain/classic/vectorstores/memory';
// 一行代码完成:向量化所有 documents → 存入内存向量库
const vectorStore = await MemoryVectorStore.fromDocuments(documents, embedding);
这一行代码内部做了什么?
css
documents[0].pageContent ──Embedding──▶ 向量[0] ──存入──▶ 向量数据库
documents[1].pageContent ──Embedding──▶ 向量[1] ──存入──▶ 向量数据库
documents[2].pageContent ──Embedding──▶ 向量[2] ──存入──▶ 向量数据库
... ... ... ...
💡 索引阶段到此结束。 此时知识库已经建好,向量数据库里存满了文档的向量表示,随时可以被检索。
第四步 & 第五步:检索(Retriever)
LangChain 提供了 Retriever(检索器),它是向量检索的标准入口。
javascript
// 创建检索器,k=3 表示返回最相似的 3 条文档
const retriever = vectorStore.asRetriever({ k: 3 });
const question = "东东和光光是怎么成为朋友的?";
// 检索!
const docs = await retriever.invoke(question);
retriever.invoke(question) 内部做了什么?
ini
"东东和光光是怎么成为朋友的?"
│
▼
Embedding 向量化
│
▼
在向量数据库中计算余弦相似度
│
▼
按相似度排序,取 Top-K(k=3)
│
▼
返回 3 个最相关的 Document 对象
进阶:查看相似度分数
如果你不仅想看结果,还想看每条结果有多相关 ,可以用 similaritySearchWithScore:
javascript
const scoredResults = await vectorStore.similaritySearchWithScore(question, 3);
scoredResults.forEach(([doc, score], i) => {
// score 是距离,越小越相似
// 转换为相似度:similarity = 1 - score
const similarity = (1 - score).toFixed(4);
console.log(`[文档 ${i + 1}] 相似度: ${similarity}`);
console.log(`内容: ${doc.pageContent.substring(0, 50)}...`);
console.log(`元数据: 章节=${doc.metadata.chapter}, 角色=${doc.metadata.character}`);
});
💡
retriever.invokevssimilaritySearchWithScore:
方法 说明 retriever.invoke()标准检索入口,内部会做去重、过滤、rerank 等处理 similaritySearchWithScore()纯向量查询,返回结果 + 距离分数,不做额外处理 一般用
retriever.invoke()就够了,需要调优时再用similaritySearchWithScore()看分数。
第六步:Augmented Prompt + LLM 生成
这是 RAG 的最后一步------把检索到的文档塞进 Prompt,让 LLM 基于这些真实内容生成回答。
javascript
// 1. 把检索到的文档拼成上下文字符串
const context = docs
.map((doc, i) => `[片段${i}]\n${doc.pageContent}`)
.join("\n\n-----\n\n");
// 2. 组装 Augmented Prompt
const prompt = `
你是一个讲友情故事的老师。基于以下故事片段回答问题,
用温暖生动的语言。如果故事中没有提到,
就说"这个故事里还没有提到这个细节"。
故事片段:
${context}
问题:${question}
老师的回答:
`;
// 3. 调用 LLM 生成回答
const response = await model.invoke(prompt);
console.log(response.content);
💡 Augmented 的含义就在这里 ------Prompt 不是空洞地问 LLM,而是用检索到的真实文档"增强"了 Prompt。LLM 有了上下文依据,就不会胡编乱造了。
🧠 核心概念速查表
| 概念 | 一句话解释 | 类比 |
|---|---|---|
| Document | 知识库的最小单元,包含文本和元数据 | 一本书中的一段话 + 批注 |
| Embedding | 把文本转成高维向量 | 给每段话生成一个"语义指纹" |
| VectorStore | 存储和检索向量的数据库 | 按"语义指纹"查找的图书馆 |
| Retriever | 向量检索的标准接口 | 图书馆的"智能搜索栏" |
| Top-K | 返回最相似的 K 条结果 | 搜索结果只看前 K 条 |
| Augmented Prompt | 把检索结果塞进 Prompt | 开卷考试的"参考资料" |
| 相似度分数 | 向量之间的距离,越小越相似 | 两段话"有多像"的量化指标 |
⚠️ 我踩过的 3 个坑(实战经验)
坑 1:切分粒度选错了,检索质量断崖下降
一开始我按整段切分,结果一个 chunk 里藏了三个不相关的知识点,检索出来的文档相关性很低。后来改成按 200-300 token 切分,检索准确率明显提升。
建议:chunk size 一般在 200-500 token 之间效果最好,太小丢上下文,太大会引入噪声。
坑 2:Embedding 模型选型差异巨大
用 OpenAI 的 text-embedding-3-small 做英文效果很好,但换到中文场景,BGE-M3 的召回率明显更高。模型选型要根据你的实际语言场景来,别盲目跟风。
建议:中文场景优先考虑 BGE、Jina 等对中文优化过的模型,效果可能比 OpenAI 的好一个量级。
坑 3:Top-K 不是越大越好
一开始我设 k=10,结果后面的文档跟问题关系不大,反而干扰了 LLM 的判断,生成的回答里掺了很多无关信息。后来改回 k=3,效果反而更好。
建议:Top-K 设 3-5 即可,配合 Rerank 模型做二次筛选效果更佳。宁可精准,不要贪多。
🏗️ 从 Demo 到生产:你需要知道的
Demo 用的是 MemoryVectorStore(内存存储),生产环境需要考虑更多:
| 维度 | Demo | 生产 |
|---|---|---|
| 向量存储 | MemoryVectorStore(内存) | Pinecone / Milvus / Chroma / Weaviate |
| 文档切分 | 手动写 Document | RecursiveCharacterTextSplitter 自动切分 |
| Embedding 模型 | OpenAI text-embedding-3-small | 可选 BGE、Jina 等开源模型 |
| 检索策略 | 纯向量相似度 | 混合检索(向量 + 关键词)、Rerank |
| 对话记忆 | 无 | 加入 ConversationBufferMemory |
生产级 RAG 架构
css
原始文档 → 文档加载 → 文本切分 → Embedding → 向量数据库
│
用户提问 → Embedding → 混合检索 → Rerank → Top-K ─────┘
│
▼
Augmented Prompt → LLM → 回答
💡 重点总结
- RAG 的本质:让大模型"开卷考试",先检索再生成,减少幻觉
- 三大阶段:索引(建库)→ 检索(找相关文档)→ 生成(增强 Prompt)
- Embedding 是核心:把文本变成向量,实现语义级别的搜索
- Document 的 pageContent 参与向量化,metadata 不参与
- retriever.invoke() 是标准检索入口,内部做了去重、过滤等处理
- Augmented Prompt = 检索到的上下文 + 用户问题,这是 RAG 生成质量的关键
🔗 参考资料
💬 交流讨论
你在实际项目中用 RAG 时,遇到最头疼的问题是什么?
是切分粒度不好把握?检索召回率不够?还是 LLM 生成的答案偏了题?
在评论区聊聊,说说你的经历,大家一起交流 👇
觉得这篇文章帮到了你?
- 👍 点赞让更多人看到
- ⭐ 收藏方便下次复习
- 👆 关注我,后续会更新 RAG 生产级实战、Rerank 优化、混合检索等进阶内容!