理解 RAG:从概念到实践
本文用 WWH 原则(What / Why / How)系统讲解 RAG。读完你将理解 RAG 的本质、为什么现代 AI 产品离不开它、以及如何自己实现一个。
一、What — RAG 是什么?
1.1 一句话定义
RAG = Retrieval-Augmented Generation(检索增强生成)。
它是一种让 AI 先"查资料"再"回答问题"的技术架构——在 LLM 生成回答之前,先从外部知识库中检索相关信息作为参考。
1.2 类比理解
把 LLM 想象成一个参加开卷考试的学生:
| 场景 | 对应 RAG 概念 |
|---|---|
| 学生的大脑和语言能力 | LLM(语言大模型) |
| 考场提供的参考书 | 你的文档(知识库) |
| 遇到题目先翻书找相关章节 | 检索(Retrieval) |
| 翻到的章节 | 检索到的 Chunk(文档片段) |
| 基于翻到的内容写答案 | 生成(Generation) |
| 答不出来就老实说不知道 | System Prompt 约束 |
如果这个学生不能翻书(纯 LLM)——那他只能凭记忆答题,记忆截止在训练日期,不知道的内容就瞎编。
1.3 数据流
┌──────────── 离线准备(索引)────────────┐
│ │
│ 文档 → 解析 → 切分 → 向量化 → 向量库 │
│ │
└────────────────────────────────────────┘
↓
┌──────────── 在线查询(回答)────────────┐
│ │
│ 问题 → 向量化 → 向量库搜索相似段落 │
│ → 拼接 Prompt → LLM 生成 → 回答 │
│ │
└────────────────────────────────────────┘
二、Why — 为什么要用 RAG?
2.1 LLM 的三大先天缺陷
| 缺陷 | 具体表现 | RAG 如何解决 |
|---|---|---|
| 知识截止 | LLM 训练数据有截止日期,不知道训练后发生的事情 | 文档实时上传,知识随时更新 |
| 幻觉 | 没有信息来源时,LLM 倾向于编造看似合理但错误的内容 | 给 LLM 提供参考资料,约束它"只能基于资料回答" |
| 私有知识盲区 | LLM 从未见过你公司的内部文档、代码库、合同 | 把私有文档向量化后存入本地知识库 |
2.2 为什么不能直接把文档全塞给 LLM?
三个原因:
- 上下文窗口有限:即使是最新的百万 token 上下文模型,塞一本 500 页的书也会超出限制,而且推理极慢、成本极高
- 信噪比低:整本书 99% 的内容和你的问题无关,LLM 容易被无关信息干扰
- 成本指数增长:上下文越长,推理计算量越大,API 费用越高
RAG 的做法是先筛选、再阅读——只把最相关的几个段落喂给 LLM。
2.3 RAG vs Fine-tuning vs 长上下文
| 方式 | 适合什么 | 不适合什么 |
|---|---|---|
| RAG | 知识频繁变化、需要溯源、数据安全 | 需深度理解文档整体结构的任务 |
| Fine-tuning | 让 LLM 学会特定风格/格式/术语 | 保证知识的准确性(微调学模式,不学事实) |
| 长上下文 | 单篇文档的完整理解 | 多文档、高并发、成本敏感 |
三者不互斥,实际产品中常组合使用:RAG 检索 + 长上下文理解 + Fine-tuning 定制风格。
三、How — 怎么实现?
3.1 核心实现步骤(六步)
步骤一:文档解析
把 PDF/Word/Markdown 等格式的文档提取为纯文本
↓
步骤二:文本切分 (Chunking)
把长文本切成小块(每块 ~500 字),块之间保留少量重叠
↓
步骤三:向量化 (Embedding)
每块文字通过 Embedding 模型转成一组数字(如 384 维向量)
↓
步骤四:向量存储
把所有向量存入向量数据库(如 LanceDB),建索引
↓
步骤五:检索 (Retrieval)
用户问题也转成向量 → 在向量库中找最相似的 Top-K 个文档块
↓
步骤六:生成 (Generation)
文档块 + 问题 → 拼成 Prompt → 发给 LLM → 生成回答
3.2 流程图
用户上传文档 用户提问
│ │
▼ ▼
┌─────────┐ ┌─────────┐
│ 解析文本 │ │ 向量化 │
│pdf-parse│ │384维向量 │
│mammoth │ └────┬────┘
│marked │ │
└────┬────┘ ┌────▼────┐
│ │ 向量检索 │
▼ │ Top-5 │
┌─────────┐ │ 相似度 │
│ 文本切分 │ │ ≥ 0.5 │
│512token │ └────┬────┘
│64overlap│ │
└────┬────┘ ┌────▼────┐
│ │Prompt拼接│
▼ │system+ │
┌─────────┐ │ctx+问题 │
│ 向量化 │ └────┬────┘
│384维 │ │
└────┬────┘ ┌────▼────┐
│ │ LLM生成 │
▼ │llama.cpp│
┌─────────┐ │流式输出 │
│LanceDB │ └────┬────┘
│向量存储 │ │
└─────────┘ ┌────▼────┐
│前端渲染 │
│回答+来源 │
└─────────┘
3.3 详细实现指引
| 你想做的事 | 参考文档 |
|---|---|
| 20 分钟搭一个能用的 RAG 系统 | 二、从零搭建本地 RAG 知识库 |
| 逐行写出完整代码 + 深入理解设计原理 | 三、手把手教你从零写一个本地 RAG |
| 理解 Demo 到生产级的升级路径 | 三、手把手教程 §16(技术选型 + 架构图 + 评估体系) |
| 面试 RAG 相关岗位 | 四RAG 模拟面试问答(27 个问答) |
四、核心概念速查
4.0 Token(词元)
一句话:Token 是 LLM 和 Embedding 模型处理文本的最小单位。
一个中文字 ≈ 1~2个 token,一个英文单词 ≈ 1~2 个 token。当你看到"模型最大支持 512 token"时,意思是它一次只能"读懂"约 300~500 个中文字。
这就是为什么长文档必须切成小块——不是不想一次处理整本书,而是模型的"阅读窗口"有限。
4.1 向量 (Vector)
一句话:向量是一串有序数字,用来表示一段文字在"语义空间"中的位置。
举例:all-MiniLM-L6-v2 模型把任意文本转成一个 384 维向量——即一个包含 384 个浮点数的数组,比如 [0.12, -0.34, 0.78, ...]。
为什么有用:含义相近的文字,在向量空间中距离也近。
"苹果手机" → [0.8, 0.3, 0.1, ...]
"iPhone" → [0.7, 0.4, 0.1, ...] ← 距离很近
"香蕉" → [0.1, -0.5, 0.9, ...] ← 距离很远
4.2 维度 (Dimension)
一句话:向量有多少个数字,就是多少维。
| 维度 | 模型示例 | 特点 |
|---|---|---|
| 384 维 | all-MiniLM-L6-v2 | 轻量,够用 |
| 768 维 | BGE-base-zh-v1.5 | 平衡 |
| 1024 维 | BGE-large-zh-v1.5 | 更精准,但更大更慢 |
| 1536 维 | OpenAI text-embedding-3-small | API 调用,不开源 |
维度越高,语义信息越多,但检索速度越慢、存储越大。384 维对大多数场景已经够用。
4.3 Embedding 模型(向量大模型)
一句话:把文字转成向量的模型。
和 LLM(语言大模型)是两回事:
| Embedding 模型 | LLM(语言大模型) | |
|---|---|---|
| 输入 | 一段文字 | 一段文字 |
| 输出 | 一串数字(向量) | 一段文字(回答) |
| 目的 | 衡量"文字有多像" | 生成自然语言 |
| 大小 | 小(80MB~2GB) | 大(0.5B~数百B 参数) |
| 例子 | all-MiniLM-L6-v2, BGE-large-zh | Qwen2.5, GPT-4, DeepSeek-V3 |
在 RAG 中的分工:Embedding 模型负责"找到相关段落",LLM 负责"基于段落写回答"。
4.4 语言大模型 (LLM)
一句话:能理解和生成人类语言的 AI 模型。
在 RAG 中的角色:阅读检索到的文档片段,基于它们生成自然语言回答。
为什么小模型(0.5B)和大模型(7B+)差距很大?
- 0.5B:能理解简单指令,但容易忽略 Prompt 约束、输出不稳定、中文能力有限。适合 Demo 演示流程
- 7B+:指令遵循能力强、幻觉率明显降低、中文理解和生成质量高。适合生产环境
- 72B+:推理能力强、能处理复杂多跳问题、但硬件要求高
推理引擎 vs 模型文件——为什么 LLM 是两个东西?
很多初学者以为"下载一个大模型"就是一个可双击的
.exe文件。实际上,跑一个本地 LLM 需要两个独立的东西配合:
推理引擎 模型文件 是什么 可执行程序(如 llama-server)权重数据文件(如 .gguf)装了什么 计算逻辑——怎么跑 Transformer 知识——数十亿个训练出来的参数 类比 游戏机 游戏卡带 独立运行? 不能,必须加载模型 不能,只是数据 启动命令本质上是:
llama-server -m qwen2.5-0.5b.gguf——让引擎把模型"吃进去",然后在 8080 端口提供 HTTP API。换模型只需换-m后面的文件路径,引擎不用变。这在你的本地 RAG Demo 中有直接体现:
bin/目录放引擎(llama-server),models/目录放模型文件(.gguf),generator.ts用spawn把二者连接起来。
4.5 相似度 (Similarity)
一句话:衡量两个向量有多"近"的数学指标。
| 方式 | 含义 | 取值范围 | 怎么理解 |
|---|---|---|---|
| 余弦相似度 | 两个向量方向的接近程度 | -1 到 1 | 1 = 完全相同方向,0 = 垂直(无关),-1 = 相反 |
| 欧氏距离 | 空间中两点直线距离 | 0 到 ∞ | 0 = 完全相同,越大越不相似 |
RAG 中最常用余弦相似度。实践中通常设一个阈值(如 0.5),低于这个值的检索结果直接丢弃。
如果所有结果都低于阈值怎么办? 这说明知识库里没有相关内容。此时不应勉强让 LLM "编一个答案"——正确做法是直接告诉用户"未找到相关文档段落,请尝试上传更多相关文件或换个问题"。
4.6 Chunk(文档片段)
一句话:把长文档切成的小块,每块是检索的最小单位。
为什么切分?
- Embedding 模型一次只能处理有限长度的文本
- 小块检索更精准——"关于绩效考核的规定"只应该命中相关段落,而不是整本员工手册
切分关键参数:
- Chunk Size:每块多大(中文建议 300-500 字)
- Chunk Overlap:相邻块重叠多少(防止关键信息被切在边界上)
4.7 向量数据库 (Vector Database)
一句话:专门存储和搜索向量的数据库。
| 传统数据库 | 向量数据库 |
|---|---|
WHERE name = '张三'(精确匹配) | "找和这段话最相似的 5 个段落"(近似匹配) |
| B-Tree 索引 | HNSW / IVF 索引 |
| 返回精确结果 | 返回近似结果(可调精度) |
常用选择:
- Demo/个人项目:LanceDB(嵌入式,零配置)
- 生产/团队:Milvus / Qdrant(分布式,支持混合检索)
4.8 Prompt 模板
一句话:告诉 LLM"你是谁、用什么资料、怎么回答"的指令。
RAG Prompt 的核心要素:
1. 角色定义 → "你是一个文档问答助手"
2. 约束规则 → "仅使用下面的参考资料,不知道就说不知道"
3. 参考资料 → (检索到的文档片段)
4. 用户问题 → (用户实际输入)
5. 格式要求 → "用 Markdown 格式回答"
为什么"不要编造"是关键约束?没有这句,LLM 在检索不到信息时会用自己的训练知识补充回答——看起来合理,但可能完全错误。
五、在线 AI 产品是怎么做的?
5.1 核心结论
DeepSeek、豆包、Kimi 都不是"一个纯 LLM 在回答问题"。它们背后是 LLM + 多种增强技术的组合系统。
5.2 各家产品技术架构对比
| 能力 | DeepSeek | 豆包 (字节) | Kimi (月之暗面) |
|---|---|---|---|
| 核心 LLM | DeepSeek-V3 / R1 | 豆包大模型 (自研) | Moonshot-v1 / k1.5 |
| 联网搜索 | ✅ 手动 | ✅ 自动判断 | ✅ 默认开启 |
| 文档 RAG | — | ✅ 支持上传文件问答 | ✅ Kimi+ 上传文件 |
| 长上下文 | 128K | 128K | 200 万字 |
| 代码/工具 | ✅ 代码解释器 | ✅ 代码执行 | ✅ |
| 推理增强 | ✅ R1 深度思考 | — | ✅ k1.5 思维链 |
| 多模态 | ❌ 纯文本 | ✅ 图片理解 | ✅ 图片/文件 |
| 记忆系统 | ✅ 多轮对话 | ✅ | ✅ |
5.3 它们的增强管线长什么样?
你输入一个问题后,实际发生的是:
你的问题
│
▼
┌─────────────┐
│ 意图识别 │ ← "需要联网吗?需要查文档吗?还是可以直接回答?"
└──────┬──────┘
│
┌────┼────┬──────────┐
▼ ▼ ▼ ▼
┌──┐ ┌──┐ ┌──┐ ┌──────┐
│联 │ │文 │ │工 │ │直接 │
│网 │ │档 │ │具 │ │回答 │
│搜 │ │RA │ │调 │ │(常识) │
│索 │ │G │ │用 │ │ │
└─┬┘ └─┬┘ └─┬┘ └───┬───┘
│ │ │ │
└────┼────┼──────────┘
│ │
▼ ▼
┌─────────────────┐
│ 上下文拼装 │ ← 搜到的网页 + 文档片段 + 工具结果 + 历史对话
└────────┬────────┘
▼
┌─────────────────┐
│ 语言大模型 │ ← 阅读上下文,生成回答
└────────┬────────┘
▼
┌─────────────────┐
│ 格式化 + 引用 │ ← 标注来源、渲染 Markdown
└─────────────────┘
5.4 谁在"计算"答案?
这个问题非常关键:当你在 DeepSeek 里输入一个问题时,是 LLM 自己判断需要联网搜索,还是另一个程序在指挥它?
答案是:另一个程序在指挥 LLM,LLM 只负责"生成文字"。
你的问题: "今天杭州天气怎么样?"
│
▼
┌─────────────────────────┐
│ 编排程序(Orchestrator)│ ← 这不是 LLM,是工程师写的调度代码
│ │
│ "这个问题涉及时效信息, │
│ 需要联网搜索" │
└───────────┬─────────────┘
│
┌─────┼─────┐
▼ ▼
┌──────────┐ ┌──────────┐
│ 搜索引擎 │ │ 直接给LLM │
│ 抓取天气 │ │(无需搜索 │
│ 相关网页 │ │ 的问题) │
└────┬─────┘ └─────┬────┘
│ │
▼ │
┌──────────┐ │
│提取天气数据│ │
│作为上下文 │ │
└────┬─────┘ │
│ │
└──────┬──────┘
▼
┌─────────────────────────┐
│ 语言大模型 (LLM) │
│ │
│ 收到: │
│ "用户问天气,这是搜索到 │
│ 的信息:[网页内容]" │
│ │
│ 输出: │
│ "今天杭州晴转多云, │
│ 气温 18-25°C..." │
└─────────────────────────┘
关键认知:
| 角色 | 谁在做 | 能力边界 |
|---|---|---|
| 编排程序(Orchestrator) | 工程师写的代码 | 判断意图、调用搜索、组装上下文。不会生成文字 |
| 向量模型(Embedding) | 专门的小模型(如 all-MiniLM-L6-v2) | 只做一件事:把文字转成向量。不会生成文字 |
| 语言大模型(LLM) | DeepSeek-V3 / Qwen / GPT | 只做一件事:收到上下文 + 问题 → 生成回答。不会搜索、不会判断意图 |
用餐厅类比:
顾客(你)点菜
│
▼
服务员(编排程序) —— 判断"这个菜厨房能做吗?需要去隔壁菜市场买菜吗?"
│
├── 去菜市场(搜索引擎/向量库)拿食材
│ │
│ ▼
│ 采购员(向量模型) —— 在菜市场找最匹配的食材
│
└── 把食材 + 订单交给厨师
│
▼
厨师(LLM) —— 只负责烹饪。不会买菜,不会接待顾客
回到你的场景:
"我问 DeepSeek 一个简单问题,是 LLM 自动计算最佳匹配结果,还是由另一个程序完成?"
另一个程序完成检索和路由,LLM 只负责最后一步——看着参考资料写回答。
这和你搭建的本地 RAG Demo 完全一致:chat.ts(编排程序)→ retriever.ts(检索)→ generator.ts(调用 LLM)→ 返回。你写的 chat.ts 就是那个"服务员"。
5.5 那"1+1 等于几"这种问题呢?
这个问题触及了关键:不是所有问题都需要走检索流程。
你问 "1+1 等于几?" 你问 "公司去年的营收是多少?"
│ │
▼ ▼
┌───────────────┐ ┌───────────────┐
│ 编排程序 │ │ 编排程序 │
│ │ │ │
│ 这是常识问题 │ │ 这需要查文档 │
│ LLM 自己知道 │ │ 或联网搜索 │
└───────┬───────┘ └───────┬───────┘
│ │
│ 直接跳过检索 │ 先检索再生成
│ │
▼ ▼
┌───────────────┐ ┌───────────────┐
│ LLM │ │ 向量模型检索 │
│ │ │ → 拿到文档 │
│ 回答: 2 │ │ → LLM 生成回答│
└───────────────┘ └───────────────┘
三种问题的处理路径完全不同:
| 问题类型 | 举例 | 编排程序怎么做 | LLM 拿到什么 |
|---|---|---|---|
| 常识/知识类 | "1+1 等于几?"、"中国的首都是?" | 直接发给 LLM | 只有问题本身 |
| 时效类 | "今天天气怎么样?"、"最新的 iPhone 是哪款?" | 先联网搜索,结果作为上下文 | 问题 + 搜索结果 |
| 文档/私有知识类 | "我们公司去年的营收是多少?" | 先从向量库检索,文档片段作为上下文 | 问题 + 文档片段 |
关键认知:即使是"1+1"这种问题,LLM 也不是在"计算",而是在"回忆"。
LLM 本质上是一个文字接龙机器——它在训练数据中见过无数次"1+1=2"这个模式,所以能"猜"出下一个 token 大概率是"2"。它没有内置计算器,也不会真正进行数学运算。
这就是为什么:
- 简单的 1+1 → LLM 能答对(训练数据里见过太多次)
- 复杂的 17 位数乘法 → LLM 经常答错(没见过这个具体算式,只能硬猜)
- 需要精确计算的场景 → 应该让编排程序调用一个真正的计算器,把结果传给 LLM(这就是"工具调用")
总结:编排程序是大脑,LLM 是嘴巴。
| 场景 | 编排程序(决策者) | LLM(执行者) |
|---|---|---|
| "1+1=?" | 判断:不需要检索,直接回答 | 凭训练记忆输出"2" |
| "今天天气?" | 判断:需要联网 → 调搜索引擎 → 把结果拼进 Prompt | 基于天气数据生成自然语言回答 |
| "公司财报分析?" | 判断:需要查文档 → 调向量库检索 → 把 Chunk 拼进 Prompt | 基于文档片段生成分析 |
| "12345 × 67890 = ?" | 判断:需要精确计算 → 调计算器 API → 把结果拼进 Prompt | 基于计算结果生成回答 |
5.6 关键洞察
① 联网搜索 = 互联网版的 RAG
你问"今天天气怎么样"→ 搜索引擎抓取网页 → 提取相关片段 → 喂给 LLM → 生成回答。这和本地文档 RAG 原理完全一样,区别只是"知识源"从本地 PDF 换成了互联网网页。
② 长上下文 ≠ RAG,两者互补
Kimi 的 200 万字上下文是把整本书塞进 LLM,让它通读。好处是不丢信息,代价是慢和贵。实际产品中常混合:RAG 快速筛出候选段落 → 长上下文精细理解。
③ Embedding 模型和 LLM 是两回事,但配合工作
| 谁是 | 做什么 | 在你的电脑上 |
|---|---|---|
| Embedding 模型 (all-MiniLM-L6-v2) | 把文字转成向量,做搜索 | ✅ 本地 CPU 跑 |
| LLM (Qwen2.5-0.5B) | 生成回答 | ✅ 本地 CPU 跑 |
| DeepSeek 的 Embedding | DeepSeek 用自研 embedding 做搜索 | ❌ 云端 |
| DeepSeek 的 LLM | DeepSeek-V3 生成回答 | ❌ 云端 |
本地 RAG 的价值在于:你把"搜索"和"回答"都搬到自己电脑上,数据不离开你的硬盘。
六、延伸阅读
| 你想了解 | 去看 |
|---|---|
| 从零安装和使用 RAG 系统 | 二、从零搭建本地 RAG 知识库 |
| 逐行写出 RAG 的全部代码 | 三、手把手教你从零写一个本地 RAG |
| Demo 怎么升级到生产级 | 三、手把手教程 §16 (技术选型 + 架构图 + 评估体系) |
| 面试 RAG 岗位 | 四、RAG 模拟面试问答 |