大模型又幻觉了?因为你少了一道“判官“工序
你搭好了 RAG:切分、向量化、检索、重排,四件套齐活。结果一上线,大模型还是给你一本正经地胡说八道。
问题多半不在召回,而在最后那一关没人把关。向量相似度 0.55 的段落,你不知道它是"真相关"还是"只是字面像"——而这恰恰是幻觉的温床。
这篇讲我在开源项目 feishu-kb-qa 里用生产级代码实现的一套反幻觉方案:向量粗排 → LLM 判官打分 → 对抗式复核 → fail-closed 兜底。用真实数据告诉你,怎么把无关文档误报从 13 条压到 0。
适合人群:已经搭过 RAG demo、想上生产但被幻觉困扰的工程师。源码获取方式见文末。

一、先承认一个事实:向量分数根本分不清"相关"和"无关"
很多人搭 RAG 时默认信一个假设:向量相似度高 = 相关,低 = 不相关,设个 0.7 阈值就能过滤。
这是 RAG 里最大的一个坑。我用自己项目的真实数据给你看(嵌入模型是智谱 embedding-3):
| 段落类型 | 余弦相似度区间 |
|---|---|
| 真相关(真能回答问题的段落) | 0.55 ~ 0.76 |
| 不相关(只是字面/话题沾边) | 0.25 ~ 0.55 |
| 重叠区 | 0.55 附近 |
看出来了吗?相关的和不相关的,在 0.55 附近是重叠的。意思是:一条 0.56 的段落,它可能是真答案,也可能是个"看着像但其实没用"的干扰项——光看分数,你根本分不清。
📌 关键认知:如果你照搬网上"设 0.7 阈值"的经验,真答案(0.55~0.76)会被误杀一大半;如果你不设阈值全放进去,干扰项会带偏大模型。向量相似度只能用来"排序",不能用来"判定相关"。

▲ 图 1 | 真相关和不相关在 0.55 附近重叠——光看分数,分不清"真能答"和"只是字面像"
那怎么办?我的答案:让 LLM 来当判官。
二、核心思路:给 RAG 加一道"判官"工序
先看这张图,理解"判官"在整个 RAG 链路里的位置:

▲ 图 2 | 判官不是替代重排,而是叠在重排之后——向量负责"别漏",判官负责"别错"
经典 RAG vs 加判官的 RAG
| 经典 RAG(向量+重排) | 我的方案(向量+判官+复核) | |
|---|---|---|
| 召回 | 向量/BM25 | 向量/BM25(一样) |
| 粗排 | 向量余弦排序 | 向量余弦排序(一样) |
| 精排 | rerank 模型或直接 Top-K | LLM 判官逐篇打 0~3 分 |
| 兜底 | 无 | fail-closed,宁可留空不硬塞 |
| 结果 | 分不清"字面像"和"真相关" | 只有"真能回答"的才喂给大模型 |
一句话总结这条链路的设计哲学:
🎯 心法:检索是核心,宁可找不到、也不能找错。
向量负责"广撒网别漏"(recall),判官负责"严把关别错"(precision)。两件事分工,各管一头。
三、判官怎么打分:0~3 分的证据价值标准
判官不是让 LLM 随便说"相关/不相关",而是给它一套严格的 0~3 分评分标准——评的是"证据价值",不是"话题重合度"。
这是我项目里判官 prompt 的核心(逐字摘自源码):
| 分数 | 含义 |
|---|---|
| 3 | 直接且充分地回答了问题 |
| 2 | 含与问题直接相关的实质信息,可支撑部分回答 |
| 1 | 仅同主题/同领域,但不含可回答的信息(如复述问题、只表达相同诉求、仅有标题) |
| 0 | 与问题无关,或属于另一主题 |
这里有两条规则,是整套设计的灵魂:
📌 关键认知 1:评分标准是"是否提供了可回答问题的信息",而不是"词语或话题重合度"。
📌 关键认知 2:不确定是否含有效信息时,取较低分(宁可误杀,不可错放)。

