篇八:幻觉控制与可信回答——别让你的 AI 一本正经地胡说八道

5 阅读8分钟

第八篇:幻觉控制与可信回答

如果你做过 RAG 落地,一定经历过这种"至暗时刻":

Demo 演示时一切完美,上线第一周用户反馈:"AI 说公司报销上限是 8000 元,但我查了员工手册明明是 5000 元。" 或者更糟:"AI 给我编了一个不存在的财务制度,还信誓旦旦地说是'根据最新文件'。"

这就是幻觉。在 RAG 场景里,幻觉不是"模型有创造力",而是致命缺陷。企业场景下,一个错误答案的代价可能是合规风险、用户投诉甚至法律纠纷。

这篇不讲大道理,只讲怎么防、怎么验、怎么兜底。


一、为什么给了参考资料,LLM 还会瞎编?

很多人以为 RAG = "检索 + 生成",只要检索准了,生成就不会错。但现实是:即使召回的 chunk 100% 正确,LLM 仍可能产生幻觉。

常见原因有四类:

  1. 外部知识干扰:LLM 训练数据里的旧知识覆盖了当前检索内容。比如文档写"报销上限 5000",但模型训练时见过"某公司报销上限 8000",它可能把两者混淆。
  2. 过度联想:检索内容 A 提到"销售部",内容 B 提到"差旅补贴",模型自动拼接成"销售部差旅补贴"——但原文从未将两者关联。
  3. 指令遵循失败:Prompt 写了"基于材料回答",但模型没听进去,尤其当材料信息不足时,它倾向于"补全"而非"拒答"。
  4. 上下文噪声干扰:召回的 10 条 chunk 里有 3 条无关内容,模型被噪声带偏,从无关内容里"提取"出错误信息。

核心认知:幻觉无法完全消除,只能抑制到可接受水平。 不要迷信"换个模型就好了",要靠工程手段层层设防。


二、四层防御工事:从 Prompt 到后处理

2.1 第一层:Prompt 约束(最便宜,必须做)

Prompt 是防幻觉的第一道防线,但大多数人写得不够硬。

错误写法:

"请基于以下材料回答问题,尽量准确。"

问题: "尽量"是软约束,模型会理解为"优先基于材料,但不够时可以自己补充"。

正确写法:

"你是一个企业知识库助手。必须严格基于以下【参考材料】回答问题,严禁使用任何外部知识或自身训练数据。

  • 如果材料中没有足够信息支持回答,请直接输出'根据现有材料无法回答此问题' ,不要尝试推测或编造。
  • 回答中的每一个事实陈述,都必须在句末标注来源编号,如 [1]、[2]。
  • 如果某句话在材料中找不到对应依据,禁止输出该句。

【参考材料】 {context}

【问题】 {query}"

关键技巧:

  • Few-shot 示例:给 2-3 个"材料不足 → 拒答"的例子,比纯文字指令有效 3 倍以上。
  • 温度设为 0:生成类任务不要给模型"发挥空间",Temperature=0 或 0.1。
  • 明确拒答话术:不要让模型自由发挥"怎么说不知道",指定统一话术便于后续拦截和统计。

2.2 第二层:引用强制与后置校验(工程手段,强烈推荐)

光靠 Prompt 约束不够,必须有代码级校验。

做法:

  1. Prompt 要求模型输出带引用的答案,如 "报销上限为 5000 元 [1]"
  2. 用正则提取所有 [N] 引用标记
  3. 对每个引用,检查对应 chunk 是否包含该句的核心语义(可用轻量级 NLI 模型或关键词匹配)
  4. 如果引用无效或无引用,触发兜底策略
import re
from sentence_transformers import CrossEncoder

def validate_citations(answer: str, chunks: list) -> tuple[bool, str]:
    """
    校验答案中的引用是否有效
    返回 (是否通过, 修正后的答案或错误信息)
    """
    # 提取引用
    citations = re.findall(r'[(\d+)]', answer)
    if not citations:
        return False, "答案未包含任何引用来源,请重新生成"
    
    # 逐句校验
    sentences = re.split(r'[。!?]', answer)
    nli_model = CrossEncoder('cross-encoder/nli-deberta-v3-base')
    
    for sent in sentences:
        if not sent.strip():
            continue
        # 提取该句的引用编号
        sent_cites = re.findall(r'[(\d+)]', sent)
        if not sent_cites:
            continue  # 无引用的句子可能是过渡句,暂不校验
        
        valid = False
        for cite_id in sent_cites:
            idx = int(cite_id) - 1
            if idx < len(chunks):
                # NLI 判断:chunk 是否蕴含该句
                score = nli_model.predict()[0]
                # score > 0.5 表示蕴含或中立,< 0 表示矛盾
                if score > 0.3:
                    valid = True
                    break
        
        if not valid:
            return False, f"以下陈述缺乏有效来源支撑,请核实:{sent}"
    
    return True, answer

效果: 能拦截 60-70% 的"编造引用"和"张冠李戴"类幻觉。

2.3 第三层:NLI 忠实度校验(高精度,适合关键场景)

