RAG 是什么

RAG 是什么

用一个真实项目,把「检索增强生成」讲清楚。


一、为什么需要 RAG

大语言模型(LLM)有一个根本性的局限:它只知道训练截止日期之前的公开信息,对你的私有文档一无所知。

你的笔记、公司的内部文档、个人收藏的资料——这些东西 LLM 统统不知道。如果你把整份文档塞进对话窗口,很快就会超出上下文长度限制,而且成本也极高。

RAG(Retrieval-Augmented Generation,检索增强生成)就是为了解决这个问题而生的:

先检索,后生成。 不是把所有文档喂给 LLM,而是先找到最相关的片段,只把这几段内容作为上下文,让 LLM 在此基础上回答。

这个思路听起来简单,但要做好,里面有很多细节。下面我用自己写的一个真实项目——面向本地 Markdown 笔记的 RAG 问答系统——把每个环节都拆开讲清楚。


二、RAG 的两个阶段

RAG 系统的工作分为两个完全独立的阶段:

1
2
【离线阶段:索引】                    【在线阶段:查询】
文档 → 分块 → 向量化 → 存入向量库 问题 → 向量化 → 检索 → 重排 → LLM 生成 → 带引用的回答

两个阶段共用同一套向量库,但时机不同:索引可以在后台静默运行、增量更新;查询则是实时响应用户提问。


三、离线阶段:把文档变成可检索的向量

1. 文件扫描与增量检测

每次索引不能都全量重跑,否则代价太大。这个系统用 SHA-256 + SQLite 实现增量检测:

1
2
3
4
文件路径 → SHA-256 哈希 → 与上次记录比较
├─ 哈希相同 → 跳过
├─ 哈希不同 → 标记为 modified,重新索引
└─ 路径消失 → 标记为 deleted,从向量库删除对应向量

SQLite 里维护了一张表,记录每个文件的路径、doc_id、sha256 和修改时间。这样文件一旦没有实质变化,哈希不变,就直接跳过,只处理真正有变化的文件。

2. 结构化分块(Chunking)

把整篇文档直接向量化是不行的,因为:

  • 文档太长,向量会”平均化”,失去细节
  • 检索时需要精确定位到具体段落,而不是整篇文章

分块策略采用两级切分

第一级:按 Markdown 标题切分

1
2
3
# 主标题        ← h1
## 子章节 ← h2
### 小节 ← h3

每个标题下的内容作为一个 section,同时保留标题层级信息(headings)。这样每个 chunk 都知道自己属于哪个章节。

第二级:对超长 section 做递归字符切分

设定 chunk_size=700chunk_overlap=100。超过 700 字的 section 会被进一步切分,相邻 chunk 保留 100 字的重叠,避免语义在边界断裂。

分隔符优先级:\n\n > \n > 中文句号 > 英文句号 > 空格。

每个最终的 chunk 携带这些元数据:

  • doc_id:文档的唯一 ID(由文件路径 sha256 派生,稳定不变)
  • chunk_iddoc_id#序号
  • path:文件相对路径
  • title:文档标题
  • headings:所在的标题层级路径,如 ["RAG系统", "检索", "重排"]
  • tags:从 YAML Front Matter 读取的标签

3. 向量化(Embedding)

每个 chunk 的文本内容被送入 Embedding 模型,转换为一个高维向量(本项目用 Ollama 本地运行 nomic-embed-text,维度 768)。

向量的本质:把语义相近的文本映射到空间中相近的位置。 “苹果手机” 和 “iPhone” 在向量空间里距离很近,即使字面完全不同。

4. 存入向量库(Qdrant)

向量和对应的 payload(元数据)一起存入 Qdrant。Qdrant 使用 HNSW(Hierarchical Navigable Small World)索引结构,能在毫秒级完成近似最近邻搜索。

为了加速过滤,对 doc_idtagsmtime 字段建立了 Payload 索引,支持按标签或时间范围预过滤后再做向量搜索。


四、在线阶段:从提问到带引用的回答

1. 问题向量化

用户的问题同样用相同的 Embedding 模型转换为向量。必须用同一个模型,否则问题向量和文档向量不在同一个语义空间里,检索会乱套。

2. 向量检索(Retrieve)

拿着问题向量去 Qdrant 做余弦相似度搜索,召回 top_k=50 个最相似的 chunk。

余弦相似度衡量的是方向的接近程度,而不是向量的长度。两个向量夹角越小,cosine 值越接近 1,语义越相近。

3. 重排(Rerank)