▲ 图 3 | 判官看的是"答得上答不上",不是"像不像"——一个像看脸,一个像看身份证
为什么"证据价值"比"相似度"准
举个真实例子。用户问:“实时电价波动怎么分析?”
| 候选段落 | 向量相似度 | 判官打分 | 为什么 |
|---|---|---|---|
| 《负电价出现的场景和影响》正文 | 0.585 | 3 | 直接回答了电价波动 |
| 《电力现货市场未来技术需求.pdf》 | 0.521 | 0 | 只是同领域,根本没回答波动分析 |
| 一条聊天记录"现在电价多少?" | 0.56 | 1 | 只是追问同一问题,不是答案 |
看第三条——向量相似度 0.56,看着挺高吧?但它只是用户在问同一个问题,根本没有答案。判官一眼就能识破:“这是复述问题,判 1 分”。
📍 小结:向量看的是"长得像不像",判官看的是"答得上答不上"。一个像看脸,一个像看身份证——判断"能不能用",得看身份证。
四、判官还不够:再加一道"对抗式复核"
判官很强,但不完美。我实测它在真实流量上的准确率大概是 90~95%。
它有个软肋:偶尔会对"话题相邻、沾了点边"的文档心软,给个 2 分放进来。残留的那几条误报,几乎都出在这种"2 分边界档"上。
复核策略:只盯"压线档",换"怀疑者"口吻
我没有让判官对所有结果再判一遍(太贵),而是只挑"刚好压线(2 分)"的边界档,换一个"怀疑者"的口吻再严判一遍:

▲ 图 4 | 判 3 分的直接信任、判 1/0 的直接丢,只对压线 2 分换怀疑者严判——省调用又提精度
怀疑者的 prompt 长这样(逐字摘自源码):
你是严格的相关性复核者。判断它是否确实为回答该问题提供了有效信息(不是只沾了话题的边)。
抱持怀疑:只有当它真的包含可回答该问题的信息时才判 true;泛泛相关、只提到话题、只是同类事项、拿不准,一律 false。
对比一下两个角色的口吻:
| 角色 | 口吻 | 倾向 |
|---|---|---|
| 判官(初判) | 客观评估证据价值 | 不确定取较低分 |
| 怀疑者(复核) | 抱持怀疑,拿不准就毙 | 极度严格 |
为什么不全量复核
❌ 误区:很多人一想到"复核",就让所有结果都过一遍。
✅ 正确做法:只对压线 2 分复核。判 3 分的(很确定)直接信任省一次调用;判 1/0 的直接丢掉也不用复核。复核是给"模棱两可"的边界档准备的"二审"。
📍 小结:判官解决 90% 的问题,复核专治那 5~10% 的"心软误报"。好钢用在刀刃上,复核只用在边界档。
五、最关键的设计:fail-closed(宁可留空,不硬塞)
整套方案里,我个人觉得最值钱的是这条原则:fail-closed。
什么是 fail-closed?就是当精排失败、或者判官判定一篇都不相关时,宁可给用户"没找到",也绝不把召回的内容硬塞给大模型。
三个 fail-closed 的落地点
| 场景 | 错误做法(放任幻觉) | 正确做法(fail-closed) |
|---|---|---|
| 精排代码报错 | 把召回的全部原样展示 | 退化为"过阈值的",都没有就留空 |
| 判官判定一篇都不相关 | 还是喂点东西给大模型让它编 | 触发"没找到相关内容"兜底卡 |
| 判官自身调用失败 | 给 0 分全杀 | 给 3 分"不误杀"(外层有兜底) |
注意第三条是个微妙的设计:判官内部失败是"fail-open"(给 3 分保留),但外层精排是"fail-closed"(留空)。两层方向相反,是为了应对不同的失败——判官偶发失败时别误杀真答案,整条精排崩溃时别放毒。

