通用 RAG 进入领域后为什么会失效
很多团队的 RAG 系统在通用问答上表现不错,一接入法律、医疗、金融或企业内部知识库就明显拉胯。常见的反应是换更大的模型、换更强的 embedding,或者干脆把 top-k 调大。这些动作偶尔有用,但多数时候是在掩盖一个更根本的问题:失效发生在哪一层没有搞清楚,适配手段就无从谈起。领域失效不是单一问题,而是三层不同性质的问题叠加,每层需要完全不同的解法。
第一层:词法失配——词表里根本没有这些词
领域的第一道墙是词法层面的。医疗领域的"室性早搏(VPC)""房颤(AF)",法务领域的"不当得利""无因管理",企业内部的"凤凰计划""2024 采购制度 v3",这些术语、缩写和内部代号在通用模型的训练分布里要么稀疏、要么缺失。后果在检索侧和生成侧表现不同:检索侧,embedding 对稀疏词的向量质量差,BM25 因为分词切错(比如"无因管理"被切成"无因"和"管理")而失灵;生成侧,模型遇到陌生缩写会自信地瞎编展开——把 VPC 编成"虚拟私有云"而不是"室性早搏",这种错误在跨领域词汇上尤其常见(VPC 在云计算语境是后者的意思)。
词法失配最重要的特征是:它是纯数据问题,不需要动模型就能大幅缓解。这一点后面会反复用到。
第二层:语义失配——同形不同义
第二道墙更隐蔽:同一个词形在领域内外的含义不同。金融语境的"利率期限结构"不是通用语料里"利率"和"结构"的组合语义;"缓存"在 CPU 微架构文档和 Redis 运维文档里是两个东西;"利率"在债券交易台和存款业务线的语境里指向完全不同的文档集合。通用 embedding 把这些词映射到"平均语义",在领域内的判别力就会下降——两个本该区分开的概念在向量空间里挤在一起,检索出来的文档互相污染。
语义失配的判断方法是抽样看 bad case:如果检索回来的文档在词面上高度相关(关键词都对得上)但语义上答非所问,基本就是这一层的问题。这一层靠同义词表和查询侧消歧能解决一部分,剩下的需要 embedding 层面的适配。
第三层:推理与格式习惯失配
第三道墙最少被人谈论,但对答案质量影响不小:领域有自己的推理习惯和输出格式。法务答案需要引用条文编号("依据《民法典》第 X 条"),医学答案需要区分循证等级,财务答案习惯用表格和科目余额对齐,检验报告有固定的字段结构。通用 LLM 没有在这些格式上被专门训练过,它给法务问题的答案是"知乎体"——流畅、正确、但没有一个条文引用,这在专业场景里等于不可用。
这层的失效发生在生成侧而不是检索侧,所以诊断时要能把两者分开:检索明明命中了正确的条文,答案却丢掉了引用格式——这是模型习惯问题,换检索方案解决不了。
先诊断再动手:三层失效的定位方法
动手适配之前,必须有一套能把三层失效分离出来的诊断流程。没有诊断的适配就是在三个杠杆里瞎拉,而其中最贵的那个(继续预训练)经常被最先拉。
构建 domain probe set
从领域语料里构造一个 100 到 300 条的探针集,每条包含查询、标准答案、以及标准答案应该命中的文档(gold chunk)。这个集合有三重用途:分层诊断、适配后的回归测试、以及作为后续微调数据的种子。构造时注意两点:一是查询要模拟真实用户的问法(包括缩写、口语化、错别字),不要写成新闻标题式的工整问句;二是 gold chunk 的标注要宽进——凡是包含答案依据的段落都算命中,不要只标唯一一段。
分层归因的三个实验
有了探针集,做三个实验就能把失效归因到层:
- 检索归因:用探针查询跑检索,看 recall@10。低于 70% 说明检索侧有问题,优先处理词法与语义层。
- 排序归因:把 gold chunk 人工混入候选池,只跑 reranker,看 gold chunk 能不能进 top-3。能进说明召回没问题而是排序问题。
- 生成归因:把 gold chunk 直接塞进 prompt,看答案质量与格式。这时如果答案还是丢引用、错格式、漏要点,问题就在生成侧的领域习惯上。
三个实验的组合给出明确的适配方向,下面的 mermaid 图是这套归因逻辑的完整决策流程:
flowchart TD
A[领域探针集 100-300 条] --> B{检索归因<br/>recall@10}
B -- 低于 70% --> C[词法/语义层失效]
B -- 达标 --> D{排序归因<br/>gold 进 top-3?}
C --> C1[文档侧术语展开]
C --> C2[查询侧同义词表]
C --> C3[embedding 领域微调<br/>差距大才动]
D -- 不能进 --> E[排序层失效<br/>reranker 微调或换模型]
D -- 能进 --> F{生成归因<br/>gold 直塞 prompt}
E --> F
F -- 仍差 --> G[生成侧适配<br/>RAFT / 格式 SFT]
F -- 达标 --> H[管线组装问题<br/>调上下文组装]
C1 & C2 & C3 & E & G & H --> I[回归测试<br/>领域探针 + 通用集]
检索侧适配:先做不动模型的三件事
检索侧适配有一个性价比阶梯,绝大多数团队走到第二级就够了,根本轮不到微调 embedding。
第一级:文档侧的术语展开
在入库的预处理阶段做术语规范化:缩写首次出现处展开成"全称(缩写)"的并列形式,内部代号在文档头部的元数据区补一行别名说明。这个改写让 embedding 和 BM25 同时受益——embedding 拿到的是"全称+缩写"的丰富上下文,向量质量提升;BM25 拿到了缩写这个字面 token,字面匹配通道打通。成本只是入库时的一张术语映射表加一遍文本替换,而且可逆、可重跑、可灰度,是所有手段里风险最低的。
第二级:查询侧的同义词与别名词典
在查询理解层加一张领域别名词典:用户问"室早",查询改写模块把它规范成"室性早搏"并保留原词双路检索。这张词典最好由领域专家给出种子、再从检索日志里挖补充(高点击但低字面重叠的 query-doc 对就是现成的别名对)。注意别名词典要做成有方向的扩展而不是无脑双向替换——"室早→室性早搏"是安全的,反向替换会污染那些真的在讲其他东西的查询。这个方向性问题在我们系列里讲领域术语表的那篇有专门展开。
第三级:embedding 领域微调——什么时候才值得
微调 embedding 的判据很明确:做完前两级后,探针集 recall@10 仍然低于 80%,且 bad case 归因指向语义失配而非词法失配。只有两个条件同时满足才动这一步。数据构造不需要人工标注:从语料结构里挖弱监督对——标题与正文、问题与答案、条文与释义、代码与 docstring,几万对起步;再按对比学习的惯例挖难负例(用当前模型检索 top-20 里非 gold 的部分)。
经验教训里最重要的一条是防灾难性遗忘:只拿领域数据微调,模型会丢失通用语义结构,表现为领域内涨点、通用查询大幅回退。缓解办法是混合采样——领域数据与通用开源检索集(比如 MS MARCO 的子集)按 3:1 到 5:1 混训。此外学习率要小(1e-5 到 3e-5 量级)、训练轮数要少(1-2 个 epoch),embedding 微调的目标是"语义校准"而不是"重新学一遍检索"。
生成侧适配:RAFT 与格式固化
RAFT:让模型学会在噪声里找答案
检索系统的现实是永远有噪声:top-k 里混着无关段落、过时段落和"看起来相关实则答非所问"的段落。但常规 SFT 训练时给模型的都是干净的正确上下文,模型从没练过"面对五个段落里三个是干扰项该怎么办"。RAFT(Retrieval Augmented Fine-Tuning,Zhang 等人 2024 年提出,arXiv:2403.10131)专门修这个洞:训练数据构造时,把 oracle 段落与若干 distractor 段落混在一起作为上下文,让模型生成带推理链(CoT)的答案——模型被迫先判断"哪个段落有用",再作答。
RAFT 论文在 PubMed(生物医学问答)和 HotpotQA(多跳问答)上的实验给出了两个关键结论:一是相对不区分干扰项的常规 SFT,RAFT 在 RAG 场景下有约 2 到 4 个点的稳定提升;二是训练时不带 distractor 的模型在推理时遇到检索噪声会显著退化。为什么这样做:因为 RAG 的线上分布就是"有噪声的上下文",训练分布贴近线上分布这个朴素原则,在检索增强场景里经常被 SFT 管线忽略。
# RAFT 风格训练样本构造(示意)
import random
def build_raft_sample(query, oracle_chunk, distractor_pool, k_distract=3):
"""把 oracle 段落与 k 个干扰段落混合,构造带 CoT 的训练样本"""
distractors = random.sample(distractor_pool, k_distract)
ctx_chunks = distractors + [oracle_chunk]
random.shuffle(ctx_chunks) # oracle 位置随机,防止模型学到位置偏置
context = "\n\n".join(
f"[段落{i+1}] {c}" for i, c in enumerate(ctx_chunks)
)
# 目标答案由强模型基于 oracle 生成,并要求显式引用段落编号作为推理链
prompt = (
f"以下是从知识库检索到的若干段落,其中部分与问题无关。\n"
f"{context}\n\n问题:{query}\n"
f"请先指明哪些段落包含答案依据,再给出最终答案。"
)
return {"prompt": prompt, "meta": {"oracle_ids": [oracle_chunk[:32]]}}
格式与引用习惯:小数据 SFT 的高杠杆用法
输出格式(法条引用、表格化、字段顺序)是生成侧最容易也最值得修的:几百到一千条带标准格式的领域问答对做 SFT,就足以把输出骨架掰过来。这类数据甚至可以先用强模型生成、领域专家只做格式与事实抽查,人力成本低。为什么不做更大的 SFT:因为知识本身不该靠 SFT 灌——模型参数里的领域知识无法溯源、无法更新、无法审计,与 RAG"知识在库、模型只负责读"的架构哲学直接冲突。SFT 只该改"习惯",不该塞"知识"。
DAPT:最后才考虑的重武器
继续预训练(领域自适应预训练,DAPT,Gururangan 等人 ACL 2020 提出)动辄需要数十亿 token 的领域语料和可观的算力,而且收益高度依赖前提:领域语料与模型训练分布的差异足够大,且领域语料规模足够(通常十亿 token 级起步)。在决定走这条路之前先做一个便宜实验:拿领域语料里的原文段落直接考模型 zero-shot 阅读理解。如果模型已经能读懂领域文本(比如现代大模型对金融法律文本的阅读理解普遍不差),DAPT 的边际收益就很小,预算应该全部投给检索侧和 RAFT。通用基座已经"见过"领域文本这件事,是 2020 年代初的结论在今天经常失效的原因——DAPT 是给"模型完全没见过的领域"准备的,不是给"模型见过但读得一般"的领域准备的。
适配顺序与回归测试
推荐路径:先便宜后昂贵
把上面的手段按成本排成一条路径:文档侧术语展开 → 查询侧别名词典 → 生成侧格式 SFT(如果格式是主要痛点)→ RAFT(如果噪声鲁棒性是主要痛点)→ embedding 微调 → reranker 微调 → DAPT。每一级做完都回探针集重测,涨幅低于预期就停下来重新归因,而不是机械地走完全部六级。实践中大部分领域问题在前三级就能解决到可上线水平,微调级别的动作留给确实有语义鸿沟的领域(比如强专业性的医学子领域、内部代号密集的企业环境)。
适配的守门:通用能力回归
所有动过模型参数的适配,必须同时跑通用基准做回归(MMLU 类知识集 + 通用检索评测集),并且把回归阈值写进发布流程:领域探针涨点而通用集回退超过 1 个点,就不允许发布。工程上更优雅的做法是用 LoRA 做领域适配器:领域权重与基座分离,按查询域动态挂载或干脆多领域多适配器并存,通用流量走基座——适配不再是有损的"改造",而是可插拔的"外挂",回滚与多领域共存都变成配置问题。
小结
领域适配的正确姿势是"先归因、再适配、后回归",而不是看到效果差就拉最贵的杠杆。三层失效对应三组手段:词法失配用文档侧术语展开和查询侧别名词典解决,纯数据手段、零模型成本;语义失配才值得动 embedding 微调,且必须混合通用数据防遗忘;生成侧的格式与噪声鲁棒性问题交给格式 SFT 和 RAFT,SFT 只改习惯、不塞知识。DAPT 是留给"模型真的没见过"的领域的最后选项。整条路径的原则只有一句话:每一级适配都必须被探针集验证过收益、被通用集验证过无害,才允许进入下一级。RAG 领域适配不是一次性的项目,而是带着守门评测的持续迭代——把它做成流程,比做成某次 heroic 的微调更接近生产可用。
读者留言
COMMENTS 暂无还没有留言,来说第一句?