Featured image of post RAG入门:从问题定义到系统设计

RAG入门:从问题定义到系统设计

从0到1理解 RAG 的目标、核心组件、数据流,以及第一版系统应该如何落地。

RAG,全称 Retrieval-Augmented Generation,核心思想其实很朴素:不要把所有知识都塞进模型参数里,而是在回答问题前,先从外部知识库里把最相关的信息找出来,再交给模型生成答案。

这套方法之所以重要,是因为纯大模型回答经常会遇到三类问题:

  1. 知识过期 — GPT-4 的训练数据截止到 2023 年底,你问它 2024 年 3 月的新闻,它要么说不知道,要么编一个。
  2. 专有知识缺失 — 你们公司的内部文档、API 规范、业务规则,模型从来没见过。
  3. 幻觉问题 — 模型会"自信地胡说八道",尤其是当它不确定的时候。

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-small1536OpenAI,多语言通用场景
text-embedding-3-large3072OpenAI,更高精度高质量要求
bge-large-zh1024开源,中文优化中文场景
bge-m31024开源,多语言多语言混合
e5-large-v21024开源,需要加前缀英文场景

选择原则:

  • 中文为主:选 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 倍

代价:精度损失。但换来的是内存占用大幅降低,可以索引更多文档。

向量库的选择

向量库算法特点适用场景
ChromaHNSW轻量,易上手开发/小规模
QdrantHNSW开源,功能全中等规模生产
Milvus多种分布式,高性能大规模生产
Pinecone专有全托管,免运维不想运维
WeaviateHNSW原生支持多模态多模态场景

四、为什么单靠向量检索不够

向量检索很强大,但有天生的局限。

局限 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 字左右简单可控,效果有保障
Embeddingtext-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 质量提升的关键,也是最容易踩坑的地方。

最后修改于 Jun 15, 2026 14:56 +0800
使用 Hugo 构建
主题 StackJimmy 设计