▲ 图 5 | 宁可留空不硬塞——判官内部 fail-open(别错杀),外层精排 fail-closed(别放毒)
为什么 fail-closed 是反幻觉的命门
幻觉的本质是大模型基于错误/无关的上下文"编"出看似合理的答案。你给它的每一段无关内容,都是在递给它"编造的素材"。
所以最朴素的反幻觉逻辑就是:别给它喂不确定的东西。找不到比找错强——用户看到"没找到"会换个问法,看到"一本正经的胡说"会失去信任。
🎯 心法:检索是核心,宁可找不到、也不能找错。
六、真实战绩:误报从 13 条降到 0
光讲设计没用,得看数据。我给项目搭了一套多维审计:用一批各类真实问题跑完整链路,再用独立严格评审逐条判"展示的每条是不是真相关",把它做成回归测试。
一轮优化的战果
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 召回率(recall) | 60% | 80% |
| 精确率(clean,展示内容全部真相关) | 80% | 100% |
| 无关文档误报 | 13 条 | 0 条 |
13 条误报是怎么归零的(诚实拆解)
这里我要诚实说:误报从 13 降到 0,不是判官+复核一家之功,是三件套合力的结果:
| 手段 | 干掉的误报数 | 说明 |
|---|---|---|
| 砍掉"读不到正文的相关文件"展示 | 9 条 | 13 条里 9 条是 PDF/Office,只凭标题相似度误判 |
| LLM 判官过滤 | ~3 条 | 0~1 分的直接挡掉 |
| 对抗式复核 | ~1 条 | 压线 2 分的心软误报 |
📌 关键认知:最大的提升来自一个反直觉的决策——砍掉一条展示路径(只展示能按正文验证相关性的内容)。有时候反幻觉不是加算法,而是做减法。这个教训我会在后续文章里专门展开。
七、完整链路代码(可直接抄的结构)
这是我项目里精排这一段的核心代码(精简到 30 行,带注释):
// 第一层:向量把候选切块、按语义排序,取出每篇"与问题最相关的那一块"(_anchorText)
const ranked = await rankDocs(cleanQ, llmDocs, {
embedConfig, topChunks: Math.max(24, readPool * 3), vectorFloor: minScore
});
// 第二层:LLM 判官逐篇打 0~3 分(交给判官的就是每篇的 _anchorText)
const judgeItems = ranked.docs.map(d => ({ text: d._anchorText || d.content }));
const scores = await judgeRelevance(cleanQ, judgeItems, provider);
const scored = ranked.docs.map((d, i) => ({ d, score: scores[i] ?? 3 }));
// 第三层:对抗式复核——只对"刚好压线 2 分"的边界档换怀疑者严判
if (config.adversarial !== false) {
const borderline = scored.filter(x => x.score === judgeMin); // 只挑压线档
if (borderline.length) {
const verdicts = await verifyRelevance(cleanQ,
borderline.map(x => ({ text: x.d._anchorText })), provider);
borderline.forEach((x, i) => {
if (verdicts[i] === false) { x.score = judgeMin - 1; x.rechecked = true; }
});
}
}
// 第四层:只留复核后 ≥2 分的;全低分 → 留空(fail-closed,不硬塞)
const kept = scored.filter(x => x.score >= judgeMin);
llmDocs = kept.map(x => x.d);
// fail-closed 兜底:精排任何环节报错,都绝不"把召回的全部放出来"
// } catch (_e) { llmDocs = ranked.docs?.slice(0,5) ?? []; }
四件事一气呵成:向量粗排 → 判官精排 → 边界档复核 → 阈值过滤。
八、几个你可能想问的问题
Q1:判官要调一次 LLM,不会很慢很贵吗?
会,但有解。① 判官只判候选里"每篇的最相关那块"(doc 级判定),不是所有 chunk,数量可控(通常 5~12 篇)。② 打分结果按 model+sha1(问题+段落) 做缓存,重复问题秒出。③ 复核只对压线档,不是全量。
Q2:为什么不直接用 rerank 模型(如 bge-reranker)?
可以,但 rerank 还是基于相似度打分,本质上还是"看脸",遇到"字面像但没答上"的干扰项照样翻车。LLM 判官是基于推理判断"能不能答上",维度更高。实测判官比通用 rerank 准得多。
Q3:阈值到底怎么定?
我的 minScore 实测定在 0.35(代码默认值;文档里写的 0.40 是旧版)。但关键认知是:向量阈值只管粗筛别漏,真正的"相关判定"交给判官。所以向量阈值宁可低一点(0.3~0.4),让候选都进判官,判官再严判。
Q4:这套方案只适合有 LLM API 的项目吧?
对,判官和复核都要调 LLM。但成本可控——按上面 Q1 的优化,一次问答大概多 1~2 次 LLM 调用(判官 1 次 + 复核 0~1 次),相比生成的 token 成本,判官那点开销很值。
九、总结:记住这三条
| # | 要点 | 一句话 |
|---|---|---|
| 1 | 向量只排序,不判定 | 分不清相关无关,别拿相似度当阈值 |
| 2 | LLM 判官 + 边界复核 | 判官评证据价值,复核专治压线档 |
| 3 | fail-closed 宁可留空 | 找不到比找错强,别给大模型递毒药 |
整条链路的核心就一句话:
向量广撒网别漏,判官严把关别错,复核专治模棱两可,失败就留空——宁可找不到,也不能找错。
名词速查表
| 术语 | 一句话解释 |
|---|---|
| RAG | 检索增强生成,先查文档再回答 |
| 判官(Judge) | LLM 给候选段落打 0~3 分,评"证据价值" |
| 证据价值 | 是否提供可回答问题的信息(不是话题重合度) |
| 对抗式复核 | 对压线 2 分的边界档换"怀疑者"严判一遍 |
| fail-closed | 失败时宁可留空,不硬塞不确定的内容 |
| minScore | 向量粗筛阈值,只管"别漏",不管"判定" |
| judgeMin | 判官入选阈值(默认 2 分),低于此分丢弃 |
| 证据价值重叠区 | 真相关和假相关在向量分数上的重叠区间 |
| _anchorText | 每篇文档里与问题最相关的那一块原文 |
| 多维审计 | 真实问题跑全链路 + 独立评审,做成回归测试 |
写在最后
RAG 反幻觉这件事,最反直觉的一个认知是:幻觉的根源往往不在大模型,而在你喂给它什么。你喂它一堆"看着像但没用"的段落,它当然会编——它只是在尽力完成"基于上下文回答"的任务。
判官工序的本质,是在"召回"和"生成"之间,加一道人类审核工序:不是看像不像,而是看能不能用。这恰恰是生产系统和 demo 的分水岭——demo 追求"能答出来",生产追求"答得对、答不上来就老实说不知道"。
这套方案我已经在开源项目 feishu-kb-qa 里用生产级代码完整实现了(飞书知识库 QA 系统,带权限隔离、多路召回、流式生成)。判官和复核的完整 prompt、动态阈值、fail-closed 兜底,都能在源码里看到。
🔧 源码获取:项目还在整理中,暂时没传 GitHub。想要源码的读者,可以文末扫码关注公众号,留言"RAG 源码",我会发给你。
📚 系列文章:这是《AI 大模型》专栏的实战篇,上一篇讲了 RAG 全链路原理,这一篇讲反幻觉,后续会拆解项目里的切分、检索、产品化设计。
如果觉得有用,欢迎点赞收藏,做项目时随时回查。下一篇会讲 《别照抄 0.7:我的 RAG 阈值是怎么用真实数据标定出来的》,感兴趣可以关注。

关注公众号,获取源码和后续连载
👇 想要 feishu-kb-qa 完整源码,或者持续追更这个 RAG 实战系列?欢迎关注公众号,留言 “RAG 源码” 即可获取。

公众号会第一时间发布系列连载、踩坑记录和源码更新,也欢迎来交流 RAG / 大模型应用开发的问题。
更多推荐



所有评论(0)