第27回 高级 RAG(下)——模块化架构与训练方法
搭棚先立梁和柱,修路先分主与支。
若把百招揉一坨,出门一步就绊死。
前两回我们升级了两件事:
- 第25回:问得更像文档(查询变换)
- 第26回:找得更全更稳(混合检索与重排)
可工程一旦复杂,就会出现新麻烦:
你把所有招式都堆进一个“RAG 大管道”,它反而不稳:
- 不同问题需要不同策略,你却一锅煮
- 证据有的需要多跳,有的只要一句话,你却同样召回 k=10
- 有的文档适合关键词,有的适合语义,你却固定权重
所以高级 RAG 的终点,是“模块化”:
把系统拆成可替换模块,并让系统能“按题选招”。
很多 2024 的 RAG 综述把 RAG 演进拆成:Naive RAG → Advanced RAG → Modular RAG,强调模块化是走向可控与可维护的关键。1
一、模块化 RAG 的核心:路由 + 策略 + 记账
模块化 RAG 至少要有三件东西:
- 路由(Router):这题该走哪条管道?
- 策略(Policy):k 取多少?用 BM25 还是 dense?要不要 HyDE?要不要重排?
- 记账(Telemetry):每一步做了什么、花了多少、效果如何
看官若问“这不就是智能体吗”?
正是。
模块化 RAG 本质是把检索做成“可控工作流”,
而不是一段固定模板。
二、三种常见模块:路由、压缩、纠错
1)路由模块:按题选检索法
举个最常见的路由规则:
- 问题包含专名/编号/日期:更偏 BM25
- 问题是描述性口语:更偏 dense + 改写
- 问题是多跳:先分解再检索
真实系统里路由可以是规则,也可以是小模型/大模型分类器。
2)上下文压缩:把“十段证据”变成“可读证据”
检索召回越多,生成越容易被噪声污染。
因此常见“压缩”模块:
- 抽取与问题最相关的句子
- 去重与合并
- 只保留可引用片段
3)纠错与再检索:证据不足就回头
如果评估模块判断:
- 证据相关性低
- 证据不足以支撑回答
就触发:
- 改写查询
- 扩大召回
- 换检索器
- 再检索
这就是导读提到的“纠错循环”精神;CRAG 是讨论这一方向的代表之一。2
三、“训练方法”讲什么:让系统学会何时检索、检索多少、怎么用证据
模块化 RAG 不只是一堆规则堆叠。
它也可以被“学习”:
- 训练检索器:让向量更懂你的领域文档
- 训练重排器:让排序更符合真实相关性
- 训练路由器:让系统在不同问题上选择更优策略
- 训练生成器的“用证据能力”:让它学会引用、学会拒答、学会纠错
这就把 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
-
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
-
Gu, J.-C., et al. Corrective Retrieval Augmented Generation arXiv:2401.15884 (2024-01) https://arxiv.org/abs/2401.15884 ↩ ↩2
-
Yu, H. Evaluation of Retrieval-Augmented Generation: A Survey arXiv:2405.07437 (v2: 2024-07-03) https://arxiv.org/abs/2405.07437 ↩