Skip to content

幻觉机理与根因诊断 ​

幻觉不是一个单一标签。真正难的是:它到底是预训练统计偏差、检索链路漏召回、上下文拼接错误、推理中途偏航,还是安全对齐导致的保守误答。本章从数学推导、信息论、Transformer 源码、RAG 链路拆解、对齐训练副作用五个维度,把"为什么会编"拆到可以精准归因、精准修复的粒度。只有把根因拆清楚,测试结论才有修复价值,否则报告只会停留在"这里回答错了"。

教学导读

**定位:**这一章不是重复"怎么测幻觉",而是从数学、源码和工程三个层面把幻觉从结果标签拆成可以追根因、做治理、看趋势的缺陷体系。 **前置依赖:**建议已学习 RAG、推理测试和基础幻觉检测方法;了解交叉熵、softmax 等基础概念。 **适用场景:**知识库问答、事实问答、推理任务、工具增强生成、安全边界分析、企业 Agent 系统。 **学完产出:**你应该能把一个错误回答拆成事实幻觉、grounding miss、推理偏航或工具误用,给出精确的根因层级标签和修复建议,并能搭建自动化的幻觉检测 pipeline。

先做一个纠偏。

"幻觉"这个词太常用了,以至于大家容易把所有错误答案都叫幻觉。实际上,这会让修复方向变模糊。对测试来说,更好的做法是把错误拆成事实编造、证据未使用、证据混合、工具误用、推理偏航、记忆捏造等更细的类型。一个"有幻觉"的标签对研发没有任何定位价值,等于告诉医生"病人不舒服"但不说哪里不舒服。

01. 幻觉的定义与学术脉络 ​

在自然语言生成(NLG)领域,"幻觉"(Hallucination)一词最早在摘要生成任务中被系统性使用。Maynez 等人(2020)在 ACL 论文中将幻觉定义为:生成内容中出现了在源文本中没有依据支撑的信息。随着大语言模型(LLM)的能力跃升,幻觉的定义也从"偏离源文本"扩展到"偏离可验证的世界事实"。

1.1 从 GPT-2 到 GPT-4:幻觉研究的演进 ​

阶段代表模型幻觉特征研究焦点
2019-2020GPT-2, BART高频胡言乱语,语法错误多摘要忠实性、Factual Consistency
2020-2022GPT-3, T5-XXL语法正确但事实编造,"流畅错"出现FaithDial、QA 事实核查、知识冲突
2022-2023ChatGPT, GPT-3.5高度流畅的幻觉,用户难以分辨TruthfulQA、HaluEval、RLHF 与幻觉
2023-2024GPT-4, Claude 3幻觉率降低但更隐蔽,推理幻觉增多长链推理幻觉、RAG grounding、对齐税
2024-2026GPT-4o, o1/o3思维链中的隐式幻觉、工具调用幻觉CoT 忠实性、Agent 幻觉、多模态幻觉

1.2 两种基础定义 ​

内在幻觉(Intrinsic Hallucination) ​

生成内容与源输入/证据直接矛盾。例如:文档说"试用期不享受年假",模型回答"试用期可以休年假"。这类幻觉最容易通过自动化检测发现,因为存在明确的证据冲突。

外在幻觉(Extrinsic Hallucination) ​

生成内容在源输入中既没有支持也没有反驳的依据,是模型自行"添加"的信息。例如:问"公司差旅政策",模型除了正确内容外还加了一句"通常国际差旅有额外补贴"——文档中根本没提。这类幻觉更难检测,因为它可能碰巧正确。

为什么区分内在/外在很重要?

内在幻觉的修复方向是加强 grounding 约束和引用机制;外在幻觉的修复方向是限制模型在证据之外的生成自由度(如 constrained decoding、拒答策略)。测试时如果不区分这两类,修复动作会打到错误的层上。

1.3 重要学术基准 ​

基准年份核心思路覆盖维度
TruthfulQA2022817 道设计让模型犯错的题,检测模型是否会输出常见误解事实性
HaluEval2023自动生成的大规模幻觉评测集,涵盖 QA/对话/摘要多任务幻觉
FaithDial2022对话场景中的忠实性标注,评估回答是否忠于知识对话 grounding
FELM2023从世界知识、数学、推理、代码多维度标注幻觉多领域细粒度
RAGTruth2024专门针对 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 issues

5.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 None

7.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 summary

9.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 必做项 ​

  1. 分类标签:把幻觉类问题按二级缺陷类型打标签(至少 6 种),不要只写一个大类。
  2. 双字段记录:RAG 任务同时记录 expected_docs 和 expected_answer,区分召回问题和生成问题。
  3. 证据边界断言:对高风险问题做 evidence-based 断言,检查回答是否超出证据边界。
  4. 中间步骤检查:对推理任务抽样检查中间步骤或思维轨迹摘要,不仅看最终答案。
  5. 对抗集构建:把线上 bad case 转成对抗集,并记录属于哪一种幻觉类型。
  6. 多文档冲突覆盖:测试集至少包含 10% 的多文档冲突场景。
  7. 位置敏感性测试:将关键证据分别放在上下文的开头/中间/结尾,验证位置对结果的影响。
  8. 否定句测试:专门构造包含否定表述的证据("不允许""不适用""除...外"),验证模型对否定逻辑的处理。