向量检索召回的 50 个结果里,有些虽然向量相似度高,但实际上并不精确匹配用户的问题。重排做二次筛选。

当前实现是一个轻量的关键词重排:统计问题中每个词在 chunk 内容里出现的次数,得分越高排越靠前,最终只保留 top_n=5 个。

这是一个 MVP 实现。生产系统通常会接入真实的 Cross-Encoder 重排模型(如 bge-reranker-v2-m3),效果显著更好,但速度也更慢、成本更高。

4. LLM 生成

把 5 个 chunk 的内容拼装成上下文,连同用户问题一起送给 LLM:

1
2
3
4
5
6
7
8
9
10
11
12
你是一个Markdown笔记问答助手。请严格依据提供的上下文回答。
如果上下文不足,请明确说不知道。
回答后必须给出引用,格式为[path#heading]。

问题: {query}

上下文:
[notes/rag.md#检索]
检索阶段负责从向量库中...

[notes/llm.md#提示词工程]
提示词的质量直接影响...

LLM 被明确要求:只能依据提供的上下文回答,不能乱编;如果上下文不够,必须说不知道。 这个约束极为重要,是 RAG 减少幻觉的核心机制。

5. 返回带引用的回答

最终输出是结构化的 JSON:

1
2
3
4
5
6
7
8
9
10
{
"answer": "RAG 系统的检索阶段分为向量检索和重排两步...",
"citations": [
{
"path": "notes/rag.md",
"heading": "检索",
"snippet": "检索阶段负责从向量库中找到..."
}
]
}

每条引用精确到 文件路径#标题 级别,用户可以直接追溯原始笔记,验证回答是否准确。


五、完整链路回顾

1
2
3
4
5
6
7
8
9
10
11
12
13
14
【索引时】
Markdown 文件
↓ SHA-256 增量检测(SQLite)
↓ 按标题分块 + 递归字符切分
↓ Embedding(nomic-embed-text / 768维)
↓ 存入 Qdrant(HNSW 索引 + Payload 索引)

【查询时】
用户提问
↓ Embedding(同一模型)
↓ 向量检索 top_k=50(余弦相似度)
↓ 关键词重排 → top_n=5
↓ 拼装 Prompt → LLM(MiniMax / Claude)
↓ 结构化输出:回答 + path#heading 引用

六、RAG 的边界与局限

RAG 不是万能药,有几个天然的局限需要清醒认识:

1. 检索质量决定生成质量
“Garbage in, garbage out”。如果检索阶段没有找到相关内容,LLM 无论多聪明也无法给出好答案。Embedding 模型的质量、分块策略、召回数量都直接影响最终效果。

2. 跨文档推理很难
“综合我所有笔记,总结出一个结论” 这类需要跨多文档全局推理的问题,RAG 效果有限。它更擅长的是”找到相关段落,在此基础上回答”。

3. 实体关系理解弱
向量相似度捕捉的是语义相近,但对于”A 和 B 是什么关系”这类结构性问题,Graph RAG(知识图谱 + 向量混合)会更合适。

4. 分块粒度难以完美
块太大,向量被稀释,精度下降;块太小,单块缺少上下文,生成时 LLM 信息不足。需要针对具体场景调参,没有通用答案。


七、可以继续扩展的方向

这个项目是 MVP,后续有几个方向可以显著提升效果:

  • 真实 Reranker:接入 bge-reranker-v2-m3 等 Cross-Encoder 模型,比关键词匹配准确得多
  • Multi-Query 检索:让 LLM 把原始问题改写为多个角度的子问题,分别检索后合并,提高召回率
  • Hybrid Search:向量检索(dense)+ BM25 关键词检索(sparse)结合,互补彼此的盲区
  • Parent Document Retriever:存储细粒度 chunk 用于检索,但把父级大块文本喂给 LLM,兼顾精度与上下文完整性
  • 知识图谱(Graph RAG):提取实体和关系,支持跨文档的结构性推理

小结

RAG 的核心思想只有一句话:不要把所有东西塞给 LLM,而是先找到最相关的内容,只把这些内容作为上下文。

围绕这句话,展开了一套工程体系:增量索引、结构化分块、向量化、近似最近邻搜索、重排、Prompt 约束、引用溯源。每个环节都有工程取舍,每个环节的质量都影响最终体验。

RAG 本质上是一个信息检索问题 + 生成问题的结合体。做好它,需要同时理解这两个领域——这也是它有趣的地方。


本文基于作者实际开发的 rag_notes 项目,技术栈:LangChain + Qdrant + Ollama + MiniMax。