第27回 高级 RAG(下)——模块化架构与训练方法

搭棚先立梁和柱,修路先分主与支。
若把百招揉一坨,出门一步就绊死。

前两回我们升级了两件事:

  • 第25回:问得更像文档(查询变换)
  • 第26回:找得更全更稳(混合检索与重排)

可工程一旦复杂,就会出现新麻烦:
你把所有招式都堆进一个“RAG 大管道”,它反而不稳:

  • 不同问题需要不同策略,你却一锅煮
  • 证据有的需要多跳,有的只要一句话,你却同样召回 k=10
  • 有的文档适合关键词,有的适合语义,你却固定权重

所以高级 RAG 的终点,是“模块化”:
把系统拆成可替换模块,并让系统能“按题选招”。

很多 2024 的 RAG 综述把 RAG 演进拆成:Naive RAG → Advanced RAG → Modular RAG,强调模块化是走向可控与可维护的关键。1


一、模块化 RAG 的核心:路由 + 策略 + 记账

模块化 RAG 至少要有三件东西:

  1. 路由(Router):这题该走哪条管道?
  2. 策略(Policy):k 取多少?用 BM25 还是 dense?要不要 HyDE?要不要重排?
  3. 记账(Telemetry):每一步做了什么、花了多少、效果如何

看官若问“这不就是智能体吗”?
正是。
模块化 RAG 本质是把检索做成“可控工作流”,
而不是一段固定模板。


二、三种常见模块:路由、压缩、纠错

1)路由模块:按题选检索法

举个最常见的路由规则:

  • 问题包含专名/编号/日期:更偏 BM25
  • 问题是描述性口语:更偏 dense + 改写
  • 问题是多跳:先分解再检索

真实系统里路由可以是规则,也可以是小模型/大模型分类器。

2)上下文压缩:把“十段证据”变成“可读证据”

检索召回越多,生成越容易被噪声污染。
因此常见“压缩”模块:

  • 抽取与问题最相关的句子
  • 去重与合并
  • 只保留可引用片段

3)纠错与再检索:证据不足就回头

如果评估模块判断:

  • 证据相关性低
  • 证据不足以支撑回答

就触发:

  • 改写查询
  • 扩大召回
  • 换检索器
  • 再检索

这就是导读提到的“纠错循环”精神;CRAG 是讨论这一方向的代表之一。2


三、“训练方法”讲什么:让系统学会何时检索、检索多少、怎么用证据

模块化 RAG 不只是一堆规则堆叠。
它也可以被“学习”:

  1. 训练检索器:让向量更懂你的领域文档
  2. 训练重排器:让排序更符合真实相关性
  3. 训练路由器:让系统在不同问题上选择更优策略
  4. 训练生成器的“用证据能力”:让它学会引用、学会拒答、学会纠错

这就把 RAG 从“工程技巧”推进到“端到端系统学习”。

在 2024 的 RAG 评估综述里也提到:
很多基准开始更强调“组件协同”的能力,而不是单一检索或单一生成。3


四、极简可跑代码:一个“路由 + 检索 + 过滤”的模块化小流水线

我们做一个最小模块化 RAG:

  • 路由器:看问题里有没有专名(这里用“包含英文/数字”当玩具特征)
  • 检索器 A:BM25(更偏关键词)
  • 检索器 B:余弦词袋(更偏语义玩具)
  • 过滤器:只保留得分超过阈值的证据
import math
import re


DOCS = [
    "BM25 擅长关键词与专名匹配。",
    "Dense 检索擅长同义表达与语义相似。",
    "HyDE 先生成假文档再检索真实证据。",
    "RAG 评估要看忠实度与引用准确性。",
    "CRAG 讨论检索错误时的纠错与再检索。",
]


def tok(s):
    return [w for w in re.split(r"[^0-9A-Za-z\u4e00-\u9fff]+", s) if w]


def bow(tokens):
    v = {}
    for t in tokens:
        v[t] = v.get(t, 0) + 1
    return v


def cosine(a, b):
    da = sum(x * x for x in a.values())
    db = sum(x * x for x in b.values())
    if da == 0 or db == 0:
        return 0.0
    dot = sum(a.get(k, 0) * b.get(k, 0) for k in a.keys())
    return dot / math.sqrt(da * db)


