ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

RAG去噪新思路:用NLI模型分离检索相关性与事实支撑性

RAG去噪新思路:用NLI模型分离检索相关性与事实支撑性

RAG 系统做久了,你会发现一个很反直觉的现象:检索回来的东西越多,回答质量反而越差。早期我以为是召回率不够,拼命加 chunk、调 top_k,结果模型开始胡言乱语,答案里混进一堆似是而非的片段。后来才想明白,问题不在“召回得少”,而在“召回得脏”。检索器只负责找“像”的内容,它不负责判断“对不对”。这两件事之间的鸿沟,就是噪声。这次我想聊聊怎么用 NLI(自然语言推理)模型给 RAG 做一道去噪工序,把“检索相关性”和“事实支撑性”这两件事拆开处理。这套思路适合已经跑通基础 RAG、但被幻觉和冗余上下文折磨的开发者,也适合想理解 RAG 进阶优化逻辑的同学,哪怕你只写过最简单的检索问答 Demo,也能照着往下走。

1. 为什么 RAG 需要一道独立的去噪工序

1.1 检索相关性不等于答案可用性

先把一个常见误解摆出来:很多人默认“检索得分高 = 这段内容有用”。实际上向量检索算的是语义相似度,它衡量的是“这段话和问题在讲同一件事”,而不是“这段话能回答这个问题”。这两者差别很大。

举个我实际遇到的例子。问“某个接口的超时重试次数默认是多少”,检索器可能把一段讲“重试机制设计原则”的文档排到最前面,因为里面反复出现“重试”“超时”“次数”这些词,语义相似度极高。但这段文档压根没给出具体数值,它讲的是理念。模型拿到这段上下文,要么编一个数字,要么含糊其辞。检索得分 0.89,可用性为零。

这就是噪声的第一种形态:语义相关但信息缺失。检索器没有能力识别它,因为它的目标函数里根本没有“是否包含答案”这一项。

1.2 噪声的三种典型形态

我把实际踩过的噪声归成三类,后面去噪策略的设计基本都围绕它们展开。

噪声类型表现检索器能否识别典型来源
语义相关但无答案讲同一主题,但不含所需事实不能主题性文档、概述性段落
语义相关但相矛盾含所需事实,但版本过时或说法冲突不能多版本文档、不同来源
完全无关的干扰项靠关键词巧合混进来部分能短 chunk、表格碎片、标题残留

第二类最阴险。同一份知识库里有新旧两版说明,检索器把两段都召回,一段说默认 3 次,一段说默认 5 次。模型看到矛盾信息,往往随机挑一个,或者干脆两个都提,输出“可能是 3 次或 5 次”。用户看到这种回答直接失去信任。检索器对此无能为力,因为它不知道哪段更新、哪段更权威。

1.3 为什么不在检索阶段一次性解决

有人会问,那我把检索器训得更准不就行了?方向对,但落地很难。检索模型的核心是双塔结构,query 和 document 分别编码再算相似度,它天生看不到“query 和 document 之间的推理关系”。你要让它判断“这段能不能推出答案”,等于要求一个相似度函数干推理的活,模型容量和训练目标都不匹配。

更现实的做法是两段式:检索负责“广撒网保召回”,去噪负责“精筛选保精度”。这跟搜索系统的经典架构是一个思路——召回层和排序层分离,各司其职。NLI 模型正好适合做这个精筛层,因为它的训练目标就是判断两段文本之间的蕴含、中立、矛盾关系,天然贴合“这段上下文能不能支撑这个答案”的判断需求。

提示:不要试图用一个大模型 prompt 同时完成检索和去噪,成本和延迟都会失控。分工明确的两段式流水线,在工程上更可控,也更容易定位问题。

2. NLI 模型凭什么能做去噪

2.1 三分类任务和 RAG 去噪的天然对应

NLI 的标准任务是给定前提(premise)和假设(hypothesis),判断两者关系:蕴含(entailment)、中立(neutral)、矛盾(contradiction)。把它映射到 RAG 场景,对应关系非常直接。

把检索到的 chunk 当作前提,把“待验证的答案陈述”当作假设。如果 chunk 蕴含该陈述,说明这段上下文能支撑答案,保留;如果中立,说明不相关或信息不足,丢弃或降权;如果矛盾,说明这段上下文和答案冲突,必须剔除,甚至可以作为“该 chunk 不可信”的信号。

这个映射的妙处在于,它把“去噪”从模糊的相关性打分,变成了一个有明确语义标签的分类问题。你不再需要拍脑袋定一个相似度阈值,而是拿到 entailment / neutral / contradiction 三个概率,决策依据清晰得多。

2.2 和交叉编码器重排的区别

这里要澄清一个容易混淆的点:NLI 去噪和常见的 cross-encoder 重排不是一回事,虽然它们结构相似。

