第 8 章 RAG 工程
本章要解决的问题
知识库问答总是答非所问、检索不到、甚至编造------RAG 到底哪里出了问题?
章节大纲
- 8.1 RAG 全流程:解析 → 分块 → 向量化 → 检索 → 重排 → 生成
- 8.2 分块策略与元数据设计
- 8.3 混合检索:向量 + 关键词 + 重排
- 8.4 RAG 评测:命中率、忠实度、幻觉率
- 🛠 解决方案:RAG 效果差的 10 大原因定位与修复(查不到/答不对/幻觉)
8.1 RAG 全流程:让模型"带着资料回答问题"
8.1.1 什么是 RAG:模型不知道的,检索来补
RAG(Retrieval-Augmented Generation,检索增强生成)= 先把相关资料检索出来,再把资料连同问题一起交给模型回答。
它解决的是 LLM 的两个天生缺陷:
- 知识边界:模型不知道私有数据、指定资料、需要可溯源的内容。RAG 让模型"带着资料回答",而不是凭训练记忆猜测。
- 幻觉倾向:不知道就编(呼应第 13 章"知识型错误反思修不了"------RAG 才是正解)。
第 1 章的 6 概念关系里说过:RAG 是 Agent 的"知识配件"。本章讲透这个配件的完整工程。
8.1.2 全流程六步
css
[离线阶段:建索引]
① 解析(Parsing) PDF/Word/网页 → 纯文本
② 分块(Chunking) 长文本 → 语义完整的块
③ 向量化(Embedding) 文本块 → 向量(语义数字表示)
④ 存储(Indexing) 向量 + 原文 + 元数据 → 向量数据库
│
[在线阶段:问答]
⑤ 检索(Retrieval) 问题向量化 → 找最相似的 Top-K 块
⑥ 生成(Generation) 资料 + 问题 → 模型生成答案

图 1:RAG 六步全流程
两个阶段的思维要分开:离线阶段(①②③④)决定"索引质量",在线阶段(⑤⑥)决定"检索准确度",生成(⑥)决定"能否基于资料组织答案"------但答案质量由"检索召回 + 上下文组织 + 生成约束"共同决定。RAG 出问题,先定位是哪个环节(8.4 会讲怎么定位)。
8.1.3 最小 RAG 实现
python
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.deepseek.com", api_key="<你的Key>")
# 演示用向量库(生产用 Chroma/FAISS/Qdrant 等)
from sklearn.feature_extraction.text import TfidfVectorizer # 简化为 TF-IDF 演示
from sklearn.metrics.pairwise import cosine_similarity
class MiniRAG:
def __init__(self):
self.chunks = []
self.vectorizer = TfidfVectorizer()
self._built = False
# ① ② 解析+分块(演示:按段落分块)
def index(self, documents):
for doc in documents:
# 真实场景:按章节/段落/固定长度切块(8.2 详述)
# 演示用:按双换行分段,过滤空段
chunks = [c.strip() for c in doc.split("\n\n") if c.strip()]
self.chunks.extend(chunks)
# ③ ④ 向量化+存储
def build(self):
self.matrix = self.vectorizer.fit_transform(self.chunks)
self._built = True
# ⑤ 检索
def retrieve(self, query, top_k=3):
if not self._built:
raise RuntimeError("请先调用 build() 构建索引,再检索")
q_vec = self.vectorizer.transform([query])
scores = cosine_similarity(q_vec, self.matrix)[0]
top_idx = scores.argsort()[-top_k:][::-1]
return [self.chunks[i] for i in top_idx]
# ⑥ 生成
def answer(self, question):
if not self._built:
raise RuntimeError("请先调用 index() + build(),再问答")
context = "\n\n".join(self.retrieve(question))
resp = client.chat.completions.create(
model="deepseek-chat",
messages=[{"role": "user", "content":
f"""请仅基于以下资料回答问题。资料中没有的信息,请明确说"资料中未提及"。
资料:
{context}
问题:{question}"""}],
temperature=0.3, # 问答任务温度(第24章表二 0.3~0.5)
)
return resp.choices[0].message.content
注意生成提示词的两个关键约束:①"仅基于资料回答"(防幻觉);②"没有就说没有"(防编造)------这两句是 RAG 提示词的标配。
生产加固 :
answer()中的 LLM 调用应加timeout和重试逻辑;question输入应做长度限制和注入检测(用户可能在问题中注入"忽略以上资料"类指令,呼应第 22 章);检索结果为空时应返回"未找到相关资料"而非让模型自由发挥。
8.2 分块策略与元数据设计
8.2.1 分块:RAG 质量的"地基"
分块是整个 RAG 里最影响质量、最容易被忽视的环节。 块太小→语义不完整;块太大→检索噪音多、向量不聚焦。