def bm25_build(docs):
    toks = [tok(d) for d in docs]
    N = len(docs)
    df = {}
    lens = [len(t) for t in toks]
    avgdl = sum(lens) / max(1, N)
    for ts in toks:
        for w in set(ts):
            df[w] = df.get(w, 0) + 1
    idf = {w: math.log(1 + (N - c + 0.5) / (c + 0.5)) for w, c in df.items()}
    return {"toks": toks, "idf": idf, "lens": lens, "avgdl": avgdl}


def bm25(q, i, idx, k1=1.2, b=0.75):
    ts = idx["toks"][i]
    tf = {}
    for w in ts:
        tf[w] = tf.get(w, 0) + 1
    dl = idx["lens"][i]
    avgdl = idx["avgdl"]
    s = 0.0
    for w in q:
        if w not in tf:
            continue
        f = tf[w]
        denom = f + k1 * (1 - b + b * dl / avgdl)
        s += idx["idf"].get(w, 0.0) * (f * (k1 + 1) / denom)
    return s


def route(q):
    if re.search(r"[A-Za-z0-9]", q):
        return "bm25"
    return "dense"


def retrieve(q, topk=3):
    r = route(q)
    qt = tok(q)
    if r == "bm25":
        idx = bm25_build(DOCS)
        scored = [(bm25(qt, i, idx), i) for i in range(len(DOCS))]
    else:
        qv = bow(qt)
        scored = [(cosine(qv, bow(tok(DOCS[i]))), i) for i in range(len(DOCS))]
    scored.sort(reverse=True)
    return r, [(DOCS[i], float(score)) for score, i in scored[:topk]]


def filter_ctx(hits, thr):
    return [d for d, s in hits if s >= thr]


if __name__ == "__main__":
    for q in ["HyDE 是什么", "BM25 适合啥", "RAGTruth 评估什么"]:
        r, hits = retrieve(q, topk=3)
        ctx = filter_ctx(hits, thr=0.05)
        print("Q:", q, "| route:", r)
        print("CTX:", ctx)
        print()

这就是“模块化”的味道:
你可以替换路由规则、替换检索器、替换过滤阈值,
而不用把整套系统推倒重来。


五、小结:模块化是为了“可控”,训练是为了“自适应”

这一回你要记住:

  • 模块化 RAG = 路由 + 策略 + 记账
  • 常见模块:路由、压缩、纠错循环
  • 训练不是玄学:就是让系统学会何时检索、检索多少、怎么用证据

下一回(第28回)我们从“算法”转到“账本”:
RAG 真正上线后,最先打你的不是论文难题,而是:

  • 延迟与并发
  • 缓存与成本
  • 隐私与合规

这三件事不解决,再好的 RAG 也只能停在演示里。
欲知后事如何,且听下回分解。


幻觉核查

  • “Naive/Advanced/Modular RAG”三段式表述:可核对 RAG 综述对 RAG 演进与组件化的归纳。1
  • CRAG 的纠错检索定位:可核对其摘要对“纠错与再检索”的描述。2
  • 本回代码为教学用路由/检索/过滤最小例子,不宣称与真实系统的学习型路由器等价。

逻辑审计

  • 与第26回衔接:第26回讲“混合+重排”作为单招,本回把单招装进可替换模块与可路由流水线。
  • 与第24回一致:模块化的目的之一是更容易做诊断与回归评估:每个模块都能单独测。
  • 为后续铺路:第28回将把“记账”延伸到并行、缓存、隐私;第29回讨论长上下文对模块化 RAG 的冲击与融合。

引用与溯源

Footnotes

  1. Gao, Y., et al. Retrieval-Augmented Generation for Large Language Models: A Survey arXiv:2312.10997 (v5: 2024-03-27) https://arxiv.org/abs/2312.10997 2

  2. Gu, J.-C., et al. Corrective Retrieval Augmented Generation arXiv:2401.15884 (2024-01) https://arxiv.org/abs/2401.15884 2

  3. Yu, H. Evaluation of Retrieval-Augmented Generation: A Survey arXiv:2405.07437 (v2: 2024-07-03) https://arxiv.org/abs/2405.07437