重排模型输出的是一个相关性分数,本质还是“像不像”。NLI 模型输出的是关系标签,本质是“推不推得出”。前者回答“这段和问题相关吗”,后者回答“这段能证明这个说法吗”。在 RAG 里,后者才是我们真正需要的判断。

我做过对比测试,同一个知识库、同一批 query,用重排模型筛 top-3 和用 NLI 筛 top-3,后者在“答案准确性”上的提升更明显,尤其在多版本文档共存的场景下。重排模型会把新旧两版都留下,因为它俩都相关;NLI 模型能识别出“旧版陈述”和“新版陈述”之间的矛盾关系,从而触发冲突处理逻辑。

2.3 模型选型的现实考量

选 NLI 模型不用追求最大最强。去噪是一个高频调用的环节,每个 query 可能要跑几十次推理(每个候选 chunk 一次),延迟和成本必须算进去。

我的经验是:中小型 NLI 模型(参数量在亿级以内)足够用,因为判断“蕴含/中立/矛盾”比生成答案简单得多,不需要顶级模型的推理能力。优先选在你所在领域语料上做过微调的版本,通用 NLI 模型在专业术语密集的场景下会误判。如果找不到领域微调版,至少用领域数据做一轮小样本校准,把阈值调准。

另外要注意语言匹配。中文场景必须用中文 NLI 模型,拿英文 NLI 模型硬套中文文本,蕴含判断会大面积失真,这个坑我踩过,误判率高到没法用。

3. 去噪流水线的完整设计与实操

3.1 整体架构:先粗筛再精判

直接对每个召回 chunk 跑 NLI 推理,成本偏高。我的做法是两级过滤,先用便宜的规则粗筛,再用 NLI 精判。

第一级粗筛用轻量手段:去掉长度过短的 chunk(比如少于 50 字,信息量不足)、去掉纯标题或纯表格碎片、按检索分数做一个宽松的初筛(比如保留 top-20,不设硬阈值)。这一级的目标是砍掉明显没戏的,把候选量降下来。

第二级才是 NLI 精判。对剩下的候选 chunk,逐个和“待验证陈述”做 NLI 推理,拿到三分类概率,再按策略决策。这样 NLI 的调用次数从“全量召回”降到“精筛候选”,延迟能压下来一大截。

3.2 待验证陈述从哪来

这里有个关键设计问题:NLI 需要“假设”,也就是待验证的陈述。但用户输入的是问题,不是陈述。怎么把问题变成可验证的陈述?

两种做法。第一种是先让模型基于检索上下文生成一个草稿答案,再把草稿拆成若干陈述句,逐句去和 chunk 做 NLI。这种做法精度高,但多了一次生成调用,延迟增加。第二种是把问题本身改写成陈述形式,比如“默认重试几次”改写成“默认重试次数是某个值”,然后去验证 chunk 是否蕴含这个开放陈述。这种做法快,但对开放式问题的判断会偏粗。

我实际用的是混合策略:简单事实型问题走第二种,复杂多跳问题走第一种。判断标准是问题是否涉及多个实体或需要组合推理。这个分流逻辑用规则就能实现,不需要额外模型。

3.3 决策策略:三分类怎么用

拿到 entailment / neutral / contradiction 三个概率后,决策不能简单取 argmax,要结合业务容忍度。

def decide(entail_prob, neutral_prob, contra_prob, config): # 高置信蕴含,直接保留 if entail_prob >= config.entail_high: return "keep" # 高置信矛盾,直接剔除,并标记冲突 if contra_prob >= config.contra_high: return "drop_conflict" # 中立为主,说明信息不足,降权而非直接删 if neutral_prob >= config.neutral_high: return "downweight" # 灰区,保留但降权,交给下游排序 return "keep_low"

参数上,entail_high我一般设 0.7 到 0.8,contra_high设 0.6 左右(矛盾判断宁可敏感一点,因为矛盾信息危害最大),neutral_high设 0.6。这些值不是拍脑袋,是拿一批标注过的 query-chunk 对做校准得出的,不同领域要重新调。

注意:矛盾判断的阈值不要设太高。矛盾信息一旦漏过,模型很容易被带偏,输出自相矛盾的答案,这种错误比“信息不足”严重得多。宁可错杀,不可放过。

3.4 冲突检测的额外价值

NLI 去噪还有个副产品:冲突检测。当同一个待验证陈述同时被多个 chunk 蕴含,又被另一些 chunk 矛盾时,说明知识库里存在版本冲突。这时候不要简单按票数选,而应该触发一个“来源可信度”判断——比如按文档更新时间、来源权威性排序,优先采信更新的、更权威的。

