大模型又幻觉了?因为你少了一道“判官“工序

发布时间:2026/7/22 20:27:56
大模型又幻觉了?因为你少了一道“判官“工序 你搭好了 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-KLLM 判官逐篇打 0~3 分兜底无fail-closed,宁可留空不硬塞结果分不清字面像和真相关只有真能回答的才喂给大模型一句话总结这条链路的设计哲学:心法:检索是核心,宁可找不到、也不能找错。向量负责广撒网别漏(recall),判官负责严把关别错(precision)。两件事分工,各管一头。三、判官怎么打分0~3 分的证据价值标准判官不是让 LLM 随便说相关/不相关,而是给它一套严格的 0~3 分评分标准——评的是证据价值,不是话题重合度。这是我项目里判官 prompt 的核心(逐字摘自源码):分数含义3直接且充分地回答了问题2含与问题直接相关的实质信息,可支撑部分回答1仅同主题/同领域,但不含可回答的信息(如复述问题、只表达相同诉求、仅有标题)0与问题无关,或属于另一主题这里有两条规则,是整套设计的灵魂:关键认知 1:评分标准是是否提供了可回答问题的信息,而不是词语或话题重合度。关键认知 2:不确定是否含有效信息时,取较低分(宁可误杀,不可错放)。▲ 图 3 判官看的是答得上答不上,不是像不像——一个像看脸,一个像看身份证为什么证据价值比相似度准举个真实例子。用户问:“实时电价波动怎么分析?”候选段落向量相似度判官打分为什么《负电价出现的场景和影响》正文0.5853直接回答了电价波动《电力现货市场未来技术需求.pdf》0.5210只是同领域,根本没回答波动分析一条聊天记录现在电价多少?0.561只是追问同一问题,不是答案看第三条——向量相似度 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)constrankedawaitrankDocs(cleanQ,llmDocs,{embedConfig,topChunks:Math.max(24,readPool*3),vectorFloor:minScore});// 第二层:LLM 判官逐篇打 0~3 分(交给判官的就是每篇的 _anchorText)constjudgeItemsranked.docs.map(d({text:d._anchorText||d.content}));constscoresawaitjudgeRelevance(cleanQ,judgeItems,provider);constscoredranked.docs.map((d,i)({d,score:scores[i]??3}));// 第三层:对抗式复核——只对刚好压线 2 分的边界档换怀疑者严判if(config.adversarial!false){constborderlinescored.filter(xx.scorejudgeMin);// 只挑压线档if(borderline.length){constverdictsawaitverifyRelevance(cleanQ,borderline.map(x({text:x.d._anchorText})),provider);borderline.forEach((x,i){if(verdicts[i]false){x.scorejudgeMin-1;x.recheckedtrue;}});}}// 第四层:只留复核后 ≥2 分的;全低分 → 留空(fail-closed,不硬塞)constkeptscored.filter(xx.scorejudgeMin);llmDocskept.map(xx.d);// fail-closed 兜底:精排任何环节报错,都绝不把召回的全部放出来// } catch (_e) { llmDocs ranked.docs?.slice(0,5) ?? []; }四件事一气呵成:向量粗排 → 判官精排 → 边界档复核 → 阈值过滤。八、几个你可能想问的问题Q1:判官要调一次 LLM,不会很慢很贵吗?会,但有解。① 判官只判候选里每篇的最相关那块(doc 级判定),不是所有 chunk,数量可控(通常 5~12 篇)。② 打分结果按modelsha1(问题段落)做缓存,重复问题秒出。③ 复核只对压线档,不是全量。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向量只排序,不判定分不清相关无关,别拿相似度当阈值2LLM 判官 边界复核判官评证据价值,复核专治压线档3fail-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 / 大模型应用开发的问题。