图 2:分块策略对比
| 分块方式 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| 固定长度 | 按 N 字符/Token 切 | 简单 | 切断语义(最差) |
| 递归分隔 | 按段落/句子/标点逐级切 | 语义较完整 | 块大小不均 |
| 语义分块 | 按语义边界(换段/标题)切 | 语义最完整 | 需解析结构 |
推荐 :递归分隔(按 \n\n → \n → 句号 逐级切,设定目标块大小如 500~1000 token),这是工程上的甜点方案。
文档结构感知:分块前应先解析文档结构------标题层级、表格、代码块、FAQ 等应优先保持完整,不要被固定长度切断。PDF/Word 的版面分析、Markdown 的标题层级、HTML 的标签结构都是天然的分块边界。
8.2.2 块大小的选择
块越小:检索更精准,但上下文不完整 → 答案缺上下文
块越大:上下文完整,但检索噪音多 → 答非所问
经验法则:
- 问答型文档:300~800 token/块(兼顾完整与精准)
- 长文档分析:500~1000 token/块(保上下文)
- 代码/结构化:按函数/类切块
一个实用技巧:重叠(overlap)------相邻块重叠 10~15%,防止关键信息正好被切在边界上。
8.2.3 元数据设计:让检索"可过滤"
元数据(Metadata)= 每个块附带的属性,用于检索时的过滤(不是相似度,是硬条件):
python
chunk = {
"text": "报销标准:差旅费每人每天上限 500 元",
"metadata": {
"doc_id": "doc-001",
"title": "差旅管理制度",
"category": "制度", # 按类别过滤
"department": "行政部", # 按部门过滤
"version": "2026-07", # 按版本过滤
"updated_at": "2026-07-01",
},
}
元数据过滤的价值 :用户问"报销标准"时,先按 category=制度 过滤再检索------比纯向量相似度精准得多。元数据设计 = 给检索装"筛子"(呼应第 24 章 A5 租户隔离:多租户知识库必须按 tenant_id 过滤)。
8.3 混合检索:向量 + 关键词 + 重排
8.3.1 为什么向量检索不够
向量检索(语义相似)有盲区:
- 专有名词:"GPT-4o-mini" 的向量可能匹配不到,但关键词能精确命中。
- 编号/型号:"A123456789" 这种 ID,语义检索基本失效。
- 缩写:"MCP" 的语义向量可能漂移。
关键词检索(BM25)擅长精确匹配,向量检索擅长语义匹配------两者互补。
8.3.2 混合检索(Hybrid Search)
python
def hybrid_retrieve(query, vector_db, bm25_index, top_k=5):
# 向量分数(语义)
vec_scores = vector_db.search(query, top_k*4) # 粗召回取宽:20 条
# 关键词分数(BM25 精确匹配)
bm25_scores = bm25_index.search(query, top_k*4)
# 融合:RRF 按排名倒数合并(不依赖分数尺度)→ 取 top_k(5 条)
fused = rrf([vec_scores, bm25_scores])
return fused[:top_k]
# RRF:Reciprocal Rank Fusion
def rrf(results_list, k=60):
scores = {}
for results in results_list:
for rank, doc in enumerate(results):
scores[doc] = scores.get(doc, 0) + 1 / (k + rank + 1)
return sorted(scores, key=scores.get, reverse=True)

图 3:混合检索 + 重排
RRF 融合:把各检索结果的排名倒数相加,不依赖分数尺度(不同检索器的分数不可比,但排名可比)------这是混合检索的标准做法。
8.3.3 重排(Rerank):把"相关"变"精确"
检索回来的 Top-K 块,用**重排模型(Cross-Encoder)**精排------它把"问题和块"一起输入,打分更准但更慢,所以只对 Top-20 精排取 Top-3:
css
召回(粗排):向量+关键词 → Top-20(快,但糙)
↓
融合(Hybrid):RRF/加权 → Top-5(代码示例的 top_k)
↓
重排(精排):Cross-Encoder → Top-3(慢,但准)
↓
生成:只拿精排后的 Top-3 作为上下文
说明:生产实践中召回/融合/精排的数量可按预算调整(如 20→10→5),但原则不变------重排只对少量候选做,因为 Cross-Encoder 比双塔检索慢 1~2 个数量级。
重排的价值 :同样的检索,加重排后答案准确率通常有显著提升(具体幅度取决于数据、索引和任务,工程经验上常见 5~15%)------这是 RAG 效果优化里投入产出比最高的单点。
8.4 RAG 评测:命中率、忠实度、幻觉率
8.4.1 三个核心指标
RAG 质量要分环节评测,三个指标对应三个问题:

图 4:RAG 三指标评测
| 指标 | 回答的问题 | 评测方法 |
|---|---|---|
| 检索命中率(Recall@K) | 需要的资料被检索到了吗? | 标注每个问题的"标准块",看检索是否命中 |
| 生成忠实度(Faithfulness) | 答案有没有忠实于资料? | 抽答案句子,核对是否都能在资料中找到依据 |
| 幻觉率(Hallucination) | 答案编造了吗? | 答案中无法在资料中找到依据的比例 |
扩展指标 (生产级评测可参考 Ragas 等框架):Answer Relevancy (答案是否切题)、Context Precision (检索上下文中相关块的比例)、Context Recall(标准答案所需信息在上下文中的覆盖率)。这些指标能更精细地定位"是检索不准还是生成不好"。
8.4.2 最小评测实现
python
EVAL_SET = [
{"question": "报销上限是多少?", "gold_docs": ["doc-001"]},
{"question": "年假怎么休?", "gold_docs": ["doc-003"]},
...
]
def eval_rag(rag):
hit = 0
for case in EVAL_SET:
retrieved = [c["doc_id"] for c in rag.retrieve_with_meta(case["question"])]
if any(d in retrieved for d in case["gold_docs"]):
hit += 1
print(f"检索命中率: {hit}/{len(EVAL_SET)} = {hit/len(EVAL_SET):.0%}")
# 忠实度/幻觉率:用 LLM-as-Judge(第17章)抽评答案
8.4.3 评测集的建设
- 起步:20~30 条真实用户问题 + 人工标注"标准答案来自哪份文档";基线阶段扩展至 ≥50 条。
- 覆盖:普通问题 + 边界问题(问不到资料的、多文档联合的、专有名词的)。
- 迭代:线上漏检的案例回填评测集(呼应第 17 章样本库、第 20 章评测体系)。
🛠 解决方案:RAG 效果差的 10 大原因定位与修复
查不到(检索不到相关资料)
| # | 原因 | 修复 |
|---|---|---|
| 1 | 分块切断了关键语义 | 换递归分隔/语义分块 + 重叠 |
| 2 | 纯向量检索漏精确匹配 | 混合检索(+BM25)+ RRF |
| 3 | 块太大噪音多 | 调小块 + 重排 |
| 4 | 元数据过滤把正确块滤掉了 | 检查过滤条件 |
| 5 | 文档没被正确解析(PDF 表格/扫描件) | 换解析工具(OCR/版面分析) |
答不对(检索到了但答错)
| # | 原因 | 修复 |
|---|---|---|
| 6 | Top-K 太小,关键块没进上下文 | 调大 Top-K 或重排 |
| 7 | 上下文太长,关键信息被淹没 | 精简(只喂重排后的 Top-3) |
| 8 | 提示词没约束"基于资料" | 加"仅基于资料回答"约束 |
幻觉(答案编造)
| # | 原因 | 修复 |
|---|---|---|
| 9 | 资料缺失却硬答 | 加"没有就说没有"约束 + 忠实度评测 |
| 10 | 温度过高 | 调低 temperature(问答任务 0.3~0.5,第 24 章表二) |
解决方案速查表(定位口诀:先检索、后生成)
RAG 出问题 → 先跑评测看检索命中率
├─ 命中率低 → 问题在索引/检索(1~5)
├─ 命中率高但答错 → 问题在上下文组织/生成(6~8)
└─ 答案编造 → 问题在约束/温度(9~10)
实战提示
- 分块是地基:换分块策略是 RAG 优化的第一动作,见效最快。
- 混合检索 + 重排是标配:单点优化里性价比最高的两个。
- 元数据过滤别忘:给检索装"筛子",尤其多租户场景。
- 先评检索再评生成:定位问题先看命中率,别一上来就调提示词。
- 评测集要迭代:漏检案例回填,RAG 是越用越准的资产(与第 17 章 Guardrail 同哲学)。