我在流水线里加了一个冲突聚合模块,把每个陈述的 keep / drop_conflict 结果汇总,如果某陈述的冲突 chunk 数超过阈值,就在最终答案里加一句“该信息存在多个版本,建议核实”,把不确定性显式暴露给用户。这比让模型硬编一个答案诚实得多。

4. 实操中踩过的坑和排查手册

4.1 常见问题速查

问题现象可能原因排查方向解决思路
去噪后召回骤降,答案缺失entail 阈值过高统计各标签分布下调 entail_high,或改 argmax
矛盾信息仍混入答案contra 阈值过高抽查矛盾样本的 contra 概率下调 contra_high,加冲突聚合
延迟明显上升NLI 调用次数过多统计每 query 推理次数加强粗筛,减少候选量
专业术语判断失真通用模型领域不适配抽样看误判样本领域数据微调或小样本校准
中文文本判断混乱模型语言不匹配确认模型训练语种换中文 NLI 模型

4.2 阈值校准的正确姿势

阈值千万别照搬别人的。我见过有人直接抄了一组 0.8/0.6/0.6 的参数,结果在自己数据上效果很差。原因是不同 NLI 模型的概率分布差异很大,同一个 0.7 在不同模型上含义完全不同。

正确做法是:从你的真实业务里抽 100 到 200 个 query,人工标注每个 query-chunk 对的真实关系(蕴含/中立/矛盾),然后跑 NLI 拿到预测概率,画一条简单的分布曲线,找能让 F1 最高的阈值点。这个过程半天能搞定,但收益巨大。校准完你会发现,默认参数和最优参数的效果差距可能有 10 个点以上。

4.3 别忽略 chunk 质量本身

NLI 去噪能救“语义相关但无答案”的噪声,但救不了“chunk 本身切得烂”的问题。如果 chunk 切分把一句话拦腰截断,NLI 看到的是残缺前提,蕴含判断必然失真。

我在做去噪之前,先回头优化了 chunk 策略:按语义边界切,控制单 chunk 在 200 到 500 字,保留一定的上下文重叠。这一步做完,NLI 去噪的效果直接上了一个台阶。所以如果你发现去噪效果不理想,先别急着换模型,检查一下 chunk 质量,很多时候问题出在上游。

4.4 一个容易被忽视的细节:陈述拆分粒度

走“先生成草稿再拆陈述”这条路时,陈述拆得太粗或太细都不行。拆太粗,一句话里混了多个事实,NLI 判断会含糊;拆太细,一句话拆成好几个短陈述,每个都要跑一次推理,成本飙升,而且短陈述缺乏上下文,蕴含判断容易出错。

我的经验是按事实单元拆:一个陈述只包含一个可独立验证的事实点。比如“默认重试 3 次,超时 5 秒”要拆成“默认重试 3 次”和“超时 5 秒”两个陈述。这个拆分用规则加小模型辅助就能做,不用上大模型。

5. 效果评估与迭代方向

5.1 怎么衡量去噪到底有没有用

别只看最终答案的“感觉好不好”,要量化。我关注三个指标:答案准确率(人工抽检)、上下文冗余度(最终喂给生成模型的 token 数)、冲突暴露率(有多少 query 触发了冲突检测)。

实测下来,加了 NLI 去噪后,上下文冗余度通常能降 40% 到 60%,因为大量中立 chunk 被剔除了;答案准确率在事实型问题上提升明显,在多版本冲突场景下提升最大。冲突暴露率这个指标很有意思,它上升不一定是坏事,说明系统开始“知道自己不知道”,而不是硬编。

5.2 什么时候该上 NLI 去噪

不是所有 RAG 都需要这道工序。如果你的知识库干净、单一版本、chunk 质量高,基础检索加生成可能就够了,加 NLI 反而增加复杂度。但只要你遇到下面任一情况,就值得上:知识库有多版本共存、领域术语密集导致检索误召回、对答案准确性要求高不能容忍幻觉、上下文窗口吃紧需要精简。

我个人的判断标准是:当“检索 top-5 里经常混进没用的 chunk”成为常态时,就该考虑去噪了。这不是模型能力问题,是架构分工问题,早拆早省心。

5.3 后续可以怎么扩展

这套流水线跑稳之后,我做了两个扩展。一是把 NLI 的判断结果反哺给检索层,作为负样本微调检索模型,形成闭环;二是把冲突检测的结果沉淀成一个“知识库健康度”看板,定期暴露哪些文档存在版本冲突,推动上游治理。这两个方向都不复杂,但把去噪从“临时补丁”变成了“持续机制”。

最后分享一个实操心得:NLI 去噪的收益不是线性的,前期调阈值、修 chunk 的投入最大,一旦流水线稳定,边际成本很低。别指望一次调参就完美,我前后迭代了四五轮才把误判率压到可接受范围。耐心点,这道工序值得。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进