10.2 进阶项 ​

  1. 根因分布监控:每周统计各层级根因的分布比例,观察趋势变化。
  2. 版本回归门禁:每次版本更新前运行回归测试,确保新版本不在已修复 case 上产生新幻觉。
  3. NLI 自动化:部署 NLI 模型作为线上实时幻觉检测的第一道防线。
  4. logits 监控:对关键业务场景启用 logits 熵值监控,高熵区域自动标记为需审核。
  5. 对齐税评估:对比有/无 safety prompt 的输出差异,量化安全约束对准确性的影响。
  6. Citation F1:对需要引用的场景计算引用的精确率和召回率。

10.3 自测标准 ​

学完这一页后,你应该能:

  • 区分内在幻觉和外在幻觉,理解它们不同的修复路径。
  • 用交叉熵损失和信息论解释为什么"流畅错"会发生。
  • 说明 RAG 链路中 embedding/ANN/rerank/拼接 每层如何引入幻觉。
  • 解释 Lost in the Middle 现象和注意力稀释的数学机制。
  • 用马尔可夫链模型计算多步推理的错误积累概率。
  • 说明 RLHF/DPO 如何通过 reward hacking 放大幻觉。
  • 使用五层诊断模型对 bad case 进行根因定位。
  • 搭建包含 LLM Judge + NLI 的端到端幻觉评测 pipeline。
  • 设计企业级的幻觉指标体系和版本回归测试机制。

11. 深度思考题与课堂练习 ​

基础题 ​

  1. 给出一个政策问答 bad case,判断它更像事实幻觉、grounding miss 还是证据混拼,并说明理由。
  2. 设计一组 RAG 评测样本,让系统区分"没检到"和"检到了但没用上"。
  3. 写一个最小断言规则,专门检测"回答超出了证据边界"。
  4. 解释为什么 beam search 可能比 nucleus sampling 产生更多的"流畅幻觉"。

进阶题 ​

  1. 假设一个 10 步推理链中每步的正确率为 0.92,且模型的自我纠正率为 0.05。用马尔可夫链模型计算最终答案正确的概率,并与不考虑自我纠正时的结果对比。
  2. 你的 RAG 系统在 top-5 检索时幻觉率为 8%,在 top-20 检索时幻觉率反而上升到 12%。用 Lost in the Middle 理论解释这一现象,并提出三种可能的改进方案。
  3. 设计一个实验方案来量化 RLHF 训练对幻觉率的影响。你需要控制哪些变量?用什么指标来衡量?如何区分"RLHF 减少了胡言乱语"和"RLHF 增加了流畅幻觉"这两种效果?
  4. 你的企业 bad case 数据库中有 500 条幻觉记录。设计一个优先级排序算法,综合考虑业务影响度、出现频率、修复难度和根因层级,输出一个修复优先级队列。给出完整的代码实现。

实战题 ​

  1. 使用本章提供的 HallucinationDiagnoser 类,对以下三个 bad case 进行诊断,并对比自动诊断结果与你的人工判断:
    • Case A:用户问"公积金提取条件",模型回答正确但附加了"通常 3 个工作日到账"(文档未提及时间)
    • Case B:用户问"年终奖发放规则",检索返回了 2023 版和 2024 版制度,模型混合引用了两个版本
    • Case C:用户问"跨境汇款手续费",模型没有调用费率查询工具,直接给出了一个看似合理的数字
  2. 部署本章的 NLI 事实一致性检测器,在你的业务测试集上运行,对比 NLI 检测和 LLM Judge 检测的一致率。分析两种方法各自的盲区。

参考答案要点 ​

  • 分类时先问"正确证据是否进入窗口",再问"结论是否被证据直接支持",最后再看有没有推理或工具层误用。
  • RAG 样本最好同时提供 gold_docs、gold_answer、must_not_claim 三类字段,这样才能把召回问题和生成问题拆开。
  • 证据边界断言至少要覆盖三件事:是否引用了证据、是否遗漏关键限制条件、是否加入证据外新结论。
  • beam search 倾向于生成高概率序列,而高概率序列更可能是训练数据中的常见模式而非罕见事实,因此更容易产生"流畅但不准确"的内容。
  • RLHF 实验需要控制的变量:base model、训练数据量、训练步数、reward model 质量。区分两种效果可以通过分别评估"胡言乱语率"(语法/语义不通顺的比例)和"流畅幻觉率"(语义通顺但事实错误的比例)。