FDE Training · Module 04

04

RAG 全链路

模块 04 · RAG 全链路

本章目标:掌握"检索增强生成(RAG)"的整条链路——客户的海量文档怎么变成"模型能查、能答、少胡说"的知识。这几乎是 FDE 落地最多就拿得出手的核心技能。


4.1 先讲明白:为什么需要 RAG

大模型有两个天生的毛病:

  1. 它不知道客户私有知识(你家合同格式、审批规则、产品手册),训练时没见过。
  2. 它会乱编(幻觉),尤其问到它不知道的东西时。

RAG 的做法特别朴素——像查资料一样:

用户提问 → 先从客户文档里把最相关的几段"查"出来 → 把资料 + 问题一起给模型 → 模型"看着资料回答",就不容易胡说、也能答客户的私有知识。

一句话人话:RAG = 给模型配一个"随查随用的公司知识库",引经据典地回答。


4.2 RAG 全链路图(背下来)

客户文档(PDF/Word/MD)
   │  ① 文档解析 & 清洗
   ▼
干净的纯文本
   │  ② 文本分块 (chunking)
   ▼
若干小块 (chunk)
   │  ③ 向量化 (embedding)  + 建索引
   ▼
向量库 (Chroma/Milvus/PGVector)
              ▲                │
     用户提问 ─┴─ ④ 查询向量化 → ⑤ 相似度检索(top-K)
                              │
                              ▼  ⑥ 重排 (rerank)
                         精选片段
                              │  ⑦ 组装 Prompt: 片段+问题
                              ▼
                         大模型 → 引经据典地回答

每一环都是优化点,也是客户项目里"为什么效果差"的排查入口。


4.3 ① 文档解析 & 清洗(最容易被低估的一环)

4.3.1 现实有多残酷

客户给你的不是干净的 .txt,而是:

  • 扫描版 PDF(纯图片,没有文字层)→ 需要 OCR。
  • 有页眉页脚、乱表格、双栏 → 解析后一堆脏文本。
  • Word 里嵌图片/公式、Excel 报错的表。

"解析不清 → 分块就烂 → 检索就准不了 → 模型就胡说"。 这条因果链要刻进脑子。

4.3.2 常用手段

文档 工具/做法
PDF 文字版 pdfplumber / PyMuPDF(保留版面位置最好)
扫描 PDF/图片 OCR:PaddleOCR / Tesseract / 多模态模型
Word python-docx 抽正文,丢掉页眉脚注
Markdown/HTML 转纯文本或保留标题结构
表格 转成"行文"描述(如"某行某列"),或结构化存储

4.3.3 清洗

  • 去除页眉页脚、重复行、连续空行。
  • 规范化空白、编码(乱码/)。
  • 大表格、长段落可能要"折叠"或拆分处理。

4.4 ② 文本分块(chunking):仔细!决定检索准不准

核心矛盾:块太小 → 信息不完整、上下文缺失;块太大 → 检索不精准、token 费。

业界常用的"结构感知分块":

def smart_chunk(text, max_len=500, overlap=50):
    """优先按标题/段落切,其次按句切,保留 overlap 防止上下句被切断。"""
    chunks = []
    # 这里用简易伪代码示意;真实场景用标题结构 + 递归拆分
    paragraphs = [p for p in text.split("\n\n") if p.strip()]
    buf = ""
    for p in paragraphs:
        if len(buf) + len(p) <= max_len:
            buf += p + "\n\n"
        else:
            if buf: chunks.append(buf.strip())
            # 超长段落按句再拆
            buf = p if len(p) <= max_len else split_by_sentence(p, max_len)
    if buf: chunks.append(buf.strip())
    return chunks

def split_by_sentence(p, max_len):
    # 按句号切,拼装到长度内
    out, cur = [], ""
    for sent in p.replace("。", "。\n").splitlines():
        ss = sent.strip()
        if not ss: continue
        cur += ss + "。"
        if len(cur) >= max_len:
            out.append(cur); cur = ""
    if cur: out.append(cur)
    return out

关键点(面试加分):

  • 结构优先:先按标题/章节切,别把章节切成两半。
  • overlap(重叠):相邻块重叠一两句,防止问题正好卡在切开处。
  • 块大小 + 检索数(top-K)联动:决定每次"塞给模型多少 token"。

