RAG 系统设计学习笔记
学习日期:2026-04-21 | 难度:中级
什么是 RAG,为什么需要它
LLM 有两个天然缺陷:知识有截止日期,以及不知道你的私有数据。
RAG(Retrieval-Augmented Generation,检索增强生成)的思路很直接:用户提问时,先去外部知识库里找到相关内容,再把这些内容连同问题一起交给 LLM 生成回答。这样 LLM 就不再依赖训练时的记忆,而是”现查现答”。
flowchart LR U([用户提问]) --> R[检索相关文档] R --> L[LLM 生成回答] DB[(知识库)] --> R L --> A([回答])
听起来简单,但魔鬼在细节里——检索不准,后面全白费。这也是 RAG 工程的核心难点所在。
什么时候用 RAG,什么时候不用
不是所有场景都需要 RAG,先做两步判断:
flowchart TD Q[有外部知识需求?] -->|否,通用知识| A[直接用 LLM] Q -->|是,私有/实时数据| B[数据量多大?] B -->|1本文档,结构清晰| C[目录 index + LLM 二次查询\nAgentic RAG] B -->|几百份以上 / 每日更新| D[RAG\n自动 Indexing Pipeline] D --> E{需要精确过滤?\n时间、分类、标签} E -->|是| F[RAG + Metadata Filter] E -->|否| G[标准 RAG]
本质分界线是维护成本:一本手册可以手动建目录,几百本手册每天更新就必须自动化。RAG 的 Indexing Pipeline 就是把”手动建索引”这件事自动化。
Agentic RAG:不用向量相似度决定查哪里,而是把决策权交给 LLM——让它看目录自己决定去哪个章节找答案。适合结构清晰、边界明确的文档,是 Long Context 和传统 RAG 之间的折中方案。
完整系统架构
RAG 系统由两条完全独立的流水线组成:离线入库和在线查询。
flowchart TD subgraph Indexing ["📥 Indexing Pipeline(离线,批量跑)"] P[PDF / 文档] --> C[Chunking 分块] C --> E[Embedding 向量化] E --> S[(pgvector + ES\n同时存向量和倒排索引)] end subgraph Query ["🔍 Query Pipeline(在线,低延迟)"] U([用户提问]) --> QR[Query Rewriting / HyDE] QR --> MF[Metadata Filter 前置过滤] MF --> HS[Hybrid Search\nBM25 + 向量检索] HS --> RRF[RRF 合并排序] RRF --> RR[Reranker 精排] RR --> LLM[LLM 生成回答] LLM --> ANS([最终回答]) end S --> HS
两条流水线解耦的好处:Indexing 可以慢慢跑,Query 只需要关注延迟。
核心概念详解
1. Query-Document Asymmetry(查询-文档语义不对称)
这是 RAG 最根本的问题,很多人忽视它。
用户提问用的是问句(“苹果公司赚钱吗”),文档里写的是陈述句(“净利润率 21%,同比增长 3%”)。两者语义相关,但在 embedding 向量空间里,问句和陈述句的结构不同,向量距离天然就远——相关内容反而检索不到。
解法一:HyDE(Hypothetical Document Embedding)
不直接用问题去检索,而是先让 LLM 生成一段”假答案”,用假答案的 embedding 去检索。用”答案形式”找”答案形式”,向量空间里天然对齐。
flowchart LR Q["用户问:苹果公司赚钱吗?"] --> LLM["LLM 生成假答案\n'苹果2024年净利润约XXX亿,利润率21%...'"] LLM --> E[假答案 Embedding] E --> DB[(向量库检索)] DB --> R[真实文档]
解法二:Query Rewriting / Query Decomposition
把模糊的短查询扩写成完整问题,或者拆成多个子问题分别检索再合并。
2. Hybrid Search(混合检索)
单一检索方式都有盲区:
| 方式 | 擅长 | 失效场景 |
|---|---|---|
| 向量检索 | 语义相似,同义词 | 问句/答句结构不对称 |
| BM25 关键词 | 精确词匹配 | 语义相关但用词不同 |
解法是两路同时跑,用 RRF(Reciprocal Rank Fusion) 合并结果:
flowchart TD Q[用户问题] --> B[BM25 关键词检索] Q --> V[向量相似度检索] B --> |文档A排1, 文档C排2| RRF[RRF 合并] V --> |文档C排1, 文档D排2| RRF RRF --> |文档C两边都有,得分最高| TOP[Top-K 候选文档]
RRF 公式:score(d) = Σ 1 / (k + rank_i(d))
在两个列表都出现的文档,两项相加,分数最高。只在一个列表出现的分数较低。
工具推荐:pgvector 或 Elasticsearch 8.x 都原生支持 Hybrid Search,一个库搞定,不需要维护两套系统。
3. Chunking 策略(分块)
文档不能整篇塞进向量库,要切成小块(chunk)再 embedding。但切法有讲究:
- 切太小 → 上下文丢失,LLM 拿到片段用不好
- 切太大 → 噪音多,embedding 质量下降,检索也不准
Semantic Chunking:按语义边界切,而不是固定字数。章节、段落是天然的切分点。
Parent-Child Chunking(更优解):同时存两种粒度,各司其职:
flowchart TD subgraph 存储 P["Parent Chunk(大块,500-1000字)\n保留完整上下文"] P --> C1["Child Chunk 1(小块,100字)"] P --> C2["Child Chunk 2(小块,100字)"] P --> C3["Child Chunk 3(小块,100字)"] end subgraph 检索 Q[用户问题] --> VC[向量检索 Child Chunks\n小块精准匹配] VC --> |找到 Child Chunk 2| PP[返回对应 Parent Chunk\n大块喂给 LLM] end
小块负责”找得准”,大块负责”用得好”。
4. Reranking(重排序)
向量检索返回的 Top-20 文档,排第一的不一定是最能回答问题的。向量相似度衡量的是”语义接近”,不是”能回答这个问题”。
Cross-Encoder 解决这个问题:把问题和文档同时输入模型,让模型直接判断”这篇文档能不能回答这个问题”。
flowchart LR subgraph Embedding模型 Q1[query] --> EQ[向量] D1[doc] --> ED[向量] EQ --> DIST[算距离\n快,适合大规模召回] end subgraph Cross-Encoder QD["(query, doc) 拼在一起"] --> SCORE[直接输出相关性分数\n准,适合精排] end
为什么不一开始就用 Cross-Encoder:对百万级文档逐一跑太慢。所以先用向量检索快速缩小到 Top-20,再用 Reranker 精排——这是”粗排 + 精排”的经典两阶段架构。
优化优先级理解:
- Query Rewriting 影响的是召回率(能不能找到相关文档)
- Reranker 影响的是精确率(找到的文档排序对不对)
- 实际优化要先诊断瓶颈在哪一侧
5. Embedding 模型选择与微调
Embedding 模型的质量直接决定向量检索的上限。
选型参考:
| 场景 | 推荐模型 |
|---|---|
| 通用场景 + 预算有限 | text-embedding-3-small、BGE-M3 |
| 中文场景 | BGE 系列(BAAI 出品,中文效果强) |
| 多语言 | BGE-M3 或 multilingual-e5 |
| 领域术语密集 + 有标注数据 | 在通用模型基础上微调 |
什么时候值得微调:两个条件都满足才值得——领域术语密集(法律、金融、医疗),且有标注数据(如”相似文档对”)。
微调方式是 Contrastive Learning(对比学习):把相似文档对的向量拉近,把不相关文档的向量推远。
6. Metadata Filtering(元数据过滤)
纯向量检索处理不了结构化条件。用户问”最近一周看好新能源的研报”,“最近一周”和”新能源”这两个条件向量检索根本不理解。
解法是在向量检索之前先用 metadata 过滤,把候选集从百万砍到万级,后面的检索和重排都在小范围内进行:
flowchart LR Q["最近一周看好新能源的研报"] --> P[解析结构化条件] P --> |time: last_7_days\nsector: 新能源| MF[Metadata Filter\n先过滤] MF --> |候选集从100万 → 1万| HS[Hybrid Search] HS --> RR[Reranker] RR --> LLM
常用 metadata 字段:时间戳、行业分类、评级(买入/持有/卖出)、机构名称、股票代码。
7. RAGAS 评估体系
系统上线前需要量化质量,RAGAS 提供 4 个维度:
flowchart TD subgraph 检索侧 CP["Context Precision\n检索回来的文档,有多少是真正相关的\n→ 衡量精准度"] CR["Context Recall\n回答所需的信息,有多少被检索回来了\n→ 衡量覆盖度"] end subgraph 生成侧 F["Faithfulness\nLLM 的回答是否忠实于检索文档\n没有凭空捏造\n→ 最难提升"] AR["Answer Relevancy\n回答是否真正切题\n→ 用户最直接感受"] end
最难提升的是 Faithfulness:Context Precision 可以靠 Reranker 持续优化,但 LLM 幻觉是模型本身的行为问题,工程手段有限。
问题诊断链路:收到用户投诉时按这个顺序查:
flowchart TD BUG["用户反馈:回答答非所问"] --> AR[先查 Answer Relevancy\n回答跑题了吗?] AR --> |低| CP[再查 Context Precision\n检索回来的文档相关吗?] CP --> |低| FIX1[问题在检索侧\n优化 Hybrid Search / Reranker] CP --> |正常| F[查 Faithfulness\nLLM 有没有幻觉?] F --> |低| FIX2[问题在生成侧\n优化 prompt 或换模型]
大规模场景性能优化(十万级文档)
文档量上来之后,延迟瓶颈主要来自三个地方:向量检索本身、召回候选太多、Reranker 逐一打分。
优化手段(按性价比排序):
| 手段 | 效果 | 成本 |
|---|---|---|
| Metadata Filter 前置 | 候选集从百万砍到万级,后面全链路都快 | 极低,加个 where 条件 |
| 控制召回数量 | Top-20 进 Reranker,不要 Top-100 | 极低,改个参数 |
| ANN 索引(HNSW) | 向量检索本身 ms 级 | 低,向量库默认支持 |
| Query 缓存 | 相同问题直接返回,跳过整条链路 | 中,需要缓存层 |
| Reranker 缓存 | 高频 (query, doc) 打分结果缓存 | 中,需要缓存层 |
ANN(近似最近邻)原理:不对所有向量逐一算距离,而是先把向量空间分区,查询时只搜最相关的分区。牺牲一点精度,换来几个数量级的速度提升。pgvector 和 Pinecone 默认使用 HNSW 算法。
易错点备忘
- Hybrid Search ≠ 分层检索:不是”大目录用关键词、细节用向量”,而是同时跑两路检索再用 RRF 合并
- 优化优先级:召回率(Query Rewriting)和精确率(Reranker)是两个独立维度,先诊断瓶颈在哪再优化
- RAGAS 最难指标:是 Faithfulness(幻觉),不是 Context Precision(有工程手段可优化)
- 诊断顺序:答非所问先查 Answer Relevancy,不是 Faithfulness
下次复习重点
- Chunking 策略(Parent-Child 的实现细节)
- RAGAS 诊断链路(从症状到根因的定位流程)