Embedding 让文字变成了数字,但光有向量还不够------你需要一整套流程把「用户提问」变成「精准答案」。这就是 **RAG(检索增强生成)**,目前大模型落地最成熟、最实用的技术方案。没有 RAG,LLM 只是一个「能聊但不懂你业务」的聊天机器人;有了 RAG,它才能变成真正懂你的业务助手。
📑 目录
- [RAG(检索增强生成):给 LLM 外挂一个「知识库」](#RAG(检索增强生成):给 LLM 外挂一个「知识库」)
- [Chunk 分块:文档怎么切才合理](#Chunk 分块:文档怎么切才合理)
- [Rerank 重排:粗筛之后的精排](#Rerank 重排:粗筛之后的精排)
- [混合检索:语义 + 关键词双保险](#混合检索:语义 + 关键词双保险)
RAG(检索增强生成):给 LLM 外挂一个「知识库」
一句话定义
Retrieval-Augmented Generation = 先从知识库检索相关文档片段 → 把检索结果作为上下文 → 让 LLM 基于这些材料生成回答。让 LLM 不再只靠「训练时的记忆」,而是可以实时查阅「参考资料」。
本质大白话
没有 RAG 的 LLM:
用户:我们公司的报销政策是什么?
LLM:(瞎编一个通用回答)"通常公司报销需要发票..."
→ 因为 LLM 没看过你们公司的内部文件!
有 RAG 的 LLM:
用户:我们公司的报销政策是什么?
① 检索:在知识库里搜索「报销政策」→ 找到 3 篇相关文档片段
② 增强:把这些片段塞进 Prompt 当作参考
③ 生成:LLM 基于这些真实文档来回答
→ 回答有据可查!来源明确!
RAG vs 微调 vs 提示词工程
| 维度 | RAG | 微调 | Prompt 工程 |
|---|---|---|---|
| 知识时效性 | 实时更新 | 训练时固定 | 每次手动更新 |
| 数据安全 | 数据不出域 | 需要训练 | 安全但有限 |
| 成本 | 低 | 高 | 最低 |
| 幻觉问题 | 大幅减少 | 有所改善 | 无改善 |
| 适合场景 | 私有知识问答 | 特定风格/格式 | 通用任务引导 |
| 实现难度 | 中等 | 较高 | 低 |
python
# 最简 RAG 流程代码
def rag_pipeline(user_query: str) -> str:
# Step 1: 检索 (Retrieval)
relevant_docs = vector_store.search(
query=embed(user_query),
top_k=5
)
# Step 2: 构建 Prompt (Augmentation)
context = "\\n\\n".join([doc.text for doc in relevant_docs])
prompt = f"""基于以下资料回答问题。如果资料中没有相关信息,请说不知道。
参考资料:
{context}
问题:{user_query}
"""
# Step 3: 生成 (Generation)
answer = llm.generate(prompt)
return answer
# 一行调用
answer = rag_pipeline("公司年假怎么算?")
❌ 常见误区
- ❌ RAG 能完全消除幻觉 --- 不能,只是大幅降低。检索不到或检索错了仍然会出错
- ❌ RAG 只需要向量检索 --- 完整的 RAG 还需要分块策略、重排序等配套(本节后面详细讲)
- ❌ 所有场景都适合 RAG --- 创意写作、翻译、闲聊等不需要 RAG,纯知识问答才是主战场
Chunk 分块:文档怎么切才合理
一句话定义
将长文档拆分成适合检索和喂给 LLM 的文本小块。切得好,检索准;切不好,要么信息断裂要么噪音太多。
为什么需要分块?
不分块的问题:
┌──────────────────────────────┐
│ 整份 PDF(100页/5万字) │
│ │
│ 问题: │
│ 1. 超出 Embedding 长度限制 │
│ 2. 检索时返回大量无关内容 │
│ 3. 浪费 Context Window 空间 │
│ 4. LLM 无法聚焦关键信息 │
└──────────────────────────────┘
合理分块后:
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
│Chunk1│ │Chunk2│ │Chunk3│ │Chunk4│ ...
│~500字│ │~500字│ │~500字│ │~500字│
└──────┘ └──────┘ └──────┘ └──────┘
每个 chunk 可以被独立检索和引用
常用分块策略对比
| 策略 | 做法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 固定长度 | 每 N 个字符/Token 切一刀 | 简单快速 | 可能切断语义 | 通用 |
| 递归字符分割 | 按段落 → 句子 → 字符递归切 | 保持语义完整 | 实现稍复杂 | 文档类 |
| 语义分割 | 用 Embedding 判断语义边界再切 | 最精确 | 慢,需要额外模型 | 高精度需求 |
| 按文档结构 | Markdown 标题/章节为界切 | 结构清晰 | 依赖文档格式好 | 技术文档 |
python
# 推荐的分块策略实现(LangChain 风格)
from langchain_text_splitters import RecursiveCharacterTextSplitter
# 策略1:递归字符分割(推荐起步选择)
splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # 每个 chunk 最大 500 字符
chunk_overlap=50, # 相邻 chunk 重叠 50 字符(防丢失上下文)
separators=["\\n\\n", "\\n", ".", " ", ""] # 优先在段落处切
)
chunks = splitter.split_documents(documents)
print(f"共分成 {len(chunks)} 个 chunk")
# 策略2:按 Markdown 标题分块(适合技术文档)
from langchain_text_splitters import MarkdownHeaderTextSplitter
md_splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=[("#", "h1"), ("##", "h2")]
)
md_chunks = md_splitter.split_text(markdown_document)
Chunk 参数选择的经验法则
Chunk Size 选择:
- 256-512 tokens → 适合短问答(FAQ、定义查询)
- 512-1024 tokens → 适合一般文档检索(最常用)
- 1024-2048 tokens → 适合长文摘要、分析类任务
Overlap 选择:
- 一般是 chunk_size 的 10%-20%
- 太小 → 上下文在边界处丢失
- 太大 → 冗余存储和计算
❌ 常见误区
- ❌ Chunk 越小越好 --- 太小的 chunk 丢失上下文,语义不完整
- ❌ Chunk 越大越好 --- 太大的 chunk 包含太多噪音,干扰检索精度
- ❌ 所有文档用同一种分块策略 --- 不同类型文档需要不同策略
Rerank 重排:粗筛之后的精排
一句话定义
对向量检索返回的 Top-K 结果进行二次精排,用更精细的模型重新打分排序。目的是让最相关的结果排在前面。
为什么需要 Rerank?
向量检索(粗筛):
Query: 「如何提升 API 响应速度?」
Top-5 检索结果:
#1 [0.89] 「Redis 缓存加速响应...」 ← 相关度中等
#2 [0.87] 「数据库索引优化指南...」 ← 相关度高!应该排第1
#3 [0.85] 「API 设计最佳实践...」 ← 相关度低(讲设计不讲性能)
#4 [0.84] 「CDN 加速静态资源...」 ← 有点相关
#5 [0.82] 「微服务架构入门...」 ← 不太相关
问题:向量相似度 ≠ 真实相关性
「缓存」和「速度」语义近,但不一定是最精准的答案
Rerank 后(精排):
#1 [0.95] 「数据库索引优化指南...」 ← 真正最相关的排到第一了
#2 [0.91] 「Redis 缓存加速响应...」 ← 也很相关
#3 [0.78] 「CDN 加速静态资源...」 ← 还行
#4 [0.65] 「API 设计最佳实践...」 → 降下去了
#5 [0.42] 「微服务架构入门...」 → 明显不相关,压到最后
主流 Reranker 对比
| 模型 | 类型 | 语言 | 效果 | 成本 |
|---|---|---|---|---|
| bge-reranker-v2 | 开源 | 多语 | ⭐⭐⭐⭐ | 免费本地运行 |
| Cohere Rerank | API | 多语 | ⭐⭐⭐⭐⭐ | 付费但效果好 |
| jina-reranker | 开源/API | 多语 | ⭐⭐⭐⭐ | 灵活 |
| cross-encoder | 开源 | 英/中 | ⭐⭐⭐ | 轻量可用 |
python
# Rerank 完整示例
def reranked_search(query, vector_store, top_k=20, rerank_top=5):
# Step 1: 向量检索(召回更多候选)
candidates = vector_store.search(embed(query), top_k=top_k)
# Step 2: Rerank 重排(用 CrossEncoder 精排)
from sentence_transformers import CrossEncoder
reranker = CrossEncoder("BAAI/bge-reranker-v2-m3")
pairs = [(query, doc["text"]) for doc in candidates]
scores = reranker.predict(pairs) # 更精准的相关性分数
# Step 3: 取重排后的 Top-K
ranked_indices = sorted(range(len(scores)), key=lambda i: scores[i], reverse=True)
return [candidates[i] for i in ranked_indices[:rerank_top]]
# 使用:先召回 20 条,重排后取最优 5 条
final_results = reranked_search("怎么优化数据库?", my_vector_store)
❌ 常见误区
- ❌ Rerank 会拖慢整个系统 --- Rerank 只处理 Top-20 左右的候选集,延迟增加很小(通常 <100ms)
- ❌ 有了 Rerank 就不需要好的检索 --- Rerank 是锦上添花,基础检索太差的话 Rerank 也救不了
- ❌ Reranker 模型越大越好 --- bge-reranker 在多数场景已经足够优秀
混合检索:语义 + 关键词双保险
一句话定义
同时使用向量检索(语义匹配) 和 **关键词检索(BM25 精确匹配)**,然后合并去重排序。两种方法互补,效果优于单一方法。
为什么单一方法不够?
向量检索的盲区:
Q: 「iPhone 15 Pro Max 的 A17 Pro 芯片主频是多少?」
向量检索可能找到:「手机芯片发展史...」「苹果产品线介绍...」
→ 语义相关但缺少精确的关键词匹配
关键词检索的盲区:
Q: "怎么让数据库查询更快?"
BM25 可能找到包含「快」「数据库」「查询」的文章
→ 但找不到「加速」「优化」「性能调优」等同义词文章
混合检索 = 两者的优点合并:
✅ 语义理解 + 精确匹配
✅ 同义词扩展 + 专有名词捕获
混合检索架构
用户 Query
│
┌────────────┼────────────┐
↓ ↓ ↓
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Embedding│ │ BM25 │ │ (可选) │
│ 向量检索 │ │ 关键词检索│ │ 其他通道 │
│ Top-K:20 │ │ Top-K:20 │ │ │
└─────┬────┘ └─────┬────┘ └─────┬────┘
│ │ │
└──────┬───────┘ │
↓ Reciprocal Rank Fusion (RRF)
┌──────────────────┐
│ 合并去重排序 │
└────────┬─────────┘
↓
┌──────────────┐
│ Rerank 重排 │
│ 最终 Top-5/10 │
└──────────────┘
python
# 混合检索实现(RRF 算法)
def hybrid_search(query, vector_store, bm25_index, k=10, alpha=0.6):
"""
alpha: 向量检索权重,(1-alpha): 关键词检索权重
"""
# 1. 向量检索
vec_results = vector_store.search(embed(query), top_k=k*2)
# 2. BM25 关键词检索
kw_results = bm25_index.search(query, top_k=k*2)
# 3. Reciprocal Rank Fusion 合并
scores = {}
for rank, doc in enumerate(vec_results):
doc_id = doc["id"]
scores[doc_id] = scores.get(doc_id, 0) + alpha / (rank + 60)
for rank, doc in enumerate(kw_results):
doc_id = doc["id"]
scores[doc_id] = scores.get(doc_id, 0) + (1-alpha) / (rank + 60)
# 4. 取最终 Top-K
final = sorted(scores.items(), key=lambda x: x[1], reverse=True)[:k]
return final
❌ 常见误区
- ❌ 混合检索一定比单一检索好 --- 如果你的数据都是自然语言描述且无专有名词,纯向量检索可能就够了
- ❌ RRF 权重不用调 --- 不同场景下向量和关键词的最佳比例不同,需要根据实际效果调整
- ❌ 混合检索太复杂不能上线 --- LangChain/LlamaIndex 都有一行代码的实现
📊 本节知识地图
┌────────────────────────────────────────────────────────┐
│ RAG 体系 --- 完整检索增强流水线 │
│ │
│ 文档输入 │
│ ↓ │
│ ┌──────────┐ │
│ │ Chunk │ ← 分块(大小+重叠是关键参数) │
│ │ 分块策略 │ │
│ └────┬─────┘ │
│ ↓ Embedding │
│ ┌───────────────────────────────────────┐ │
│ │ 检索阶段 │ │
│ │ ┌─────────┐ ┌─────────┐ ┌───────┐ │ │
│ ️ │ *向量 │ │ *关键词 │ │ Rerank│ │ │
│ │ 检索★ │ │ BM25 ☆ │ │ 重排 │ │ │
│ │ (语义) │ │ (精确) │ │ (精排)│ │ │
│ │ Top-K:20│ │ Top-K:20│ │ Top-5 │ │ │
│ └────┬─────┘ └────┬─────┘ └───┬───┘ │ │
│ └──────────┬──┘ │ │ │
│ ↓ RRF 合并 ↓ │ │
│ ┌──────────────────────────────┘ │ │
│ ↓ ↓ │
│ ┌──────────┐ 最终结果 │
│ │ LLM 生成 │ │
│ │ (基于检索)│ │
│ └──────────┘ │
│ │
│ 核心公式:文档→分块→Embedding→多路召回→Rerank→LLM生成答案 │
└────────────────────────────────────────────────────────┘
🔗 关联推荐
- 📖 2.1 向量基础 → Embedding 和相似度是 RAG 的前置知识
- 📖 1.3 交互基础 → 检索结果作为 Context 喂给 LLM
- 🔜 2.3 向量数据库 → 向量和文档存在哪里?怎么选型?
- 🔜 8.2 RAG 优化 → 进阶:HyDE、Fusion、多路召回