RAG,全称 Retrieval-Augmented Generation,核心思想其实很朴素:不要把所有知识都塞进模型参数里,而是在回答问题前,先从外部知识库里把最相关的信息找出来,再交给模型生成答案。
这套方法之所以重要,是因为纯大模型回答经常会遇到三类问题:
- 知识过期 — GPT-4 的训练数据截止到 2023 年底,你问它 2024 年 3 月的新闻,它要么说不知道,要么编一个。
- 专有知识缺失 — 你们公司的内部文档、API 规范、业务规则,模型从来没见过。
- 幻觉问题 — 模型会"自信地胡说八道",尤其是当它不确定的时候。
RAG 的目标不是让模型"更聪明",而是让系统"更可控"。你把知识更新、答案依据、召回范围、调试手段都从模型参数里解耦了出来。
一、RAG 解决的本质问题
先想清楚一个问题:为什么不让模型直接回答,而要先检索?
答案是:模型的参数空间是有限的,但知识是无限的。
GPT-4 有 1.8 万亿参数,看起来很多,但这些参数要编码:语言规律、世界知识、推理能力、常识……真正留给"事实知识"的空间并不多。而且参数一旦训练完成,就固定了。你想让它知道你们公司最新的报销流程?只能重新训练,成本几十万起步。
RAG 的思路是:把"事实知识"从参数里剥离出来,放到外部存储里。
传统模型:
知识 → 训练 → 参数 → 推理 → 答案
(知识固化在参数里,更新成本高)
RAG:
知识 → 索引 → 向量库 → 检索 → 推理 → 答案
(知识和模型解耦,更新成本低)
代价是:每次回答前,需要先检索。检索的质量直接决定了答案的质量。
二、Embedding:把文本变成向量
RAG 的第一步是让机器"理解"文本。方法是 Embedding——把文本映射到一个高维向量空间。
Embedding 的直觉
假设我们有一个 3 维空间,每个词可以表示为一个点:
"猫" → (0.2, 0.8, 0.1)
"狗" → (0.3, 0.7, 0.2) ← 和"猫"很近,都是宠物
"汽车" → (0.9, 0.1, 0.3) ← 和"猫"很远,不同类别
在这个空间里,语义相似的词距离近,语义不同的词距离远。
实际的 Embedding 维度是几百到几千维,但原理一样:让语义相似的文本在向量空间里距离近。
训练 Embedding 模型
Embedding 模型是怎么训练出来的?核心思想是:让相似文本的向量靠近,不相似文本的向量远离。
常用的训练方式有两种:
1. 对比学习(Contrastive Learning)
准备大量"相似对"和"不相似对":
相似对:
("如何重置密码", "密码重置方法")
("API 超时设置", "配置 API 超时时间")
不相似对:
("如何重置密码", "API 超时设置")
("密码重置方法", "今天天气怎么样")
训练目标:让相似对的向量距离小,不相似对的向量距离大。
损失函数(InfoNCE Loss):
L = -log( exp(sim(q, p+)/τ) / Σexp(sim(q, pi)/τ) )
其中:
- q 是 query 的向量
- p+ 是正样本的向量
- pi 是所有样本(包括正样本和负样本)
- sim 是相似度函数(如余弦相似度)
- τ 是温度参数
直觉解释:让正样本的相似度占主导,负样本的相似度被压制。
2. 自监督学习
不用人工标注,利用文本本身的结构:
- 同一个句子/段落的不同部分互为正样本
- 不同句子/段落互为负样本
代表性方法:SimCSE,把同一个句子丢进模型两次(不同的 dropout mask),得到的两个向量应该相似。
主流 Embedding 模型
| 模型 | 维度 | 特点 | 适用场景 |
|---|---|---|---|
| text-embedding-3-small | 1536 | OpenAI,多语言 | 通用场景 |
| text-embedding-3-large | 3072 | OpenAI,更高精度 | 高质量要求 |
| bge-large-zh | 1024 | 开源,中文优化 | 中文场景 |
| bge-m3 | 1024 | 开源,多语言 | 多语言混合 |
| e5-large-v2 | 1024 | 开源,需要加前缀 | 英文场景 |
选择原则:
- 中文为主:选 bge-large-zh
- 多语言混合:选 text-embedding-3-small 或 bge-m3
- 追求高质量且有预算:选 text-embedding-3-large
- 私有化部署:选 bge 系列
三、向量检索:在高维空间找邻居
有了向量,接下来是检索:给定一个 query 向量,找到最相似的文档向量。
相似度计算
最常用的是余弦相似度:
cosine(a, b) = (a · b) / (||a|| × ||b||)
其中:
- a · b 是向量点积
- ||a|| 是向量的模长
结果范围:[-1, 1],越大越相似
为什么用余弦相似度而不是欧氏距离?
因为 Embedding 的方向比长度更重要。两个文本的语义相似,体现在向量方向相近,而不是长度相近。余弦相似度只看方向,不看长度。
暴力检索的问题
最简单的检索方式是暴力计算:把 query 和所有文档向量都算一遍相似度,排序取 top-k。
问题:慢。
假设向量维度 1024,文档数量 100 万:
单次检索计算量:
100万 × 1024 = 10亿次浮点运算
耗时估算:
单核 CPU ≈ 100-500ms
GPU ≈ 10-50ms
100 万文档还算小,到了 1000 万、1 亿,暴力检索就撑不住了。
近似最近邻搜索(ANN)
解决方案:用精度换速度。不要求找到"最近的",只要找到"足够近的"。
主流算法:
1. HNSW(Hierarchical Navigable Small World)
思路:构建一个多层的图结构,每层是一个小世界网络。
第 0 层:所有节点
第 1 层:部分节点(概率选取)
第 2 层:更少的节点
...
搜索时:
从最高层开始,快速跳到目标区域附近
逐层下降,越来越精确
最后在第 0 层找到最近邻
类比:找北京的某个人。先在世界地图上定位到中国,再在中国地图上定位到北京,再在北京地图上定位到朝阳区……每一步都在缩小范围。
2. IVF(Inverted File Index)
思路:把向量空间划分为多个区域(聚类中心),每个区域维护一个倒排表。
离线阶段:
1. 用 K-means 聚类,找到 K 个中心点
2. 把每个向量分配到最近的中心点
3. 每个中心点维护一个列表(倒排表)
在线检索:
1. 找到离 query 最近的几个中心点
2. 只在这几个中心点的倒排表里搜索
3. 不用遍历所有向量
参数选择:nprobe(探测几个中心点)。nprobe 越大,精度越高,速度越慢。
3. PQ(Product Quantization)
思路:压缩向量,减少内存占用和计算量。
原始向量:1024 维浮点数 = 4096 字节
压缩方法:
1. 把 1024 维分成 8 组,每组 128 维
2. 每组用 K-means 聚类,生成 256 个中心点
3. 每个向量每组只需 1 字节(存储中心点编号)
压缩后:8 字节
压缩比:512 倍
代价:精度损失。但换来的是内存占用大幅降低,可以索引更多文档。
向量库的选择
| 向量库 | 算法 | 特点 | 适用场景 |
|---|---|---|---|
| Chroma | HNSW | 轻量,易上手 | 开发/小规模 |
| Qdrant | HNSW | 开源,功能全 | 中等规模生产 |
| Milvus | 多种 | 分布式,高性能 | 大规模生产 |
| Pinecone | 专有 | 全托管,免运维 | 不想运维 |
| Weaviate | HNSW | 原生支持多模态 | 多模态场景 |
四、为什么单靠向量检索不够
向量检索很强大,但有天生的局限。
局限 1:对精确匹配不敏感
例子:
文档:"错误码 401 表示认证失败"
用户问:"401 是什么意思"
向量检索可能召回:
"常见错误码及解决方案"(语义相似)
"权限问题排查"(语义相似)
而不是精确包含 "401" 的那段。
原因:Embedding 模型学习的是语义相似性,不是字面匹配。“401” 这个数字在向量空间里没有特殊意义,它只是一个 token。
局限 2:对专业术语不稳定
例子:
文档:"JSON Web Token 的有效期默认为 1 小时"
用户问:"JWT 的有效期"
向量检索:可能召回,也可能不召回,取决于模型是否学过 JWT = JSON Web Token
如果模型训练数据里没见过这个缩写,就找不到关联。
局限 3:对编号/版本号不稳定
例子:
文档:"v2.3.1 版本新增了批量导出功能"
用户问:"v2.3.0 有什么新功能"
向量检索:可能召回 v2.3.1 或 v2.3.2 的内容,因为"版本更新"语义相似
版本号之间的微小差异,在向量空间里可能被"版本"这个大概念淹没。
解决方案:混合检索
这就是为什么生产环境几乎都用 Hybrid Retrieval:
候选集 = 向量检索(语义) + 关键词检索(字面) + 元数据过滤(业务规则)
下一篇文章会详细讲混合检索和 Rerank 的算法。
五、RAG 的三阶段架构
理解了 Embedding 和检索,我们来看 RAG 的整体架构。一个成熟的 RAG 系统分为三个阶段:
┌─────────────────────────────────────────────────────────────────┐
│ 阶段 1:索引(Indexing) │
│ │
│ 文档 → 解析 → 切分 → Embedding → 写入向量库 │
│ │
│ 核心问题:怎么切分才能让每个 chunk 成为独立的证据? │
└────────────────────────┬────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ 阶段 2:检索(Retrieval) │
│ │
│ Query → Embedding → 向量检索 → 关键词检索 → 融合 → Rerank │
│ │
│ 核心问题:怎么召回相关内容,同时减少噪声? │
└────────────────────────┬────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ 阶段 3:生成(Generation) │
│ │
│ Query + Chunks → Prompt 组装 → LLM → 答案 + 引用 │
│ │
│ 核心问题:怎么让模型严格依据证据回答,不自由发挥? │
└─────────────────────────────────────────────────────────────────┘
每个阶段都有特定的优化空间,而且问题往往出现在你忽视的地方。
索引阶段的关键决策
1. 切分粒度
太大:噪声多,模型注意力分散 太小:信息碎片化,上下文丢失
经验值:300-500 tokens(中文约 200-350 字)
但比长度更重要的是语义完整性。一个 chunk 应该能独立成为一个"证据"。
2. 上下文补充
一个常见的坑:
原始文档:
### 2.3 校准流程
将旋钮调至 3 档,等待指示灯变为绿色。
按段落切分后,chunk 变成:
"将旋钮调至 3 档,等待指示灯变为绿色。"
问题:用户问"ZK-200 怎么校准",这个 chunk 不会召回,因为:
- 没有"ZK-200"(在文档标题里)
- 没有"校准"(在章节标题里)
解决方案:把父级标题拼到 chunk 前面。
补充上下文后:
"【ZK-200 操作手册 > 2.3 校准流程】
将旋钮调至 3 档,等待指示灯变为绿色。"
这个技巧叫 Contextual Chunking,成本低,效果好。
3. 元数据设计
每个 chunk 至少要存储:
{
"id": "唯一标识",
"content": "文本内容",
"document_id": "所属文档",
"source": "知识库来源",
"title": "文档标题",
"section": "章节路径",
"chunk_index": "在文档中的位置",
"created_at": "创建时间"
}
这些元数据在检索时可以做过滤(只搜特定来源)、在回答时可以做引用(答案来自哪个文档)。
检索阶段的关键决策
1. 召回数量
召回太少:容易漏掉正确答案 召回太多:噪声多,成本高
经验值:
- 召回阶段:50-100 个候选
- Rerank 后:保留 10-20 个
- 最终进 prompt:3-8 个
2. 检索策略
简单场景:纯向量检索够用 复杂场景:向量 + BM25 混合 高精度场景:混合 + Rerank
3. 过滤策略
利用元数据缩小检索范围:
用户问:"销售合同模板在哪下载?"
不过滤:
搜索范围 = 所有文档(产品、研发、销售、HR...)
结果可能混入研发部门的合同管理规范
按 department="sales" 过滤:
搜索范围 = 销售部门文档
结果更精准
生成阶段的关键决策
1. Prompt 设计
核心原则:让模型"不得不"依据证据回答。
差的 Prompt:
参考资料:{chunks}
问题:{query}
请回答:
模型可能无视参考资料,自由发挥。
好的 Prompt:
你是一个文档助手。请根据以下参考资料回答问题。
要求:
1. 答案必须基于参考资料,不要编造
2. 如果参考资料中没有答案,请直接说"根据现有资料无法回答"
3. 回答时标注引用来源,格式:[来源: 文档名 > 章节]
参考资料:
{chunks}
问题:{query}
请回答:
2. 上下文预算
模型有上下文长度限制,不能无限塞内容。更重要的是:内容越多,模型注意力越分散。
经验值:检索内容不超过 4000 tokens(约 3000 字中文)
分配建议:
- 关键证据:2000-3000 tokens
- Prompt 模板:200-300 tokens
- 用户问题和输出:预留 1000+ tokens
3. 引用机制
让模型标注答案的来源:
答案:"API 的超时时间默认为 30 秒 [来源: API 规范 > 3.2 超时配置]"
好处:
- 用户可以验证答案
- 系统可以追溯错误
- 增加答案的可信度
六、RAG vs 微调:怎么选
很多人问:应该用 RAG 还是微调?
答案:不冲突,解决的问题不同。
| 问题类型 | 解决方案 | 原因 |
|---|---|---|
| 知识过时 | RAG | 更新知识库即可 |
| 专有知识缺失 | RAG | 模型没见过这些知识 |
| 输出格式不稳定 | 微调 | 让模型学会特定格式 |
| 输出风格不对 | 微调 | 让模型掌握特定风格 |
| 工具使用不规范 | 微调 | 让模型学会调用模式 |
RAG 擅长:解决"知道什么" 微调擅长:解决"怎么做"
实际项目中,往往是两者结合:
微调:
- 让模型学会输出 JSON 格式
- 让模型学会引用来源
- 让模型学会说"不知道"
RAG:
- 提供最新的业务知识
- 提供可追溯的证据
- 提供领域专业信息
七、第一版 RAG 应该怎么搭
如果你从零开始,我的建议是:
不要一上来就追求复杂架构。第一版的目标是"可解释",不是"最优"。
推荐的第一版配置
| 模块 | 配置 | 原因 |
|---|---|---|
| 文档导入 | 手动,按需导入 | 先保证质量,再追求自动化 |
| 切分策略 | 按段落,500 字左右 | 简单可控,效果有保障 |
| Embedding | text-embedding-3-small 或 bge-large-zh | 成熟稳定,效果好 |
| 检索 | 纯向量检索,top_k=5 | 先跑通,再优化 |
| Prompt | 明确约束 + 引用要求 | 确保答案有依据 |
| Inspect | 保留每次检索的候选列表 | 方便定位问题 |
第一版容易犯的错误
错误 1:上来就全量导入
把公司所有文档都丢进去,结果检索噪声巨大,答案质量很差。
正确做法:先导入核心文档(100-200 篇),验证效果,再逐步扩展。
错误 2:忽视元数据
只存内容和向量,其他什么都不留。后面想做过滤、引用、增量更新,发现数据不够。
正确做法:一开始就设计好元数据结构,chunk 要保留来源、章节、时间等信息。
错误 3:不做评测
优化全靠"感觉",没有客观标准。
正确做法:准备 20-50 个测试问题,固定评测,每次改动都要看指标变化。
错误 4:没有 Inspect 能力
答案错了,不知道是检索错还是生成错。
正确做法:保留每次请求的中间结果(候选列表、分数、prompt),方便排查。
八、如何判断系统准备好了
在上线前,问自己这些问题:
□ 能回答核心问题吗?
└── 准备 20 个关键问题,正确率 > 70%
□ 能解释答案来源吗?
└── 每个答案都有引用,用户能追溯
□ 答案错了能定位吗?
└── 有 Inspect 页面,能看到检索结果
□ 知道下一步优化什么吗?
└── 有评测指标,知道哪类问题表现差
□ 文档更新后能同步吗?
└── 有增量更新机制,不是全量重建
如果这些问题都回答不了,说明系统还不成熟,需要继续打磨。
九、下一篇文章讲什么
这篇文章讲了 RAG 的基础:
- Embedding 的原理和选择
- 向量检索的算法和局限
- 三阶段架构的关键决策
下一篇会深入讲:
- Chunking 的策略:怎么切才能保留语义完整性
- 混合检索:BM25 算法原理、RRF 融合公式
- Rerank:Cross-Encoder 的工作原理、为什么比向量检索更准
这些是 RAG 质量提升的关键,也是最容易踩坑的地方。