4.5 ③ 向量化 & 建索引

  • embedding 模型:把"一段文字"变成"一组数字(向量)",语义相近 → 数字相近。
  • 用 embedding 模型给每个 chunk 生成向量,存入向量库,同时建好索引。
  • 每次增删改都要同步向量库,否则客户文档更新了,检索还是老的(这是高频坑)。
import chromadb
from chromadb.utils import embedding_functions

client = chromadb.PersistentClient(path="/data/chroma")
col = client.get_or_create_collection(
    "contracts",
    embedding_function=embedding_functions.DefaultEmbeddingFunction(),
    metadata={"hnsw:space": "cosine"},
)

# 写索引
col.upsert(
    ids=["c1", "c2"],
    documents=["第3条 违约金为本金10%", "第7条 免责情形包括不可抗力"],
    metadatas=[{"doc_id": "contract-a", "title": "采购合同"}],
)

人话:向量库就是"按意思查"的数据库,存的时候把每段变成向量 + 留点元数据(来源、页、标题)。

补一段:向量相似度到底怎么算(余弦 vs 欧式)

向量库检索的本质是"找一个和问题向量最接近的 chunk 向量",这个"接近"就是向量相似度。常用的两种:

  • 余弦相似度:只看"方向"不看"长度",两个向量夹角越小越接近,取值 [-1, 1]。文本向量维度高、长度还会随词频变化,所以语义检索默认用余弦(上面代码里的 hnsw:space: "cosine" 就是它)。
  • 欧式距离:算两个点的直线距离,值越小越近。适合对"绝对数值"敏感的场景,文本语义一般不用它。

最小示意(纯 Python,不依赖库,看懂原理就行):

import math

def cosine(a, b):
    dot = sum(x * y for x, y in zip(a, b))
    na = math.sqrt(sum(x * x for x in a))
    nb = math.sqrt(sum(x * x for x in b))
    return dot / (na * nb)          # 越接近 1 说明越像

def euclidean(a, b):
    return math.sqrt(sum((x - y) ** 2 for x, y in zip(a, b)))  # 越接近 0 说明越近

q = [1, 2, 3]   # 问题向量(示意)
c = [1, 2, 4]   # 某个 chunk 向量(示意)
print(cosine(q, c))      # 接近 1,说明很像
print(euclidean(q, c))   # 距离小,说明很近

想再往深处补(向量空间、余弦与相似度背后的数学),去 算法与 AI 看 embedding 与检索原理、去 Python 机器学习与深度学习 看向量空间与相似度的完整推导。


4.6 ④⑤ 查询检索 & 召回

res = col.query(
    query_texts=["违约要赔多少"],   # 用户问题,自动向量化
    n_results=5,
    where={"doc_id": {"$eq": "contract-a"}},  # 可加过滤,如只查某个文档
)
for doc, meta, dist in zip(res["documents"][0], res["metadatas"][0], res["distances"][0]):
    print(meta["title"], round(dist, 3), doc[:40])

会出的问题(面试高频):

  • 召回不全:问题关键词和文档用词不一致(同义改写),相似度不够。→ 用更好的 embedding、混合检索。
  • 召回太杂:捞出来一堆不相关的。→ 提高阈值、重排。
  • 权限没隔离:A 员工查到 B 部门的机密。→ 检索层加权限过滤(metadata 字段)。

4.7 ⑥ 重排(Rerank):把"大概相关"变成"精准命中"

向量检索找的是"语义接近",但 top-K 里可能有"接近但不准"的。重排是对候选再精排一遍。

  • 为什么需要:纯向量 top-K 往往前几名才对,后几名噪音多。
  • 做法:用一个 rerank 模型,对"问题 × 每个候选块"打分,重排后取 Top-N 甚至前 3。
  • 混合检索的概念:
    • 向量检索(语义)+ BM25/关键词检索(精确匹配,如专属名称、编号)。
    • 两者结果合并、去重、重排。
    • 对"合同编号、法条原文、商品名"这种必须精确的,关键词检索很关键。

人话:向量找"像的",BM25 找"对的",rerank 把最终该用的排前面。


4.8 ⑦ 组装 Prompt & 控制幻觉

4.8.1 把检索结果喂给模型

def build_prompt(query, chunks):
    ctx = "\n\n".join(f"[片段{i+1}] {c}" for i, c in enumerate(chunks))
    return (
        "你是合同审单助手。请严格依据下列资料回答,资料没有的就说'资料中未提及',不要编造。\n"
        f"---- 资料 ----\n{ctx}\n---- 问题 ----\n{query}"
    )

