第 8 章 RAG 工程

第 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 的两个天生缺陷:

  1. 知识边界:模型不知道私有数据、指定资料、需要可溯源的内容。RAG 让模型"带着资料回答",而不是凭训练记忆猜测。
  2. 幻觉倾向:不知道就编(呼应第 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)

实战提示

  1. 分块是地基:换分块策略是 RAG 优化的第一动作,见效最快。
  2. 混合检索 + 重排是标配:单点优化里性价比最高的两个。
  3. 元数据过滤别忘:给检索装"筛子",尤其多租户场景。
  4. 先评检索再评生成:定位问题先看命中率,别一上来就调提示词。
  5. 评测集要迭代:漏检案例回填,RAG 是越用越准的资产(与第 17 章 Guardrail 同哲学)。
相关推荐
CyberwayTech22 分钟前
从自然语言到结构化活动方案:Cyber TPM AI智能活动推荐如何嵌入TPM业务流程
人工智能·tpm·赛博威·营销费用管理·快消
桃西西呀23 分钟前
模型都能自己写代码了,你的 Agent 为什么还接不进一个日历?——一篇讲透 MCP 这个 AI 世界「USB-C」
人工智能·ai编程·mcp
伍树明23 分钟前
深入理解大模型Agent:从理论到年报ReAct Agent实战
人工智能
星火102423 分钟前
【从 0 到 1 动手造 Agent】03、给 Agent 装上操作系统——MemGPT/Letta 内存分层与自我演化
人工智能·后端·agent
Csvn26 分钟前
第 7 章 MCP 标准化工具接入
人工智能·aigc·agent
大模型码小白26 分钟前
Spring AI 框架中集成 MCP 的完整指南:从服务端到客户端的全流程实践
大数据·运维·数据库·人工智能·python·sql·spring
武子康28 分钟前
拆开 Pi Monorepo:改模型、循环、产品和 UI 时,代码应该放在哪一层
人工智能·llm·agent
hh95028 分钟前
Agent Plan × DeepSeek Harness:角色 Prompt 驱动的 Agent 分工优化与协作质量实验
java·前端·人工智能·prompt·adg·agent plan·adg成都社区
YHL29 分钟前
🐉 天龙八部 RAG 知识库实战:从零构建你的武侠 AI 助手
数据库·人工智能