RAG 系统全景:从原理到工程实践
RAG(Retrieval-Augmented Generation,检索增强生成)是当下 AI 应用开发中最热的技术方向之一。
GitHub 上 RAG 相关的项目数以万计,几乎所有 AI 框架都把 RAG 作为核心能力,Stack Overflow 上关于 RAG 的问题更是不计其数。如果你正在构建需要 AI 回答"不在训练数据里的问题"的应用,RAG 几乎是绕不开的技术选型。
但 RAG 也是被严重低估的技术方向。很多人觉得 RAG 就是"把文档切片扔进向量库然后检索",实际上从原型到生产,RAG 的每个环节都有大量工程细节。一个 naive 的 RAG 管线和一个调优过的 RAG 管线,效果可以差十倍。
这篇文章不讲代码,先帮你建立 RAG 的全局认知。
一、RAG 解决什么问题
大模型有两个致命缺陷:
知识有截止日期。 GPT-4o 的知识截止到 2025 年 5 月。你问它之后发生的事,它大概率在编。
没有私有数据。 你公司的内部文档、产品手册、技术规范、历史决策记录——这些内容模型的训练数据里根本没有。你问 ChatGPT "我们公司的请假流程是什么",它只能给你一个通用答案。
RAG 解决的就是这两个问题。它的核心思路很简单:先从知识库里检索出和用户问题相关的文档片段,把这些片段塞进提示词,再让大模型基于这些信息生成回答。
相当于给大模型开了一个外接硬盘。模型本身没有这些知识,但通过检索,它能"临时读取"这些知识来回答问题。
一个直观的对比:
| 场景 | 不用 RAG | 用 RAG |
|---|---|---|
| "2026 年 3 月的产品发布会讲了什么?" | 编一个看似合理但完全错误的答案 | 从发布会纪要中检索出准确信息 |
| "我们公司的代码规范是什么?" | 给一个通用的代码规范建议 | 从公司内部文档中检索出实际规范 |
| "这个 bug 之前有没有人遇到过?" | 不知道 | 从历史 issue 中找到相似案例和解决方案 |
RAG 的价值不在于让模型更聪明,而在于让模型能访问它本来不知道的信息。
二、RAG 管线全貌
一个完整的 RAG 管线大致分六步:
┌─────────────────────────────────────────────────────┐
│ RAG 管线 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 文档加载 │→│ 文档切分 │→│ 向量化 │ │
│ │ Loading │ │ Splitting│ │Embedding │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 生成回答 │←│ 检索相关 │←│ 向量存储 │ │
│ │Generation│ │Retrieval │ │ Storage │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ ← 离线阶段(建索引)→ ← 在线阶段(回答查询)→ │
└─────────────────────────────────────────────────────┘
离线阶段:建索引
Step 1:文档加载(Loading)
读取原始文档。PDF、Word、网页、Markdown、数据库记录、API 返回数据……不同来源需要不同的加载器。
这一步看起来简单,但实际坑很多。PDF 的表格和图表怎么处理?网页的导航栏和广告怎么过滤?扫描件的 OCR 质量怎么保证?文档加载的质量直接决定了 RAG 管线的上限。
Step 2:文档切分(Splitting)
把长文档切成小块(chunk)。这是 RAG 中最影响效果的环节之一,也是最容易被忽视的环节。
切分太粗,一个 chunk 里包含太多不相关内容,检索时引入噪声。切分太碎,一个 chunk 里没有足够的上下文,模型无法理解其含义。
常见的切分策略:
- 固定长度切分:按字符数或 token 数切,简单粗暴
- 递归切分:先按段落切,太长的段落再按句子切,逐层递归
- 语义切分:按主题或语义边界切,每块内容自洽
- 父子 chunk:小块用于检索,大块用于生成,兼顾精准检索和上下文
Step 3:向量化(Embedding)
把文本块转成数字向量。Embedding 模型会把语义相近的文本映射到向量空间中相近的位置。
这一步的核心是 Embedding 模型的选择。不同模型的维度、速度、效果差异很大。OpenAI 的 text-embedding-3-large(3072 维)、Cohere 的 embed-v3(1024 维)、开源的 BGE-large(1024 维)各有优劣。
Step 4:向量存储(Storage)
把向量存入向量数据库。主流选择有 Chroma(轻量、本地)、Milvus(高性能、分布式)、Pinecone(全托管云服务)、Weaviate(开源、混合检索)等。
在线阶段:回答查询
Step 5:检索(Retrieval)
用户提出一个问题,先把它向量化,然后在向量数据库中找最相似的 k 个 chunk。
检索不是简单的"找最相似"。实际生产中往往需要:
- 混合检索:向量检索 + 关键词检索(BM25),互相补充
- Query 改写:用户的问题可能表述不清,先让 LLM 改写或拆解
- Reranking:初步检索结果可能不够精准,用交叉编码器重排序
- 多路召回:从多个角度检索,合并结果
Step 6:生成(Generation)
把检索结果作为上下文,连同用户问题一起塞进提示词,让大模型生成回答。
生成的核心挑战是幻觉控制——模型可能不基于检索结果回答,而是"脑补"。需要在提示词中明确要求模型只基于提供的上下文回答,并在不确定时说明。
三、核心组件选型地图
RAG 的每个环节都有多种选择。以下不是完整的选型指南,而是帮你建立"知道有哪些选项"的认知。
3.1 文档加载器
| 类型 | 工具 | 适用场景 |
|---|---|---|
| 文件解析 | PyPDFLoader, Docx2txtLoader, Unstructured | PDF、Word、PPT 等本地文件 |
| 网页抓取 | WebBaseLoader, CheerioLoader, Playwright | 网页内容、需要 JS 渲染的页面 |
| 数据库 | SQLDatabaseLoader, MongoDBAtlasLoader | 结构化数据、NoSQL 数据 |
| API | ApifyWrapper, GitHub API Loader | 第三方平台数据 |
| 通用 | Unstructured(统一接口) | 多格式混合场景 |
3.2 文本切分器
| 策略 | 工具 | 特点 |
|---|---|---|
| 固定长度 | CharacterTextSplitter | 简单快速,不保留语义边界 |
| 递归切分 | RecursiveCharacterTextSplitter | 按段落→句子→词逐层切,保留语义 |
| 语义切分 | SemanticChunker | 基于 Embedding 相似度检测主题边界 |
| 父子 chunk | ParentDocumentRetriever | 小块检索、大块生成,兼顾精准和上下文 |
| 滑动窗口 | SentenceWindowRetriever | 检索句子,返回上下文窗口 |
3.3 Embedding 模型
| 模型 | 维度 | 特点 |
|---|---|---|
| text-embedding-3-large | 3072 | OpenAI 最强,效果好但贵 |
| text-embedding-3-small | 1536 | OpenAI 性价比款,效果不错 |
| embed-v3 | 1024 | Cohere,支持压缩维度 |
| BGE-large | 1024 | 开源首选,中英文效果好 |
| GTE-large | 1024 | 阿里开源,中文场景优秀 |
| E5-large | 1024 | Meta 开源,多语言支持好 |
3.4 向量数据库
| 数据库 | 类型 | 特点 |
|---|---|---|
| Chroma | 嵌入式 | 零配置,适合原型和中小规模 |
| Milvus | 分布式 | 高性能,支持大规模数据,运维复杂 |
| Pinecone | 云服务 | 全托管,无需运维,按量付费 |
| Weaviate | 开源 | 支持混合检索(向量+关键词),GraphQL 接口 |
| Qdrant | 开源 | 高性能,支持过滤和混合检索 |
| pgvector | 扩展 | PostgreSQL 向量扩展,适合已有 PG 基础设施 |
3.5 检索策略
| 策略 | 说明 | 适用场景 |
|---|---|---|
| 向量检索 | 基于余弦相似度找最相似 chunk | 语义匹配 |
| 关键词检索(BM25) | 基于词频和逆文档频率 | 精确关键词匹配 |
| 混合检索 | 向量 + 关键词,RRF 或加权融合 | 大多数生产场景 |
| Reranking | 用交叉编码器对初步结果重排序 | 需要高精度的场景 |
| 多路召回 | 多个检索器并行,合并结果 | 复杂查询 |
四、从项目看 RAG:Claude Code 记忆系统
理论讲完了,看一个真实的 RAG 系统长什么样。
Claude Code 的记忆系统 MAGMA 就是一个典型的 RAG 实现。它的核心存储是 LanceDB 向量数据库,配合 Obsidian 笔记作为知识源。
系统架构
用户查询
│
▼
┌─────────────────────┐
│ IntelligentRouter │ ← 意图分析 + 路由决策
│ (意图路由器) │
└──────────┬──────────┘
│
┌──────┴──────┐
▼ ▼
┌───────┐ ┌──────────┐
│ CCB │ │ MAGMA │
│(文件 │ │ (向量 │
│ 系统) │ │ 数据库) │
│ │ │ │
│Markdown│ │ LanceDB │
│ 文件 │ │ Obsidian │
│ 分类 │ │ 知识图谱 │
└───┬───┘ └────┬─────┘
│ │
▼ ▼
┌─────────────────────────┐
│ CrossSystemRetriever │ ← 跨系统检索 + 去重
│ (跨系统检索器) │
└──────────┬──────────────┘
│
▼
┌─────────────────────────┐
│ MemoryFusion │ ← 语义去重 + 排序 + Token预算
│ (记忆融合器) │
└──────────┬──────────────┘
│
▼
注入到 Claude 上下文
MAGMA 的 RAG 实现
MAGMA 的适配器(MagmaAdapter)连接 LanceDB 向量数据库,核心检索逻辑:
// 从 LanceDB 检索记忆
async readLanceDB(query: string, limit: number = 10, layer?: string) {
const lancedb = await this.loadLanceDB()
const db = await lancedb.connect({
uri: this.config.lancedbPath,
apiKey: this.config.lancedbApiKey
})
const queryVector = await this.embedQuery(query, lancedb)
for (const tableName of await db.tableNames()) {
const table = await db.openTable(tableName)
const results = await table
.vectorSearch(queryVector)
.limit(limit)
.distanceType('COSINE')
.toArray()
// score = 1 - cosine_distance
results.push({ key, value, score: 1 - entry._distance })
}
return results.sort((a, b) => b.score - a.score)
}
这个实现比较基础——纯向量检索,COSINE 距离,按分数排序。没有混合检索,没有 reranking,没有 query 改写。
这恰好说明了一个问题:即使是 Claude Code 这样的产品,记忆系统的 RAG 实现也偏向基础。 大多数生产 RAG 系统的复杂度不来自单个环节的实现,而来自多个环节的协同和调优。
为什么基础实现就够了?
因为 Claude Code 的记忆系统有它独特的优势:数据质量极高。
记忆不是从网上爬的文档,而是用户自己写的——用户偏好、项目背景、反馈指导。每条记忆都是人工撰写的,格式规范、语义清晰、长度适中。
这揭示了一个 RAG 的核心规律:数据质量 > 检索算法。 高质量的数据即使配合基础的检索算法,效果也远好于低质量数据配合复杂的检索算法。
五、RAG 效果不好的五个常见坑
如果你搭了一个 RAG 系统但效果不理想,大概率是踩了以下五个坑之一:
坑 1:文档切分太粗或太碎
症状:检索结果包含大量不相关内容,或者太短导致模型无法理解。
诊断:打印出检索到的 chunk,看它们是否语义完整。如果一个 chunk 里混杂了三个不同主题,说明切分太粗。如果一个 chunk 只有半句话,说明切分太碎。
解决:根据文档类型调整切分策略。技术文档用递归切分,对话记录按轮次切分,代码文件按函数切分。
坑 2:Embedding 模型和数据不匹配
症状:检索结果和问题的语义相关性不高,明明有相关文档但检索不到。
诊断:用几个典型问题手动检查检索结果。如果人类觉得相关但检索结果不相关,说明 Embedding 模型没有正确理解你的数据。
解决:换一个更适合你数据类型的 Embedding 模型。中文数据试试 BGE 或 GTE,多语言数据试试 E5。
坑 3:检索结果没有上下文
症状:检索到了相关 chunk,但模型回答时缺少必要的前后文信息。
诊断:看检索到的 chunk 是否自包含。如果 chunk 里大量使用"如上所述"、"这个功能"等指代,说明上下文被切断了。
解决:增大 overlap,或者用父子 chunk 策略——小块用于检索,大块用于生成。
坑 4:提示词没有约束幻觉
症状:模型不基于检索结果回答,而是结合自己的"知识"生成看似合理但实际错误的内容。
诊断:把检索结果中的关键信息改成明显错误的,看模型是否仍然基于检索结果回答。
解决:在提示词中明确约束:"只基于以下上下文回答。如果上下文不包含相关信息,请说明你不知道。不要编造信息。"
坑 5:没有评估,优化靠猜
症状:改了切分策略、换了 Embedding 模型、调整了检索参数,但不知道哪个改动有效果。
诊断:你有没有一个标准化的评估流程来衡量 RAG 效果?
解决:建立评估体系。至少要有:测试问题集、期望答案、评估指标(相关性、忠实度、完整性)。每次改动后跑一遍评估,用数据说话。
六、RAG 系统的复杂度地图
回到开头的问题:RAG 到底复不复杂?
原型很简单。 几十行代码就能跑通一个 RAG 管线:加载文档 → 切分 → 向量化 → 检索 → 生成。LangChain 的教程就是这个路径。
生产很复杂。 每个环节都有多种选择,不同选择之间的组合是指数级增长的。而且 RAG 的效果不是单个环节决定的,而是整条管线协同的结果。
简单 ←─────────────────────────────────────→ 复杂
基础 RAG 调优 RAG 生产 RAG 企业级 RAG
─────────────────────────────────────────────────
固定切分 递归切分 语义切分 自适应切分
单一向量检索 混合检索 混合+Reranking 多路召回+融合
基础提示词 约束提示词 动态提示词 多轮对话+记忆
无评估 人工评估 自动化评估 持续监控+反馈
这篇文章帮你建立了全局认知。接下来的系列文章会深入每个环节,从文档切分开始,一步步拆解 RAG 的工程细节。
系列: