---
title: RAG 与检索增强
url: https://doc.liz6.com/ai/03-applications-and-production/01-rag-and-retrieval-augmented-generation
locale: zh
area: ai
tags:
- ai
- 应用与生产
date: 2026-06-30
modified: 2026-07-16
description: 别把整库塞进窗口赌模型自己找。RAG 把"无限知识"拆成"先检索、再把命中的几页放进上下文"——决定答案质量的不是上下文有多长,而是检索有多准。
---

# RAG 与检索增强

> 别把整库塞进窗口赌模型自己找。RAG 把"无限知识"拆成"先检索、再把命中的几页放进上下文"——决定答案质量的不是上下文有多长,而是检索有多准。

## 概述

[上下文工程](/ai/01-models-and-context/01-context-engineering.md) 反复强调一条:**检索优于硬塞**。这篇把那句引子展开成可落地的工程——**RAG(retrieval-augmented generation,检索增强生成)**。

模型的参数里压着训练时见过的知识,但它**不知道**私有文档、今天的数据、本项目的约定;而且窗口有限、注意力对中段"看不清"([lost in the middle](/ai/01-models-and-context/01-context-engineering.md))。两条死路:把整个知识库塞进 system(撑爆窗口、稀释信号、贵),或指望微调把知识焊进权重(慢、贵、更新难)。RAG 是第三条路:**把知识留在窗口外的检索库里,每次只把与当前问题相关的几个片段取出来,拼进上下文**。

一句话抓住本质:**RAG 把"模型知道多少"问题,转化成"检索系统能不能把对的片段排到前面"问题。** 于是答案质量的瓶颈从模型搬到了检索——这也是为什么这一篇的大部分篇幅在讲怎么检索得准,而不是怎么生成。

## 核心回路:retrieve → augment → generate

```mermaid
flowchart LR
    Q["用户问题"] --> EMB["query 向量化"]
    EMB --> SEARCH["向量库检索<br/>top-k 相似片段"]
    DOCS["知识库<br/>(切块 + 预先 embed)"] -.离线建索引.-> SEARCH
    SEARCH --> RERANK["rerank<br/>(可选, 精排)"]
    RERANK --> AUG["拼进上下文<br/>命中片段 + 问题"]
    AUG --> LLM["模型生成<br/>(带引用)"]
    LLM --> A["答案"]
```

分两段:**离线建索引**(把文档切块、向量化、灌进向量库,一次性)和**在线检索生成**(每次请求把问题向量化、检索、拼接、生成)。后者直接接上 [Agent 循环](/ai/02-Agent/01-the-agent-loop-and-tool-use.md)——检索可以是一个工具,模型自己决定何时查、查什么。

## embedding 与向量检索:为什么能"按意思"找

关键词检索按字面匹配,问"怎么让显卡省显存"找不到写着"KV cache 量化"的段落。**embedding** 把一段文本映射成一个高维向量,语义相近的文本向量也相近,于是检索变成"在向量空间里找最近邻"——按**意思**找,不按字面。

- **相似度**:常用余弦相似度(cos similarity)。把库里每个片段预先 embed 存好,查询时只 embed 一次 query,算它与库向量的相似度,取 top-k。
- **本机可跑**:embedding 不必上云。llama.cpp 能直接跑 embedding 模型(如 Qwen3-Embedding、bge-m3),配一个本地向量库(sqlite-vec、Qdrant、Chroma)就是一套离线 RAG——参见 [本地 LLM 部署](/homelab/local-llm.md)。embedding 模型小、batch 跑得快,显存压力远小于生成模型。
- **维度与规模**:几千到几万个片段用暴力近邻就够;上百万再上 ANN 索引(HNSW、IVF)换近似检索的速度。

> embedding 模型 ≠ 生成模型,二者**各自分词、互不通用**。给检索库 embed 用的模型,和查询时 embed 用的,**必须是同一个**——换了模型整库要重新 embed,否则向量不在同一空间,检索全乱。

## chunk 策略:切不好,检索再准也没用

文档得先切成**块(chunk)**才能 embed。切块是 RAG 里最被低估、却最影响效果的一步:

| 维度 | 取舍 |
|------|------|
| 块**大小** | 太大→一块混多个主题,向量被稀释,命中也夹带噪声;太小→语义不完整,"它"指代谁丢了。常见 200–500 token。 |
| 块**重叠** | 相邻块留 10–20% 重叠,避免把一句话/一个论点从中间切断。 |
| **切分点** | 按语义边界切(标题、段落、Markdown 章节)远胜按固定字数硬切。代码按函数/类切,别按行。 |
| **附元数据** | 每块带上来源、标题、章节路径——既能过滤(只检索某文档),又能在答案里给引用。 |

经验:**先按文档的天然结构切(本站点就是按 `##` 章节),再在过大的块内按字数兜底。** 切块质量比向量库选型重要得多。

## 提升命中:混合检索与 rerank

纯向量检索有盲区:精确的专有名词、错误码、ID,字面匹配反而更可靠。两个加准的手段:

- **混合检索(hybrid)**:向量检索(管语义)+ 关键词检索 BM25(管字面),两路结果融合(如 RRF, reciprocal rank fusion)。问"`model_context_window_exceeded` 是什么"时,BM25 能精确命中那个串,向量未必。
- **rerank(精排)**:检索先粗召回 top-50,再用一个 **cross-encoder reranker** 对 query-片段逐对精打分,取真正最相关的 top-5 拼进窗口。召回管"别漏",精排管"别滥"——两段式比单纯调大 k 更省窗口、更准。

**别用拉大 k 来兜底。** k 越大,拼进窗口的噪声越多,既触发 [context rot](/ai/01-models-and-context/01-context-engineering.md) 又费 token。宁可召回宽、精排狠,最后只放少而精的几页。

## 拼进窗口:接回上下文工程

检索出片段后怎么放进上下文,直接复用 [上下文工程](/ai/01-models-and-context/01-context-engineering.md) 的全部结论:

- **位置**:把命中片段放在注意力强的位置(靠前的 system 之后、或紧贴问题之前),用清晰分隔符 / XML 标签框住,别埋在中段。
- **缓存**:稳定的指令、不变的少量参考放前缀(命中 [prompt caching](/ai/01-models-and-context/01-context-engineering.md));**每次变化的检索片段放靠后**,别插在前缀里破坏缓存。
- **要引用**:让模型在答案里标注每条结论来自哪个片段(靠 chunk 元数据)。Claude 的 [citations](/ai/01-models-and-context/01-context-engineering.md) 能力可让被引用片段携带定位信息,但注意它与结构化输出 `output_config.format` 互斥,二选一。

## 评估检索:把"准不准"量化

RAG 的失败往往不在生成,而在检索——**模型答错,常常是因为对的片段根本没被检索到**。所以要分开评估两段(接 [评估与可观测](/ai/02-Agent/05-evaluation-and-observability.md)):

- **检索质量(可编程,最可靠)**:对一组带"标准答案出处"的问题,量 **recall@k**(对的片段有没有进 top-k)、**precision@k**(top-k 里有多少真相关)、MRR。这步纯靠代码断言,零成本、无偏差。
- **生成质量(开放式)**:答案是否忠于检索内容、有没有编造(hallucination)、引用对不对——用 [LLM-as-judge](/ai/02-Agent/05-evaluation-and-observability.md),裁判与被测分离。

定位问题先看检索:recall 低就回去调 chunk / 换 embedding / 加 rerank;recall 高但答错,才是生成或 prompt 的问题。

## RAG vs 记忆 vs 长上下文:别混

三个常被搞混的"让模型多知道点"的手段,边界要清(详见 [记忆与状态](/ai/02-Agent/03-memory-and-state.md)):

| | 解决什么 | 存在哪 | 生命周期 |
|---|---|---|---|
| **长上下文** | 这一轮要看的全部资料 | 窗口内 | 一轮即逝 |
| **RAG** | 海量知识里**按需取相关片段** | 窗口外的检索库 | 库持久,每轮取一小片进窗口 |
| **记忆** | 跨会话记住**偏好/结论/踩过的坑** | 会话外的文件/库 | 跨会话持久 |

RAG 取的是"客观知识片段",记忆存的是"主观经验结论";RAG 每轮重新检索,记忆是模型主动写读。长 agent 三者常并用。

## 最佳实践

- **先量检索,再调生成。** 建一个带"出处标准答案"的小评估集,先把 recall@k 顶上去——检索不准,换什么模型、调什么 prompt 都是隔靴搔痒。
- **按语义结构切块,带足元数据。** 标题/段落/函数边界优先于固定字数;每块附来源+章节,既能过滤又能给引用。
- **召回宽、精排狠、上下文少。** 粗召回 top-50 → rerank → 只放 top-3~5,别靠拉大 k 硬塞。
- **query 与库用同一个 embedding 模型。** 换模型必须整库重 embed;升级 embedding 走灰度,别原地替换。
- **专名/错误码/ID 走混合检索。** 纯向量对字面精确项有盲区,叠一路 BM25。
- **检索片段一律放窗口靠后。** 保护前缀缓存;稳定指令在前,易变命中在后。
- **让答案带引用、可溯源。** 用 chunk 元数据标注出处,既降幻觉又便于人工核查。

## 权衡与失败模式

- **把整库塞进 system**:撑爆窗口、稀释信号、贵 → 检索后只放命中片段。
- **chunk 太大/太碎**:大块向量被稀释、碎块语义断裂 → 按语义边界切 + 适度重叠,实测 recall 调。
- **query 与库 embedding 不一致**:向量不同空间,检索全乱 → 锁定同一 embedding 模型,换模型重建索引。
- **纯向量漏掉精确项**:错误码/专名字面匹配失败 → 混合检索叠 BM25。
- **拉大 k 兜底**:噪声进窗口、触发 context rot、费 token → 召回宽 + rerank 精排 + 少放。
- **只看生成不看检索**:答错以为是模型的锅,其实片段没召回 → 先量 recall@k 再归因。
- **检索片段插进缓存前缀**:每轮命中不同,前缀缓存全废 → 命中放靠后,前缀保持稳定。

## 参考

- **研究**: "Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks"(Lewis et al., 2020)、"Lost in the Middle"(Liu et al., 2023)
- **Anthropic 官方文档**: Citations、Structured Outputs(platform.claude.com,实现前以官方为准)
- **本地实战**: [本地 LLM 部署](/homelab/local-llm.md)(本机跑 embedding 与向量库)
- **上游/下游**: [上下文工程](/ai/01-models-and-context/01-context-engineering.md)、[记忆与状态](/ai/02-Agent/03-memory-and-state.md)、[评估与可观测](/ai/02-Agent/05-evaluation-and-observability.md)

*Keywords: RAG, retrieval-augmented generation, embedding, 向量检索, cosine similarity, 近邻, ANN, HNSW, chunk, chunking, overlap, 切块, BM25, hybrid search, 混合检索, RRF, rerank, cross-encoder, recall@k, precision@k, MRR, citations, hallucination, 检索优于硬塞, vector database, sqlite-vec, Qdrant*