4.8.2 降低幻觉的四个抓手

  1. "资料没有就直说"(上面 Prompt 就做了)。
  2. 给全来源引用:让模型回答时带来源片段,便于核查(也满足客户审计)。
  3. 检索兜底:检索结果太少/太差 → 明确告诉用户"暂未查到",别硬答。
  4. 严格约束格式 + 低 temperature:审单、法务这类要确定性。

4.9 RAG 评估 & 幻觉定位(和模块 06 呼应)

客户最常质疑:"AI 回答得对不对?"你得能量化:

层面 常见指标 人话
检索召回 Recall@K、命中率 该查到的资料有没有被捞出来
检索精度 Precision@K、RR@K 捞出来的够不够准、排序靠前吗
回答质量 准确率、忠实度(faithfulness)、幻觉率 答案对不对、有没有脱离资料乱说

幻觉定位清单(遇到"答错"按这个查):

  1. 检索到的资料本身有没有相关内容?没有 → 是召回问题。
  2. 资料有但模型没用上 / 解读错 → 是 Prompt / 模型问题。
  3. 资料被截断了(超上下文窗口、分块切坏)→ 看是否被 max_tokens / 窗口吞掉。
  4. 权限 / 脏数据导致的错误资料捞进来了。

4.10 模块练习

  1. 拿一份真实 PDF(含目录),用 pdfplumber 抽文本并清洗,跑通"解析 → 分块"。
  2. 用 Chroma 建一个 10 段小语料库,写 5 个问题做检索,标出每问的 top-3 和相关度感受。
  3. 在检索后接一个简单 rerank 逻辑,对比加与不加的 top-5 差异。
  4. 实现"资料没有就直说 + 带来源引用"的 RAG Prompt,构造 3 个超出资料范围的问法验证不瞎编。
  5. 模拟一次"检索召回漏"的定位:故意选一个同义词改写的问题,尝试用混合检索/换 embedding 改善。

4.11 本章面试题

  1. "RAG 是什么?为什么能减少幻觉?" → 答:检索增强生成——先从知识库检索与问题最相关的片段,再带着资料让模型回答。因为给了真实依据,所以能答私有知识、减少凭空编造。
  2. "RAG 效果差,你怎么排查是召回还是生成的问题?" → 答:先看检索结果:把 top-K 片段打印出来,若资料里根本没相关内容→召回/索引问题;若资料有你但答案错→Prompt/模型/长度截断问题。分而治之。
  3. "分块大小怎么定?" → 答:看内容结构和检索目标:按标题结构先分,再结合 query 粒度、上下文完整性、token 成本平衡;配合 overlap 和 top-K 调节。
  4. "为什么有的客户场景需要混合检索(向量+BM25)?" → 答:向量擅长语义模糊匹配,BM25/关键词擅长精确匹配(编号、专名、法条)。合同、法务、单据场景常要精确命中,只靠向量会漏。
  5. "客户文档要权限隔离,你怎么做?" → 答:索引时给每块打 metadata(文档归属/密级/部门);检索时注入用户权限条件(where 过滤),保证只召回该用户有权限的块;必要时加数据脱敏(模块 07)。

4.12 小结

  • RAG = 给模型配随查随用的知识库,引经据典回答问题。
  • 完整链路:解析清洗 → 分块 → 向量化建索引 → 查询召回 → 重排 → 组装 Prompt。
  • 解析不清 → 一切白搭;分块 + 检索 + 权限是三个最常出问题的环节。
  • 幻觉四抓手:资料没有就直说、带来源、检索兜底、低 temperature 严格格式。
  • 评估与幻觉定位是面对客户质疑的底气。下一章:让模型"会动手干活"——Agent 编排。

延伸阅读 · 去本站教学页补齐基础

  • Rust + AI 全栈——看 RAG 七步流水线、向量库 qdrant、chunk 切分策略与 rerank 的完整代码。
  • 算法与 AI——embedding、向量与检索的底层原理,理解"语义相近"到底在算什么。
  • Python 机器学习与深度学习——向量空间、余弦/欧式相似度的数学推导与实现。
本页由 FDE 培养课程文档生成,完整课程见 FDE 培养 · 课程总览。