RAG 系统全景:从原理到工程实践

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 的工程细节。


系列