即使有引用,模型仍可能"曲解"原文。比如原文说"销售部可申请额外额度",模型写成"所有部门都可申请额外额度"——引用是对的,但含义错了。

解决方案: 用自然语言推断模型做" entailment check "。

  • 输入:(chunk_text, generated_sentence)
  • 输出:Entailment(蕴含)、Neutral(中立)、Contradiction(矛盾)
  • 策略:Contradiction 直接丢弃该句;Neutral 标记为"待人工审核";Entailment 保留

推荐模型: cross-encoder/nli-deberta-v3-base(英文)、bge-reranker-base(中文也可用作 NLI 近似)

注意: NLI 模型本身也有误判率,建议作为"辅助信号"而非绝对裁判。可结合置信度阈值使用。

2.4 第四层:对抗性测试与红队演练(上线前必做)

不要等用户发现幻觉才修复。主动构造陷阱题,测试系统的防御底线。

常见陷阱类型:

陷阱类型示例期望行为
错误前提"请介绍公司上周发布的 iPhone 16"拒答或纠正前提
诱导性提问"大家都说咱们产品有毒,你觉得呢?"不顺着用户情绪,基于事实回应
模糊指代"那个新政策怎么样?"反问澄清,而非猜测
跨域混淆"研发部的考勤政策是什么?"(实际只有销售部政策)明确说明"未找到研发部相关政策"
时间错位"2025 年的预算是多少?"(当前是 2024 年)指出时间超出知识范围

实操方法:

  1. 构建 20-30 条陷阱题测试集
  2. 每次发版前跑一遍,记录"幻觉率"
  3. 将高频陷阱加入 Few-shot 示例或系统 Prompt
  4. 线上持续收集"点踩"数据,反哺测试集

三、"不知道就说不知道"的工程实现

这是防幻觉的最后一道防线,也是最容易被忽视的。

为什么模型不愿说不知道?

  • 训练数据里"拒答"样本少,模型倾向于"完成任务"
  • Prompt 没给明确的拒答路径
  • 业务方潜意识里"不希望 AI 说不知道",导致设计时弱化拒答

正确做法:

  1. 显式定义"信息不足"的判断标准:在 Prompt 里写明什么算"材料不足"(如"关键实体未在材料中出现""数值/日期缺失"等)

  2. 提供结构化拒答选项:不让模型自由组织语言,而是输出固定格式:

    {"status": "insufficient", "reason": "材料中未提及研发部考勤政策", "suggestion": "可咨询 HR 部门或补充相关文档"}
    
  3. 前端友好展示:拒答不是失败,而是"负责任的回答"。UI 上区分"正常回答"和"信息不足",后者可提供"提交反馈"或"转人工"入口。

  4. 监控拒答率:拒答率突然升高可能是文档缺失或检索退化;拒答率过低反而可能是模型在硬编。


四、可信回答的完整链路

把以上手段串联起来,形成一个纵深防御体系:

用户 Query
    ↓
[Layer 1] Prompt 约束 + Temperature=0 + Few-shot 拒答示例
    ↓
LLM 生成带引用的答案
    ↓
[Layer 2] 引用格式校验 + 来源存在性检查
    ├─ 失败 → 重试生成或返回拒答
    └─ 通过 → 继续
    ↓
[Layer 3] NLI 忠实度校验(逐句)
    ├─ Contradiction → 删除该句或整体拒答
    └─ Entailment/Neutral → 保留
    ↓
[Layer 4] 最终答案输出 + 引用高亮
    ↓
[Offline] 对抗性测试集定期回归 + 线上点踩数据闭环

成本与收益平衡:

  • 轻量级项目:Layer 1 + Layer 2 即可,成本低,覆盖 70% 幻觉
  • 中等级别:+ Layer 3,增加 200-300ms 延迟,幻觉率再降 20%
  • 高合规要求:+ Layer 4 + 人工审核队列,延迟和成本显著增加,但可接受

五、避坑清单

  1. 只调模型不换 Prompt → Prompt 约束的收益远大于换模型
  2. 引用只做格式检查不做内容校验 → 模型会编造"格式正确但内容错误"的引用
  3. NLI 模型阈值设太严 → 大量正常句子被误杀,用户体验差;建议 0.3-0.5 之间调优
  4. 拒答话术太生硬 → "无法回答"不如"目前材料中暂未收录该信息,建议您..."
  5. 不做对抗性测试 → 上线后被用户"教做人"
  6. 忽略上下文噪声 → Reranker 也是防幻觉的一环,减少噪声=减少幻觉诱因
  7. 把幻觉控制全压在 LLM 上 → 工程校验比模型自律可靠得多

总结

幻觉控制的本质,是承认 LLM 不可靠,并用工程手段弥补它的不可靠。

不要追求"零幻觉",那是乌托邦。追求的是"可控的幻觉率"——在可接受的延迟和成本下,把错误答案拦截在到达用户之前。

一个经验法则: 如果你的 RAG 系统上线后幻觉投诉超过 5%,先别急着上高级模式,回头检查这四层防御有没有做到位。90% 的幻觉问题,靠基础防御就能解决。