幻觉机理与根因诊断
幻觉不是一个单一标签。真正难的是:它到底是预训练统计偏差、检索链路漏召回、上下文拼接错误、推理中途偏航,还是安全对齐导致的保守误答。本章从数学推导、信息论、Transformer 源码、RAG 链路拆解、对齐训练副作用五个维度,把"为什么会编"拆到可以精准归因、精准修复的粒度。只有把根因拆清楚,测试结论才有修复价值,否则报告只会停留在"这里回答错了"。
教学导读
**定位:**这一章不是重复"怎么测幻觉",而是从数学、源码和工程三个层面把幻觉从结果标签拆成可以追根因、做治理、看趋势的缺陷体系。 **前置依赖:**建议已学习 RAG、推理测试和基础幻觉检测方法;了解交叉熵、softmax 等基础概念。 **适用场景:**知识库问答、事实问答、推理任务、工具增强生成、安全边界分析、企业 Agent 系统。 **学完产出:**你应该能把一个错误回答拆成事实幻觉、grounding miss、推理偏航或工具误用,给出精确的根因层级标签和修复建议,并能搭建自动化的幻觉检测 pipeline。
先做一个纠偏。
"幻觉"这个词太常用了,以至于大家容易把所有错误答案都叫幻觉。实际上,这会让修复方向变模糊。对测试来说,更好的做法是把错误拆成事实编造、证据未使用、证据混合、工具误用、推理偏航、记忆捏造等更细的类型。一个"有幻觉"的标签对研发没有任何定位价值,等于告诉医生"病人不舒服"但不说哪里不舒服。
01. 幻觉的定义与学术脉络
在自然语言生成(NLG)领域,"幻觉"(Hallucination)一词最早在摘要生成任务中被系统性使用。Maynez 等人(2020)在 ACL 论文中将幻觉定义为:生成内容中出现了在源文本中没有依据支撑的信息。随着大语言模型(LLM)的能力跃升,幻觉的定义也从"偏离源文本"扩展到"偏离可验证的世界事实"。
1.1 从 GPT-2 到 GPT-4:幻觉研究的演进
| 阶段 | 代表模型 | 幻觉特征 | 研究焦点 |
|---|---|---|---|
| 2019-2020 | GPT-2, BART | 高频胡言乱语,语法错误多 | 摘要忠实性、Factual Consistency |
| 2020-2022 | GPT-3, T5-XXL | 语法正确但事实编造,"流畅错"出现 | FaithDial、QA 事实核查、知识冲突 |
| 2022-2023 | ChatGPT, GPT-3.5 | 高度流畅的幻觉,用户难以分辨 | TruthfulQA、HaluEval、RLHF 与幻觉 |
| 2023-2024 | GPT-4, Claude 3 | 幻觉率降低但更隐蔽,推理幻觉增多 | 长链推理幻觉、RAG grounding、对齐税 |
| 2024-2026 | GPT-4o, o1/o3 | 思维链中的隐式幻觉、工具调用幻觉 | CoT 忠实性、Agent 幻觉、多模态幻觉 |
1.2 两种基础定义
内在幻觉(Intrinsic Hallucination)
生成内容与源输入/证据直接矛盾。例如:文档说"试用期不享受年假",模型回答"试用期可以休年假"。这类幻觉最容易通过自动化检测发现,因为存在明确的证据冲突。
外在幻觉(Extrinsic Hallucination)
生成内容在源输入中既没有支持也没有反驳的依据,是模型自行"添加"的信息。例如:问"公司差旅政策",模型除了正确内容外还加了一句"通常国际差旅有额外补贴"——文档中根本没提。这类幻觉更难检测,因为它可能碰巧正确。
为什么区分内在/外在很重要?
内在幻觉的修复方向是加强 grounding 约束和引用机制;外在幻觉的修复方向是限制模型在证据之外的生成自由度(如 constrained decoding、拒答策略)。测试时如果不区分这两类,修复动作会打到错误的层上。
1.3 重要学术基准
| 基准 | 年份 | 核心思路 | 覆盖维度 |
|---|---|---|---|
| TruthfulQA | 2022 | 817 道设计让模型犯错的题,检测模型是否会输出常见误解 | 事实性 |
| HaluEval | 2023 | 自动生成的大规模幻觉评测集,涵盖 QA/对话/摘要 | 多任务幻觉 |
| FaithDial | 2022 | 对话场景中的忠实性标注,评估回答是否忠于知识 | 对话 grounding |
| FELM | 2023 | 从世界知识、数学、推理、代码多维度标注幻觉 | 多领域细粒度 |
| RAGTruth | 2024 | 专门针对 RAG 场景的幻觉标注 | 检索增强幻觉 |
02. 幻觉分类体系
建立一套足够细粒度的分类体系(taxonomy)是幻觉治理的基础。粗粒度的"有幻觉/无幻觉"标签对修复没有任何指导作用,就像把所有疾病都标记为"不健康"一样。
| 一级类型 | 二级类型 | 表面现象 | 常见根因 | 修复方向 |
|---|---|---|---|---|
| 事实幻觉 | 实体编造 | 捏造不存在的人名、机构、产品 | 预训练数据中的高频模式迁移 | 证据约束、拒答策略 |
| 事实幻觉 | 数值编造 | 虚构数字、日期、金额、百分比 | 缺少结构化数据锚点 | 工具查询、数据库绑定 |
| 事实幻觉 | 关系编造 | 错误描述实体间关系(如任职、隶属) | 知识图谱缺失或过期 | 知识库更新、时效性标注 |
| Grounding 失败 | 证据忽视 | 证据在窗口中但回答未引用 | 注意力稀释、位置偏差 | 引用机制、强制 citation |
| Grounding 失败 | 证据曲解 | 引用了证据但意思解读错误 | 语义理解错误、否定句处理弱 | 提示工程、fine-tune |
| Grounding 失败 | 证据混拼 / 多源冲突 | 将不同文档的矛盾信息糅合 | 检索返回冲突文档,无冲突解决机制 | 冲突检测、来源隔离 |
| Grounding 失败 | 版本混淆 | 将新旧版本政策混为一谈 | 文档缺少时效性元数据 | 时间戳过滤、版本标记 |
| 推理偏航 | 中间步骤错误 | 推理链某一步出错导致连锁错误 | token 级别的错误积累与自放大 | 中间步骤验证、自一致性 |
| 推理偏航 | 逻辑跳跃 | 跳过必要推理步骤直接得出结论 | 训练数据中的快捷推理模式 | 强制展示推理过程 |
| 工具误用 | 调用错误/参数错误 | 该查工具时没查,或查了却错误解释 | 规划与执行脱节 | 工具调用验证、参数校验 |
| 记忆捏造 | 虚构历史对话 | 声称用户之前说过实际没说过的话 | 长对话上下文管理失败 | 会话状态追踪 |
2.1 企业里最容易混淆的场景
| 业务场景 | 用户看到的错误 | 真正标签 | 为什么不能都叫"幻觉" |
|---|---|---|---|
| 物流客服 | 把"已揽收"说成"已签收" | 事实幻觉 / 工具误读 | 可能是没查单,也可能是查到了却解释错了——修复路径完全不同 |
| HR 政策问答 | 把试用期年假说成可享受 | grounding miss | 知识库明明有规则,问题在证据没被用上 |
| 财务制度助手 | 把差旅报销上限从 800 编成 1000 | 事实幻觉 / 版本冲突 | 既可能是编造,也可能是引用了过期制度 |
| 销售 Agent | 承诺"明天一定到货" | 证据外延伸 | 库存和物流证据只支持"预计",不支持"保证" |
| 医疗问诊助手 | 建议"停药后可以正常饮酒" | 推理偏航 + 安全幻觉 | 部分药物有禁忌,模型推理时忽略了相互作用 |
| 法律合同审查 | 将"甲方应当"解读为"甲方可以" | 证据曲解 | "应当"和"可以"在法律语境下语义完全不同 |
测试视角最重要的动作是拆标签。
同样是"答错",修复动作完全不同。事实幻觉更可能需要证据约束和校验;grounding miss 更可能要回到召回、上下文拼接和引用;工具误用则要查 Agent 轨迹与参数层。标签拆不细,修复就会一直打空拳。建议企业至少维护 6-8 个二级标签,并配合根因层级标注。
03. 语言模型生成机理:为什么 next-token 天然会"编"
要理解幻觉,必须先理解语言模型的训练目标和生成过程。模型优化的是"在当前上下文下,哪个 token 最像会出现",不是"哪个 token 最符合现实世界"。这是一个根本性的目标错位,理解这一点是理解所有幻觉现象的起点。
3.1 交叉熵损失与最大似然的完整数学推导
语言模型的训练目标是最大化训练语料上的对数似然: 最大似然目标:max_θ Σ_{i=1}^{N} Σ_{t=1}^{T_i} log P_θ(x_t^{(i)} | x_{<t}^{(i)}) 等价地,最小化负对数似然(即交叉熵损失): L(θ) = - 1/(N·T) Σ_{i=1}^{N} Σ_{t=1}^{T_i} log P_θ(x_t^{(i)} | x_{<t}^{(i)}) 这个损失函数的物理含义是:对于训练集中的每个位置,模型需要把该位置实际出现的 token 的预测概率推高。但这里有一个关键的隐含假设——训练集中出现的就是"正确"的。模型无法区分语料中的事实陈述和虚假信息。 在 softmax 层,模型输出的概率分布为: P_θ(x_t = v | x_{<t}) = exp(z_v) / Σ_{v'∈V} exp(z_{v'})
其中 z_v = W_v · h_t + b_v z_v 是 token v 的 logit 值 h_t 是位置 t 的隐层表示 W 是语言模型头的权重矩阵 V 是词表大小(通常 32k-128k) 交叉熵损失对 logit 的梯度为: ∂L/∂z_v = P_θ(v | x_{<t}) - 𝟙[v = x_t]
当 v 是正确 token 时:梯度 = P(v) - 1,推动概率向 1 当 v 不是正确 token 时:梯度 = P(v) - 0,推动概率向 0
关键洞察:梯度只关心"匹配训练数据"。
梯度更新只会让模型更好地预测训练语料中下一个实际出现的 token。如果训练语料中"员工可以享受年假"这个模式出现了 1000 次,而"试用期员工不享受年假"只出现了 10 次,那么在"员工...年假"这个上下文模式下,模型自然会偏向生成"可以享受"。这不是 bug,而是最大似然训练的必然结果。
3.2 自回归生成的概率链与条件独立性假设
自回归生成的核心是链式分解: P(x_1, x_2, ..., x_T) = Π_{t=1}^{T} P(x_t | x_1, ..., x_{t-1}) 这个分解是精确的——不需要任何条件独立性假设。但在实际生成(推理)过程中,存在两个工程层面的问题: 问题一:误差积累。生成是顺序进行的,第 t 步的输入包括前面所有已生成的 token。一旦第 k 步生成了一个不准确的 token,从第 k+1 步开始,模型看到的上下文就已经偏离了"正确轨道"。 错误传播: P(x_T | x_{<T}) 的条件包含了所有前序 token 若 x_k 有误 → x_{k+1} 基于错误前提生成 → ... → x_T 在错误链上继续
类比:在高速公路上,如果你在第 3 个出口拐错了, 后面再怎么遵守交通规则,也到不了正确目的地。 问题二:曝光偏差(Exposure Bias)。训练时模型看到的始终是真实语料(teacher forcing),但推理时模型看到的是自己之前生成的 token。训练和推理之间的这种输入分布差异被称为曝光偏差,它会放大错误传播的效果。
训练时: 推理时:
输入: [真实 token 1, 真实 token 2] 输入: [生成 token 1, 生成 token 2]
预测: token 3 预测: token 3
训练时模型从不需要处理自己的错误输出,
但推理时它必须在自己可能错误的输出上继续生成。
这就像一个学生考试时从来没做过错题练习,
但真正考试时一旦前面答错,后面全部受影响。3.3 "流畅错"的信息论解释
为什么幻觉往往听起来特别流畅、特别像真的?这个问题可以用信息论来解释。 困惑度(Perplexity)与幻觉的反直觉关系 PPL(x) = exp(-1/T · Σ_{t=1}^{T} log P(x_t | x_{<t}))
困惑度衡量模型对序列的"惊讶度":
- PPL 低 → 模型认为这个序列很"正常"
- PPL 高 → 模型认为这个序列很"意外"
关键反直觉点: 幻觉文本的困惑度往往比真实但罕见的事实更低! 假设一个模型被问到"诺贝尔物理学奖 2023 年授予了谁",正确答案涉及三个不太常见的名字(Pierre Agostini、Ferenc Krausz、Anne L'Huillier)。模型如果生成一个编造但流畅的回答"2023 年诺贝尔物理学奖授予了量子计算领域的突破性贡献",这段文本的困惑度可能比包含三个罕见人名的正确答案更低。 KL 散度视角 D_KL(P_real || P_model) = Σ_x P_real(x) · log(P_real(x) / P_model(x))
P_real:现实世界的事实分布 P_model:模型学到的分布
幻觉的信息论本质 = 模型分布 P_model 与真实世界分布 P_real 之间的 KL 散度不为零
在高 KL 散度区域:
- P_model 赋予高概率但 P_real 赋予低概率 → 模型自信地说错话
- P_real 赋予高概率但 P_model 赋予低概率 → 模型遗漏重要事实 更具体地,我们可以将生成空间划分为四个象限:
| P_model 高 | P_model 低 | |
|---|---|---|
| P_real 高 | 正确且流畅(理想状态) | 遗漏事实(模型不知道该说) |
| P_real 低 | 流畅幻觉(最危险象限) | 低频废话(容易被过滤) |
最危险的象限:P_model 高 × P_real 低。
这个象限的内容模型生成得非常自信、非常流畅,但在现实世界中是错误的。传统的困惑度过滤完全无法捕获这类错误,因为它们的困惑度反而很低。这就是为什么"流畅错"是幻觉检测中最顽固的难题。
3.4 训练数据分布偏差如何量化导致幻觉
训练数据的分布偏差是幻觉的根源之一。如果某类信息在训练数据中出现频率远高于其在真实世界中的频率,模型就会产生系统性偏差。
量化训练偏差对幻觉影响的思路:
设 f_train(pattern) = 模式在训练集中的出现频率
f_real(pattern) = 模式在现实世界中的真实频率
偏差系数 β = f_train(pattern) / f_real(pattern)
当 β >> 1 时(训练中过度代表):
模型倾向于过度生成该模式,即使上下文不支持
例:训练数据中"公司成立于2015年"出现100次
但"公司成立于2017年"只出现2次
即使证据说2017年,模型也可能输出2015年
当 β << 1 时(训练中欠代表):
模型倾向于回避该模式,可能用更"常见"的表述替代
例:罕见疾病名称、小众地名一个实验可以验证这个假说:取一组已知的事实 QA 对,统计正确答案中关键实体在训练数据(如 Common Crawl)中的出现频率,然后观察模型在这些问题上的幻觉率是否与频率负相关。多项研究已经验证了这种相关性——低频事实的幻觉率显著高于高频事实。
3.5 手撕推导:自放大效应
将上述理论应用到一个具体例子。假设用户问"试用期员工是否享受年假",上下文里没有制度原文。
t=1: "试用期员工是否..."
候选 token 概率:
"可" 0.42 ← 训练语料中"员工可以..."模式占优
"不" 0.28 ← 正确答案需要的分支
"需" 0.10
t=2: 如果上一 token 选了"可" → P("以" | "...可") = 0.81
t=3: "可以" → P("享" | "...可以") = 0.73
t=4: "可以享" → P("受" | "...可以享") = 0.92
自放大链条:
0.42 × 0.81 × 0.73 × 0.92 = 0.229("可以享受"路径)
而正确路径:
P("不") × P("享" | "不") × P("受" | "不享") = 0.28 × 0.35 × 0.88 = 0.086
错误路径的联合概率是正确路径的 2.7 倍!
这就是为什么模型会"自信地犯错"——
每一步的局部最优选择累积成全局错误。这也是为什么幻觉特别危险。
它通常不是胡言乱语,而是"像真的一样"。流畅度越高,用户越容易信;测试如果只做语义通顺检查,很容易误判通过。更危险的是,beam search 等解码策略实际上会加剧这个问题,因为它们专门寻找高概率路径——而流畅的幻觉恰恰就在高概率路径上。
04. RAG 系统中的幻觉机理
很多团队以为只要加了检索,幻觉问题就自动解决。实际上,RAG 只是给了"可被引用的证据",并没有强制模型必须正确使用这些证据。RAG 引入了一条完整的链路,每个环节都可能产生新的幻觉模式。
4.1 检索链路每一层的失败模式
一个完整的 RAG 链路包括:Query 理解 → Embedding → ANN 检索 → Rerank → 窗口拼接 → 生成。每一层都有特定的失败模式:
Embedding 层失败
语义漂移:查询和文档的 embedding 在语义空间中距离远但实际相关。典型场景:否定句("不能报销"vs"报销规则")、同义替换("辞退"vs"解除劳动合同")、跨语言术语。诊断方法:对比 query-doc 的余弦相似度与人工相关性标注。
ANN 近似搜索失败
召回遗漏:HNSW/IVF 等近似最近邻算法为了速度牺牲精度,可能在高维空间中漏掉真正相关的文档。诊断方法:对比 ANN 结果与暴力精确搜索结果,计算 recall@k。当 recall@k 低于 0.95 时需要调参。
Rerank 层失败
排序逆转:Cross-encoder reranker 可能因训练数据偏差将相关文档排到低位。长文档段落可能因长度惩罚被压低。诊断方法:检查 rerank 前后 gold document 排名变化。
窗口拼接层失败
截断关键信息:chunk 切分时可能把一个完整规则切成两半,只有一半进入上下文窗口。诊断方法:对比 chunk 边界与答案所需信息的位置关系。
4.2 上下文注意力稀释——Lost in the Middle
Liu et al. (2023) 的论文 "Lost in the Middle" 揭示了一个重要现象:当相关信息被放在长上下文的中间位置时,模型的利用率显著下降。 注意力稀释的数学直觉:
对于位置 t 的 attention weight: a_{t,s} = softmax(Q_t · K_s / √d_k) 对所有位置 s
当上下文长度 L 增大时:
- softmax 的分母 Σ exp(...) 增大
- 每个位置分到的注意力权重被"稀释"
- 中间位置的文档既没有开头的"首因效应" 也没有结尾的"近因效应",最容易被忽视
实验数据呈现的典型 U 形曲线:
文档位置: 1 2 3 4 5 6 7 8 9 10
准确率(%): 87 78 71 64 59 58 62 69 75 84
最佳位置: 开头(87%)和结尾(84%)
最差位置: 中间偏后(58%)
差距: 开头比中间高 29 个百分点
测试建议:
1. 将 gold document 分别放在开头/中间/结尾测试
2. 增加噪声文档数量,观察准确率衰减曲线
3. 在 top-k 大于 5 时强制关注位置覆盖率4.3 多文档冲突的融合策略与测试方法
当检索返回的多个文档包含矛盾信息时,模型缺乏可靠的冲突解决机制。
| 冲突类型 | 示例 | 模型常见行为 | 正确处理方式 |
|---|---|---|---|
| 时间版本冲突 | 2023版制度说上限800,2024版说1000 | 随机选一个或取平均 | 应以最新版本为准并注明 |
| 来源权威冲突 | 公司通知说A,行业标准说B | 混合两者 | 应按优先级排序并说明依据 |
| 范围覆盖冲突 | 总部政策 vs 分部特殊政策 | 只引用其中一个 | 应识别适用范围 |
| 隐式矛盾 | 两段话单独都对,合在一起矛盾 | 无法识别矛盾 | 应对比关键声明 |
多文档冲突测试设计原则
在构造测试用例时,至少要覆盖三类场景:(1) 两个来源直接矛盾,期望模型识别并说明冲突;(2) 两个来源部分矛盾,期望模型融合时标注不确定区域;(3) 两个来源看似矛盾但适用范围不同,期望模型正确区分。每类场景分别记录 conflict_detection_rate 和 resolution_accuracy。
4.4 完整 RAG 幻觉检测 pipeline
"""
RAG 幻觉检测 Pipeline
检测维度:召回完整性 × 证据利用度 × 事实一致性 × 边界合规性
"""
import json
from dataclasses import dataclass, field, asdict
from typing import Optional
@dataclass
class RAGHallucinationReport:
case_id: str
query: str
retrieval_recall: float # gold docs 被召回的比例
evidence_utilization: float # 被召回证据中被实际使用的比例
factual_consistency: float # 回答与证据的一致性得分
boundary_compliance: bool # 是否超出证据边界
hallucination_type: str = "" # 分类标签
root_cause_layer: str = "" # 根因层级
details: dict = field(default_factory=dict)
def check_retrieval_recall(retrieved_ids: list, gold_ids: list) -> float:
if not gold_ids:
return 1.0
hit = len(set(retrieved_ids) & set(gold_ids))
return hit / len(gold_ids)
def check_evidence_utilization(answer: str, evidence_chunks: list,
judge_fn) -> float:
used_count = 0
for chunk in evidence_chunks:
prompt = f"""判断以下回答是否引用或使用了这段证据的信息。
只需回答 yes 或 no。
【证据】
{chunk}
【回答】
{answer}"""
result = judge_fn(prompt).strip().lower()
if "yes" in result:
used_count += 1
return used_count / len(evidence_chunks) if evidence_chunks else 0
def check_factual_consistency(answer: str, evidence: str,
judge_fn) -> dict:
prompt = f"""逐句检查回答中的每个声明是否被证据支持。
【证据】
{evidence}
【回答】
{answer}
请输出 JSON:
{{"score": 0.0-1.0, "unsupported_claims": ["..."], "contradictions": ["..."]}}"""
return json.loads(judge_fn(prompt))
def check_boundary_compliance(answer: str, evidence: str,
judge_fn) -> dict:
prompt = f"""判断回答是否超出证据边界,即包含证据中没有依据的新信息。
【证据】
{evidence}
【回答】
{answer}
请输出 JSON:
{{"within_boundary": true/false, "extra_claims": ["..."], "reason": "..."}}"""
return json.loads(judge_fn(prompt))
def classify_rag_hallucination(report: RAGHallucinationReport) -> str:
if report.retrieval_recall < 0.5:
return "retrieval_miss"
if report.retrieval_recall >= 0.5 and report.evidence_utilization < 0.3:
return "grounding_miss"
if not report.boundary_compliance:
return "unsupported_extension"
if report.factual_consistency < 0.7:
return "evidence_distortion"
return "no_hallucination"
def run_rag_hallucination_pipeline(case: dict, judge_fn) -> dict:
"""
输入 case 格式:
{
"case_id": "rag-001",
"query": "...",
"retrieved_doc_ids": ["d1", "d2"],
"gold_doc_ids": ["d1", "d3"],
"evidence_chunks": ["chunk1 text", "chunk2 text"],
"combined_evidence": "all evidence combined",
"model_answer": "..."
}
"""
recall = check_retrieval_recall(
case["retrieved_doc_ids"], case["gold_doc_ids"])
utilization = check_evidence_utilization(
case["model_answer"], case["evidence_chunks"], judge_fn)
consistency = check_factual_consistency(
case["model_answer"], case["combined_evidence"], judge_fn)
boundary = check_boundary_compliance(
case["model_answer"], case["combined_evidence"], judge_fn)
report = RAGHallucinationReport(
case_id=case["case_id"],
query=case["query"],
retrieval_recall=recall,
evidence_utilization=utilization,
factual_consistency=consistency.get("score", 0),
boundary_compliance=boundary.get("within_boundary", True),
details={
"unsupported_claims": consistency.get("unsupported_claims", []),
"contradictions": consistency.get("contradictions", []),
"extra_claims": boundary.get("extra_claims", []),
}
)
report.hallucination_type = classify_rag_hallucination(report)
report.root_cause_layer = {
"retrieval_miss": "retrieval",
"grounding_miss": "generation",
"unsupported_extension": "generation",
"evidence_distortion": "generation",
"no_hallucination": "none"
}.get(report.hallucination_type, "unknown")
return asdict(report)05. 推理链偏航的数学模型
在多步推理或 Agent 任务里,错误常常不是终点才发生,而是在中间某个判断点就偏了。之后每一步都建立在错误前提上,最终输出看起来却依旧很顺。
5.1 错误传播的马尔可夫链分析
将推理链建模为一个马尔可夫链,每一步要么正确(状态 C)要么错误(状态 E): 状态转移矩阵: C(正确) E(错误) C(正确) [ 1-p p ] E(错误) [ q 1-q ]
p = 从正确状态跳到错误状态的概率(单步出错率) q = 从错误状态恢复到正确状态的概率(自我纠正率)
对于 n 步推理链,最终正确的概率: P(最终正确) = π_C · (1-p)^n + 修正项
当 q ≈ 0(模型几乎不会自我纠正)时: P(n步全对) ≈ (1-p)^n
数值示例: p = 0.05(每步 5% 出错率) n = 10 步推理 P(全对) = 0.95^10 = 0.599
即使每步只有 5% 的出错率, 10 步推理后正确率就降到只有 60%! 20 步推理:0.95^20 = 0.358(正确率仅 36%)
自我纠正率 q 在实践中非常低。
多项研究表明,语言模型在没有外部反馈的情况下,自我纠正能力很弱。一旦推理链进入错误状态,模型倾向于在错误前提上继续推理(因为它追求局部流畅性),而不是回溯检查之前的步骤。这是因为自回归生成无法"回头"——已经生成的 token 成为既定上下文。
5.2 CoT 中的逻辑跳跃检测方法
逻辑跳跃是指推理链中省略了必要的中间步骤,直接从前提跳到结论。这种跳跃可能导致隐蔽的错误。
"""CoT 逻辑跳跃检测器"""
def detect_logic_jumps(cot_text: str, judge_fn) -> dict:
prompt = f"""分析以下思维链推理过程,检测是否存在逻辑跳跃。
逻辑跳跃的定义:
1. 步骤间缺少必要的中间推导
2. 结论不能仅从前提直接推出
3. 引入了前文未提及的隐含假设
4. 数值计算跳过了中间步骤
【思维链】
{cot_text}
请输出 JSON:
{{
"total_steps": 步骤数,
"jumps": [
{{
"from_step": 跳跃起始步骤号,
"to_step": 跳跃目标步骤号,
"missing_reasoning": "缺少的推理内容",
"severity": "high/medium/low",
"could_cause_error": true/false
}}
],
"overall_validity": "valid/suspicious/invalid"
}}"""
return json.loads(judge_fn(prompt))
def validate_reasoning_chain(steps: list, validator_fn) -> list:
"""逐步验证推理链,检查每步结论是否由前提支持"""
issues = []
for i in range(1, len(steps)):
premise = "\n".join(steps[:i])
conclusion = steps[i]
prompt = f"""前提:
{premise}
结论:
{conclusion}
这个结论能否仅从上述前提中逻辑推出?
回答 yes/no 并简述理由。"""
result = validator_fn(prompt)
if "no" in result.lower():
issues.append({
"step": i,
"premise_summary": premise[-200:],
"conclusion": conclusion,
"validator_response": result
})
return issues5.3 HuggingFace Transformers 中 logits 分析源码剖析
要在代码层面理解幻觉,需要深入 Transformers 库中 logits 的生成和采样过程。
"""
logits 分析:从 Transformers 源码理解幻觉的生成位点
核心调用链:
model.generate()
→ GenerationMixin._generate()
→ LogitsProcessorList.__call__() # 对 logits 后处理
→ LogitsWarper (temperature, top-k, top-p)
→ 采样或 greedy 选择下一个 token
"""
import torch
import torch.nn.functional as F
from transformers import AutoModelForCausalLM, AutoTokenizer
def analyze_token_confidence(model, tokenizer, text: str,
top_k: int = 10) -> list:
"""分析每个位置的 top-k token 概率分布,识别低置信度位置"""
inputs = tokenizer(text, return_tensors="pt")
with torch.no_grad():
outputs = model(**inputs)
logits = outputs.logits # shape: [1, seq_len, vocab_size]
analysis = []
for pos in range(logits.shape[1]):
probs = F.softmax(logits[0, pos], dim=-1)
topk_probs, topk_ids = torch.topk(probs, top_k)
entropy = -torch.sum(probs * torch.log(probs + 1e-10)).item()
analysis.append({
"position": pos,
"token": tokenizer.decode(inputs["input_ids"][0, pos]),
"top_tokens": [
{"token": tokenizer.decode(tid), "prob": p.item()}
for tid, p in zip(topk_ids, topk_probs)
],
"entropy": entropy,
"top1_prob": topk_probs[0].item(),
"top1_top2_gap": (topk_probs[0] - topk_probs[1]).item()
})
return analysis
def detect_hallucination_risk_positions(analysis: list,
entropy_threshold: float = 3.0,
confidence_threshold: float = 0.3
) -> list:
"""标记高幻觉风险位置:高熵 + 低置信度"""
risky = []
for item in analysis:
if (item["entropy"] > entropy_threshold
or item["top1_prob"] < confidence_threshold):
risky.append({
"position": item["position"],
"token": item["token"],
"entropy": item["entropy"],
"top1_prob": item["top1_prob"],
"risk_reason": (
"high_entropy" if item["entropy"] > entropy_threshold
else "low_confidence"
)
})
return risky
def compare_greedy_vs_sample(model, tokenizer, prompt: str,
num_samples: int = 5,
max_new_tokens: int = 100) -> dict:
"""对比 greedy 和多次采样的结果,高方差区域是幻觉高风险区"""
inputs = tokenizer(prompt, return_tensors="pt")
# Greedy
greedy_out = model.generate(
**inputs, max_new_tokens=max_new_tokens, do_sample=False)
greedy_text = tokenizer.decode(
greedy_out[0][inputs["input_ids"].shape[1]:],
skip_special_tokens=True)
# Sampling
sample_texts = []
for _ in range(num_samples):
sample_out = model.generate(
**inputs, max_new_tokens=max_new_tokens,
do_sample=True, temperature=0.7, top_p=0.9)
sample_texts.append(tokenizer.decode(
sample_out[0][inputs["input_ids"].shape[1]:],
skip_special_tokens=True))
return {
"greedy": greedy_text,
"samples": sample_texts,
"consistency": _compute_consistency(greedy_text, sample_texts)
}
def _compute_consistency(reference: str, samples: list) -> float:
"""简单的 token 级别一致性计算"""
ref_tokens = set(reference.split())
scores = []
for s in samples:
s_tokens = set(s.split())
if not ref_tokens:
scores.append(0.0)
continue
overlap = len(ref_tokens & s_tokens) / len(ref_tokens | s_tokens)
scores.append(overlap)
return sum(scores) / len(scores) if scores else 0.0实操建议:用熵值定位幻觉高风险 token。
在生成过程中,如果某个位置的 entropy 突然升高(说明模型"不确定"),同时 top-1 概率较低,那么这个位置就是幻觉的高风险点。企业可以在生产环境中实时监控这些指标,当连续多个 token 的熵值超过阈值时触发告警或回退到拒答。
06. 对齐训练与幻觉的关系
对齐训练(RLHF、DPO 等)的初衷是让模型的行为更符合人类偏好。但对齐过程本身可能引入新的幻觉机制,或放大已有的幻觉倾向。这被称为"对齐税"(alignment tax)。
6.1 RLHF 的 Reward Hacking 与幻觉放大
RLHF 的训练过程是:先训练一个 Reward Model(RM)来预测人类偏好,然后用 PPO 等策略优化语言模型以最大化 RM 打分。问题在于 RM 本身是不完美的。 RLHF 目标: max_θ E_{x~D, y~π_θ(·|x)} [R_φ(x, y)] - β · D_KL(π_θ || π_ref)
R_φ: Reward Model 的打分(不完美的代理信号) π_ref: 参考策略(SFT 模型) β: KL 惩罚系数
Reward Hacking 发生条件: 当 R_φ(x, y) 与真实人类偏好 R*(x, y) 不一致时, 模型会优化 R_φ 而非 R*,即"欺骗"奖励模型 具体到幻觉,Reward Hacking 表现为:
- 详细性偏好陷阱:人类标注员倾向于偏好更详细的回答。RM 学到了"详细 = 好"。于是模型为了获得高 reward,会在缺乏证据时添加看似详细的内容——这就是幻觉。
- 自信度偏好陷阱:人类标注员倾向于偏好语气确定的回答。RM 学到了"自信 = 好"。于是模型即使不确定也会给出确定性表述,而不是说"我不确定"。
- 流畅度-准确度权衡:RM 往往对流畅性给予较高权重,因为不流畅的回答在标注时会被系统性降级。这导致模型在"说一句不太流畅但准确的话"和"说一句流畅但不完全准确的话"之间,选择后者。
6.2 DPO 的隐式偏好与幻觉副作用
Direct Preference Optimization(DPO)跳过了显式的 reward model 训练,直接从偏好数据优化策略。但它的隐式 reward 函数同样不完美。 DPO 损失: L_DPO(θ) = -E_{(x,y_w,y_l)~D} [log σ(β · (log π_θ(y_w|x)/π_ref(y_w|x) - log π_θ(y_l|x)/π_ref(y_l|x)))]
y_w: 人类偏好的(winning)回答 y_l: 人类不偏好的(losing)回答
DPO 的隐式 reward: r(x, y) = β · log(π_θ(y|x) / π_ref(y|x)) + β · log Z(x)
问题:如果偏好数据中 y_w 恰好包含幻觉但更"好看", DPO 会直接学到"生成这类幻觉内容"的偏好。
6.3 Constitutional AI 的安全-准确性权衡
Constitutional AI(CAI)使用一组"宪法原则"来引导模型行为,在安全性方面效果显著。但过度的安全约束会导致另一种幻觉——保守型幻觉。
过度拒答
模型把合理的专业问题误判为"危险"而拒绝回答,或给出一个过于模糊的回答。例如:用户问"如何安全地使用消毒剂",模型因为涉及化学品而拒答。
过度对冲
每个回答都加上大量免责声明和"但也有可能...",导致核心信息被稀释。用户得到了一个"什么都说了但什么都没说清"的答案。
安全替代答案
模型生成一个"安全但不准确"的替代答案,而不是承认不确定性。例如:被问到敏感话题时给出一个政治正确但事实偏离的回答。
对齐训练引发幻觉的核心矛盾。
对齐训练的目标是让模型"有用、无害、诚实"(helpful, harmless, honest)。但这三个目标之间存在张力:更有用(更详细)可能增加幻觉风险;更无害(更保守)可能降低有用性;更诚实(承认不确定)可能降低用户满意度。测试时需要分别评估这三个维度,而不是用一个单一指标衡量。
07. 根因诊断框架
前面几章分析了幻觉的各种成因,本章将这些分析组织成一个可操作的诊断框架。核心思想是:别只写"有幻觉",要写清楚幻觉发生在哪一层、根因是什么、修复方向在哪里。
7.1 五层诊断模型
每个幻觉问题都可以沿着以下五层逐层排查,定位到最底层的根因:
| 层级 | 层名 | 诊断问题 | 典型症状 | 检查方法 |
|---|---|---|---|---|
| L1 | 输入层 | 问题本身是否歧义?是否给了足够上下文? | 模型基于猜测回答 | 用清晰版问题重测 |
| L2 | 检索层 | 正确证据是否被检索到?排名是否合理? | 答案与知识库完全无关 | 检查 retrieved docs |
| L3 | 拼接层 | 证据是否正确拼接?是否有截断/冲突? | 答案混合了多源信息 | 检查 prompt context |
| L4 | 生成层 | 模型是否正确使用了上下文中的证据? | 证据在窗口中但被忽视 | citation 分析 |
| L5 | 对齐层 | 是否因安全/对齐策略导致回答偏差? | 过度拒答或过度对冲 | 去除 safety prompt 重测 |
五层诊断的执行流程:
用户问题
│
┌───▼───┐
│ L1:输入│ → 问题是否清晰?上下文是否充分?
│ 歧义? │ 是 → 标记 input_ambiguity,建议优化 prompt
└───┬───┘ 否 ↓
┌───▼───┐
│ L2:检索│ → gold docs 是否被检索到?recall@k 是否达标?
│ 漏召回│ 漏了 → 标记 retrieval_miss,检查 embedding/index
└───┬───┘ 没漏 ↓
┌───▼───┐
│ L3:拼接│ → 证据是否完整进入窗口?是否互相冲突?
│ 截断? │ 有问题 → 标记 context_assembly_error
└───┬───┘ 正常 ↓
┌───▼───┐
│ L4:生成│ → 模型是否引用了证据?是否超出边界?
│ 忽视? │ 忽视 → 标记 grounding_miss
└───┬───┘ 超出 → 标记 unsupported_extension
┌───▼───┐ 正常 ↓
│ L5:对齐│ → 是否因 safety/alignment 导致偏差?
│ 过度? │ 是 → 标记 alignment_overcorrection
└───────┘7.2 自动化根因分类器
"""
自动化幻觉根因分类器
输入:一个 bad case 的完整信息
输出:根因层级、分类标签、修复建议
"""
from dataclasses import dataclass
from enum import Enum
from typing import Optional
class RootCauseLayer(Enum):
INPUT = "L1_input"
RETRIEVAL = "L2_retrieval"
ASSEMBLY = "L3_assembly"
GENERATION = "L4_generation"
ALIGNMENT = "L5_alignment"
UNKNOWN = "unknown"
class HallucinationType(Enum):
INPUT_AMBIGUITY = "input_ambiguity"
RETRIEVAL_MISS = "retrieval_miss"
RETRIEVAL_NOISE = "retrieval_noise"
CONTEXT_TRUNCATION = "context_truncation"
CONTEXT_CONFLICT = "context_conflict"
GROUNDING_MISS = "grounding_miss"
EVIDENCE_DISTORTION = "evidence_distortion"
UNSUPPORTED_EXTENSION = "unsupported_extension"
FACTUAL_FABRICATION = "factual_fabrication"
REASONING_SLIP = "reasoning_slip"
TOOL_MISUSE = "tool_misuse"
ALIGNMENT_OVERCORRECTION = "alignment_overcorrection"
MEMORY_FABRICATION = "memory_fabrication"
@dataclass
class DiagnosisResult:
layer: RootCauseLayer
hallucination_type: HallucinationType
confidence: float
evidence: str
fix_suggestion: str
class HallucinationDiagnoser:
def __init__(self, judge_fn):
self.judge = judge_fn
def diagnose(self, case: dict) -> DiagnosisResult:
"""
case 需包含:
- query: 用户问题
- model_answer: 模型回答
- gold_answer: 正确答案(可选)
- retrieved_docs: 检索到的文档列表
- gold_docs: 应该检索到的文档列表
- context_window: 实际传入模型的上下文
- tool_calls: Agent 工具调用记录(可选)
- has_safety_prompt: 是否使用了安全提示
"""
# L1: 输入层检查
if self._check_input_ambiguity(case):
return DiagnosisResult(
layer=RootCauseLayer.INPUT,
hallucination_type=HallucinationType.INPUT_AMBIGUITY,
confidence=0.8,
evidence="问题存在歧义或缺少必要上下文",
fix_suggestion="优化 prompt 模板,补充必要约束条件"
)
# L2: 检索层检查
retrieval_issue = self._check_retrieval(case)
if retrieval_issue:
return retrieval_issue
# L3: 拼接层检查
assembly_issue = self._check_assembly(case)
if assembly_issue:
return assembly_issue
# L4: 生成层检查
generation_issue = self._check_generation(case)
if generation_issue:
return generation_issue
# L5: 对齐层检查
alignment_issue = self._check_alignment(case)
if alignment_issue:
return alignment_issue
return DiagnosisResult(
layer=RootCauseLayer.UNKNOWN,
hallucination_type=HallucinationType.FACTUAL_FABRICATION,
confidence=0.5,
evidence="未能确定具体根因层",
fix_suggestion="需要人工深入分析"
)
def _check_input_ambiguity(self, case: dict) -> bool:
prompt = f"""判断以下问题是否存在歧义或缺少必要上下文:
问题:{case['query']}
只回答 yes 或 no。"""
return "yes" in self.judge(prompt).lower()
def _check_retrieval(self, case: dict) -> Optional[DiagnosisResult]:
gold = set(case.get("gold_docs", []))
retrieved = set(case.get("retrieved_docs", []))
if not gold:
return None
recall = len(gold & retrieved) / len(gold)
if recall < 0.5:
return DiagnosisResult(
layer=RootCauseLayer.RETRIEVAL,
hallucination_type=HallucinationType.RETRIEVAL_MISS,
confidence=0.9,
evidence=f"关键文档召回率仅 {recall:.0%}",
fix_suggestion="检查 embedding 模型、chunk 策略、索引配置"
)
noise_ratio = len(retrieved - gold) / len(retrieved) if retrieved else 0
if noise_ratio > 0.7:
return DiagnosisResult(
layer=RootCauseLayer.RETRIEVAL,
hallucination_type=HallucinationType.RETRIEVAL_NOISE,
confidence=0.75,
evidence=f"检索噪声比 {noise_ratio:.0%}",
fix_suggestion="优化 reranker、调整 top-k、增加负例训练"
)
return None
def _check_assembly(self, case: dict) -> Optional[DiagnosisResult]:
context = case.get("context_window", "")
gold_answer = case.get("gold_answer", "")
if gold_answer and gold_answer[:50] not in context:
return DiagnosisResult(
layer=RootCauseLayer.ASSEMBLY,
hallucination_type=HallucinationType.CONTEXT_TRUNCATION,
confidence=0.7,
evidence="正确答案的关键信息未完整出现在上下文窗口中",
fix_suggestion="检查 chunk 大小、overlap 设置、窗口长度限制"
)
return None
def _check_generation(self, case: dict) -> Optional[DiagnosisResult]:
result = self.judge(f"""分析模型回答是否正确使用了上下文证据。
上下文:{case.get('context_window', '')[:2000]}
模型回答:{case['model_answer']}
输出 JSON:
{{"used_evidence": true/false, "exceeded_boundary": true/false,
"distorted_evidence": true/false}}""")
import json
try:
analysis = json.loads(result)
except json.JSONDecodeError:
return None
if not analysis.get("used_evidence", True):
return DiagnosisResult(
layer=RootCauseLayer.GENERATION,
hallucination_type=HallucinationType.GROUNDING_MISS,
confidence=0.85,
evidence="证据存在于上下文但未被模型引用",
fix_suggestion="强化引用机制,添加 citation 要求"
)
if analysis.get("exceeded_boundary", False):
return DiagnosisResult(
layer=RootCauseLayer.GENERATION,
hallucination_type=HallucinationType.UNSUPPORTED_EXTENSION,
confidence=0.8,
evidence="回答超出了证据边界",
fix_suggestion="添加边界约束,限制模型在证据范围外的生成"
)
return None
def _check_alignment(self, case: dict) -> Optional[DiagnosisResult]:
if not case.get("has_safety_prompt", False):
return None
prompt = f"""判断以下回答是否因过度安全/保守而偏离了准确性:
回答:{case['model_answer']}
只回答 yes 或 no,并简述理由。"""
result = self.judge(prompt)
if "yes" in result.lower():
return DiagnosisResult(
layer=RootCauseLayer.ALIGNMENT,
hallucination_type=HallucinationType.ALIGNMENT_OVERCORRECTION,
confidence=0.7,
evidence="安全对齐策略导致回答过度保守或偏离事实",
fix_suggestion="调整 safety prompt 的约束力度"
)
return None7.3 企业 bad case 管理数据库设计
幻觉治理需要持续积累 bad case。一个好的 bad case 数据库不仅是问题的记录,更是修复决策和回归测试的数据基础。
-- 幻觉 bad case 管理表结构
CREATE TABLE hallucination_cases (
case_id VARCHAR(32) PRIMARY KEY,
created_at TIMESTAMP DEFAULT NOW(),
source VARCHAR(20), -- online_feedback / test_suite / manual
severity VARCHAR(10), -- critical / high / medium / low
business_domain VARCHAR(50), -- hr_policy / finance / logistics / ...
-- 输入
query TEXT NOT NULL,
context_window TEXT, -- 实际传入模型的完整上下文
-- 输出
model_answer TEXT NOT NULL,
gold_answer TEXT,
-- 检索信息
retrieved_doc_ids JSONB, -- ["doc-001", "doc-002"]
gold_doc_ids JSONB, -- ["doc-001", "doc-003"]
retrieval_recall FLOAT,
-- 诊断结果
hallucination_type VARCHAR(40), -- 二级分类标签
root_cause_layer VARCHAR(20), -- L1-L5
root_cause_detail TEXT,
fix_suggestion TEXT,
-- 治理状态
status VARCHAR(20) DEFAULT 'open', -- open/fixing/fixed/wontfix
fixed_by VARCHAR(50), -- prompt_update / rerank_tune / ...
fixed_at TIMESTAMP,
regression_test BOOLEAN DEFAULT FALSE,
-- 版本追踪
model_version VARCHAR(30),
rag_config JSONB, -- embedding_model, chunk_size, top_k 等
prompt_version VARCHAR(30)
);
CREATE INDEX idx_type ON hallucination_cases(hallucination_type);
CREATE INDEX idx_layer ON hallucination_cases(root_cause_layer);
CREATE INDEX idx_status ON hallucination_cases(status);
CREATE INDEX idx_domain ON hallucination_cases(business_domain);
CREATE INDEX idx_severity ON hallucination_cases(severity);
-- 统计视图:按类型和层级聚合
CREATE VIEW hallucination_summary AS
SELECT
hallucination_type,
root_cause_layer,
severity,
COUNT(*) AS total_cases,
COUNT(*) FILTER (WHERE status = 'fixed') AS fixed_cases,
ROUND(100.0 * COUNT(*) FILTER (WHERE status = 'fixed')
/ NULLIF(COUNT(*), 0), 1) AS fix_rate_pct
FROM hallucination_cases
GROUP BY hallucination_type, root_cause_layer, severity
ORDER BY total_cases DESC;08. 企业级幻觉治理体系
从检测到修复再到持续监控,企业需要一个闭环的幻觉治理体系,而不是一次性的评测。
8.1 从检测到修复的完整闭环
完整闭环流程:
采集 → 检测 → 诊断 → 修复 → 验证 → 监控 → 采集
│ │ │ │ │ │
│ │ │ │ │ └─ 线上持续监控幻觉率
│ │ │ │ └─ 回归测试确认修复有效
│ │ │ └─ 按根因层级定向修复
│ │ └─ 5-layer 根因诊断
│ └─ 自动化幻觉检测 pipeline
└─ 线上日志 + 用户反馈 + 定期评测8.2 指标体系设计
| 指标名 | 定义 | 计算公式 | 目标值 |
|---|---|---|---|
| 幻觉率 | 包含幻觉的回答占比 | 有幻觉回答数 / 总回答数 | < 5% |
| Grounding Score | 回答被证据支持的程度 | Σ(每句支持得分) / 总句数 | > 0.85 |
| Citation F1 | 引用的准确率和召回率 | 2 × P × R / (P + R) | > 0.80 |
| Boundary Compliance | 回答不超出证据边界的比例 | 合规回答数 / 总回答数 | > 0.90 |
| 根因覆盖率 | 被诊断出根因的 bad case 比例 | 有根因标签数 / 总 bad case 数 | > 0.80 |
| 修复关闭率 | 已修复的 bad case 比例 | status=fixed / 总 bad case 数 | 持续提升 |
8.3 A/B 测试与版本间幻觉回归对比
每次修改(模型升级、prompt 调整、RAG 配置变更)都可能影响幻觉率。需要建立版本间对比机制:
"""幻觉回归对比测试框架"""
def run_regression_comparison(test_cases: list,
model_a_fn, model_b_fn,
judge_fn) -> dict:
"""
对比两个版本在相同测试集上的幻觉表现
model_a_fn: 基线版本的推理函数
model_b_fn: 新版本的推理函数
"""
results = {"a_only_hallucinate": [], "b_only_hallucinate": [],
"both_hallucinate": [], "neither_hallucinate": []}
a_hal_count = 0
b_hal_count = 0
for case in test_cases:
answer_a = model_a_fn(case["query"])
answer_b = model_b_fn(case["query"])
hal_a = _is_hallucination(
answer_a, case.get("gold_answer", ""),
case.get("evidence", ""), judge_fn)
hal_b = _is_hallucination(
answer_b, case.get("gold_answer", ""),
case.get("evidence", ""), judge_fn)
a_hal_count += int(hal_a)
b_hal_count += int(hal_b)
entry = {"case_id": case["case_id"], "query": case["query"],
"answer_a": answer_a, "answer_b": answer_b}
if hal_a and not hal_b:
results["a_only_hallucinate"].append(entry)
elif hal_b and not hal_a:
results["b_only_hallucinate"].append(entry)
elif hal_a and hal_b:
results["both_hallucinate"].append(entry)
else:
results["neither_hallucinate"].append(entry)
n = len(test_cases)
return {
"total_cases": n,
"version_a_hallucination_rate": a_hal_count / n,
"version_b_hallucination_rate": b_hal_count / n,
"regression_cases": len(results["b_only_hallucinate"]),
"improvement_cases": len(results["a_only_hallucinate"]),
"details": results
}
def _is_hallucination(answer, gold, evidence, judge_fn) -> bool:
prompt = f"""判断回答是否包含幻觉。
正确答案:{gold}
证据:{evidence}
模型回答:{answer}
只回答 yes 或 no。"""
return "yes" in judge_fn(prompt).lower()版本回归的核心指标。
regression_cases(新版本新增的幻觉)比 总幻觉率 更重要。一个新版本的总幻觉率可能下降了,但如果它在之前正确的 case 上产生了新的幻觉(回归),这说明修复引入了新问题。版本发布的 gate 应该是:回归 case 数为 0 或经过人工确认可接受。
09. 完整代码实战
9.1 基于 LLM-as-Judge 的多维度幻觉检测
"""
多维度幻觉检测:LLM-as-Judge 方法
同时评估 5 个维度:事实性、忠实性、完整性、边界合规、一致性
"""
import json
from typing import Callable
MULTI_DIM_JUDGE_PROMPT = """你是一个专业的幻觉检测评审员。请从以下 5 个维度评估回答质量。
【用户问题】
{query}
【参考证据】
{evidence}
【模型回答】
{answer}
请对每个维度打 1-5 分并给出简要理由,然后输出 JSON:
{{
"factuality": {{
"score": 1-5,
"reason": "..."
}},
"faithfulness": {{
"score": 1-5,
"reason": "回答是否忠于提供的证据"
}},
"completeness": {{
"score": 1-5,
"reason": "是否覆盖了证据中的关键信息"
}},
"boundary_compliance": {{
"score": 1-5,
"reason": "是否超出证据边界添加了不支持的信息"
}},
"internal_consistency": {{
"score": 1-5,
"reason": "回答内部是否自相矛盾"
}},
"overall_hallucination": {{
"detected": true/false,
"type": "none/factual/grounding/extension/reasoning/mixed",
"severity": "none/low/medium/high/critical"
}}
}}"""
def multi_dim_hallucination_judge(query: str, evidence: str,
answer: str,
judge_fn: Callable) -> dict:
prompt = MULTI_DIM_JUDGE_PROMPT.format(
query=query, evidence=evidence, answer=answer)
result = judge_fn(prompt)
try:
return json.loads(result)
except json.JSONDecodeError:
return {"error": "JSON 解析失败", "raw": result}
def batch_evaluate(cases: list, judge_fn: Callable) -> dict:
"""批量评测并汇总统计"""
results = []
for case in cases:
r = multi_dim_hallucination_judge(
case["query"], case.get("evidence", ""),
case["model_answer"], judge_fn)
r["case_id"] = case.get("case_id", "")
results.append(r)
# 汇总统计
dims = ["factuality", "faithfulness", "completeness",
"boundary_compliance", "internal_consistency"]
summary = {}
valid = [r for r in results if "error" not in r]
for dim in dims:
scores = [r[dim]["score"] for r in valid if dim in r]
summary[f"{dim}_avg"] = sum(scores) / len(scores) if scores else 0
summary[f"{dim}_min"] = min(scores) if scores else 0
hal_detected = [r for r in valid
if r.get("overall_hallucination", {}).get("detected")]
summary["hallucination_rate"] = (
len(hal_detected) / len(valid) if valid else 0)
summary["total_evaluated"] = len(valid)
summary["detailed_results"] = results
return summary9.2 基于 NLI 模型的事实一致性检测
NLI(Natural Language Inference)模型可以判断两个句子之间的蕴含/矛盾/中立关系,是一种不依赖 LLM Judge 的幻觉检测方法。
"""
基于 NLI 模型的事实一致性检测
使用 HuggingFace 上的 NLI 模型进行推理
优势:不需要调用 LLM API,可以本地运行,速度快且确定性强
"""
from transformers import pipeline
import re
class NLIFactChecker:
def __init__(self, model_name="cross-encoder/nli-deberta-v3-base"):
self.nli = pipeline(
"text-classification", model=model_name,
return_all_scores=True)
def check_sentence_against_evidence(self, sentence: str,
evidence: str) -> dict:
"""检查单个句子是否被证据支持"""
result = self.nli(
f"{evidence}", f"{sentence}",
truncation=True, max_length=512)
scores = {item["label"]: item["score"] for item in result[0]}
return {
"sentence": sentence,
"entailment": scores.get("ENTAILMENT", 0),
"contradiction": scores.get("CONTRADICTION", 0),
"neutral": scores.get("NEUTRAL", 0),
"verdict": self._get_verdict(scores)
}
def check_answer(self, answer: str, evidence: str) -> dict:
"""逐句检查完整回答的一致性"""
sentences = self._split_sentences(answer)
results = []
for sent in sentences:
if len(sent.strip()) < 5:
continue
r = self.check_sentence_against_evidence(sent, evidence)
results.append(r)
supported = sum(1 for r in results if r["verdict"] == "supported")
contradicted = sum(
1 for r in results if r["verdict"] == "contradicted")
neutral = sum(1 for r in results if r["verdict"] == "neutral")
total = len(results)
return {
"total_sentences": total,
"supported": supported,
"contradicted": contradicted,
"neutral": neutral,
"consistency_score": supported / total if total > 0 else 0,
"has_contradiction": contradicted > 0,
"sentence_details": results
}
def _get_verdict(self, scores: dict) -> str:
if scores.get("ENTAILMENT", 0) > 0.7:
return "supported"
if scores.get("CONTRADICTION", 0) > 0.5:
return "contradicted"
return "neutral"
def _split_sentences(self, text: str) -> list:
return [s.strip() for s in re.split(r'[。!?\.\!\?]', text)
if s.strip()]
# 使用示例
if __name__ == "__main__":
checker = NLIFactChecker()
evidence = "北京地区试用期员工不享受年假。正式员工在入职满1年后可享受年假。"
answer = "通常来说,员工入职后都可以享受年假,包括试用期员工。"
result = checker.check_answer(answer, evidence)
print(f"一致性得分: {result['consistency_score']:.2f}")
print(f"存在矛盾: {result['has_contradiction']}")
for detail in result["sentence_details"]:
print(f" [{detail['verdict']}] {detail['sentence']}")
print(f" 蕴含={detail['entailment']:.3f} "
f"矛盾={detail['contradiction']:.3f} "
f"中立={detail['neutral']:.3f}")9.3 端到端幻觉评测 pipeline
"""
端到端幻觉评测 Pipeline
整合 LLM-as-Judge + NLI + logits 分析,输出综合报告
"""
import json
from datetime import datetime
from typing import Callable, Optional
class HallucinationEvaluationPipeline:
def __init__(self, judge_fn: Callable,
nli_checker=None,
logits_analyzer=None):
self.judge_fn = judge_fn
self.nli_checker = nli_checker
self.logits_analyzer = logits_analyzer
def evaluate_single(self, case: dict) -> dict:
"""评测单个 case"""
report = {
"case_id": case.get("case_id", ""),
"timestamp": datetime.now().isoformat(),
"query": case["query"],
"model_answer": case["model_answer"],
}
# 维度1:LLM Judge 多维度评估
judge_result = multi_dim_hallucination_judge(
case["query"],
case.get("evidence", ""),
case["model_answer"],
self.judge_fn
)
report["llm_judge"] = judge_result
# 维度2:NLI 事实一致性(如果可用)
if self.nli_checker and case.get("evidence"):
nli_result = self.nli_checker.check_answer(
case["model_answer"], case["evidence"])
report["nli_check"] = {
"consistency_score": nli_result["consistency_score"],
"has_contradiction": nli_result["has_contradiction"],
"contradicted_sentences": [
d["sentence"] for d in nli_result["sentence_details"]
if d["verdict"] == "contradicted"
]
}
# 综合判定
report["final_verdict"] = self._aggregate_verdict(report)
return report
def evaluate_batch(self, cases: list,
output_file: Optional[str] = None) -> dict:
"""批量评测并输出报告"""
results = []
for case in cases:
r = self.evaluate_single(case)
results.append(r)
summary = self._compute_summary(results)
full_report = {"summary": summary, "results": results}
if output_file:
with open(output_file, "w", encoding="utf-8") as f:
json.dump(full_report, f, ensure_ascii=False, indent=2)
return full_report
def _aggregate_verdict(self, report: dict) -> dict:
signals = []
judge = report.get("llm_judge", {})
if judge.get("overall_hallucination", {}).get("detected"):
signals.append(("llm_judge", "hallucination_detected"))
nli = report.get("nli_check", {})
if nli.get("has_contradiction"):
signals.append(("nli", "contradiction_found"))
if nli.get("consistency_score", 1.0) < 0.5:
signals.append(("nli", "low_consistency"))
is_hallucination = len(signals) >= 1
confidence = min(1.0, len(signals) * 0.4 + 0.2) if signals else 0.1
return {
"is_hallucination": is_hallucination,
"confidence": confidence,
"signals": signals,
"severity": judge.get(
"overall_hallucination", {}).get("severity", "unknown")
}
def _compute_summary(self, results: list) -> dict:
total = len(results)
hal_count = sum(
1 for r in results
if r["final_verdict"]["is_hallucination"])
severity_dist = {}
for r in results:
sev = r["final_verdict"].get("severity", "unknown")
severity_dist[sev] = severity_dist.get(sev, 0) + 1
return {
"total_evaluated": total,
"hallucination_count": hal_count,
"hallucination_rate": hal_count / total if total else 0,
"severity_distribution": severity_dist,
"evaluation_time": datetime.now().isoformat()
}10. 测试设计清单
10.1 必做项
- 分类标签:把幻觉类问题按二级缺陷类型打标签(至少 6 种),不要只写一个大类。
- 双字段记录:RAG 任务同时记录
expected_docs和expected_answer,区分召回问题和生成问题。 - 证据边界断言:对高风险问题做 evidence-based 断言,检查回答是否超出证据边界。
- 中间步骤检查:对推理任务抽样检查中间步骤或思维轨迹摘要,不仅看最终答案。
- 对抗集构建:把线上 bad case 转成对抗集,并记录属于哪一种幻觉类型。
- 多文档冲突覆盖:测试集至少包含 10% 的多文档冲突场景。
- 位置敏感性测试:将关键证据分别放在上下文的开头/中间/结尾,验证位置对结果的影响。
- 否定句测试:专门构造包含否定表述的证据("不允许""不适用""除...外"),验证模型对否定逻辑的处理。
10.2 进阶项
- 根因分布监控:每周统计各层级根因的分布比例,观察趋势变化。
- 版本回归门禁:每次版本更新前运行回归测试,确保新版本不在已修复 case 上产生新幻觉。
- NLI 自动化:部署 NLI 模型作为线上实时幻觉检测的第一道防线。
- logits 监控:对关键业务场景启用 logits 熵值监控,高熵区域自动标记为需审核。
- 对齐税评估:对比有/无 safety prompt 的输出差异,量化安全约束对准确性的影响。
- Citation F1:对需要引用的场景计算引用的精确率和召回率。
10.3 自测标准
学完这一页后,你应该能:
- 区分内在幻觉和外在幻觉,理解它们不同的修复路径。
- 用交叉熵损失和信息论解释为什么"流畅错"会发生。
- 说明 RAG 链路中 embedding/ANN/rerank/拼接 每层如何引入幻觉。
- 解释 Lost in the Middle 现象和注意力稀释的数学机制。
- 用马尔可夫链模型计算多步推理的错误积累概率。
- 说明 RLHF/DPO 如何通过 reward hacking 放大幻觉。
- 使用五层诊断模型对 bad case 进行根因定位。
- 搭建包含 LLM Judge + NLI 的端到端幻觉评测 pipeline。
- 设计企业级的幻觉指标体系和版本回归测试机制。
11. 深度思考题与课堂练习
基础题
- 给出一个政策问答 bad case,判断它更像事实幻觉、grounding miss 还是证据混拼,并说明理由。
- 设计一组 RAG 评测样本,让系统区分"没检到"和"检到了但没用上"。
- 写一个最小断言规则,专门检测"回答超出了证据边界"。
- 解释为什么 beam search 可能比 nucleus sampling 产生更多的"流畅幻觉"。
进阶题
- 假设一个 10 步推理链中每步的正确率为 0.92,且模型的自我纠正率为 0.05。用马尔可夫链模型计算最终答案正确的概率,并与不考虑自我纠正时的结果对比。
- 你的 RAG 系统在 top-5 检索时幻觉率为 8%,在 top-20 检索时幻觉率反而上升到 12%。用 Lost in the Middle 理论解释这一现象,并提出三种可能的改进方案。
- 设计一个实验方案来量化 RLHF 训练对幻觉率的影响。你需要控制哪些变量?用什么指标来衡量?如何区分"RLHF 减少了胡言乱语"和"RLHF 增加了流畅幻觉"这两种效果?
- 你的企业 bad case 数据库中有 500 条幻觉记录。设计一个优先级排序算法,综合考虑业务影响度、出现频率、修复难度和根因层级,输出一个修复优先级队列。给出完整的代码实现。
实战题
- 使用本章提供的
HallucinationDiagnoser类,对以下三个 bad case 进行诊断,并对比自动诊断结果与你的人工判断:- Case A:用户问"公积金提取条件",模型回答正确但附加了"通常 3 个工作日到账"(文档未提及时间)
- Case B:用户问"年终奖发放规则",检索返回了 2023 版和 2024 版制度,模型混合引用了两个版本
- Case C:用户问"跨境汇款手续费",模型没有调用费率查询工具,直接给出了一个看似合理的数字
- 部署本章的 NLI 事实一致性检测器,在你的业务测试集上运行,对比 NLI 检测和 LLM Judge 检测的一致率。分析两种方法各自的盲区。
参考答案要点
- 分类时先问"正确证据是否进入窗口",再问"结论是否被证据直接支持",最后再看有没有推理或工具层误用。
- RAG 样本最好同时提供
gold_docs、gold_answer、must_not_claim三类字段,这样才能把召回问题和生成问题拆开。 - 证据边界断言至少要覆盖三件事:是否引用了证据、是否遗漏关键限制条件、是否加入证据外新结论。
- beam search 倾向于生成高概率序列,而高概率序列更可能是训练数据中的常见模式而非罕见事实,因此更容易产生"流畅但不准确"的内容。
- RLHF 实验需要控制的变量:base model、训练数据量、训练步数、reward model 质量。区分两种效果可以通过分别评估"胡言乱语率"(语法/语义不通顺的比例)和"流畅幻觉率"(语义通顺但事实错误的比例)。