Skip to content

04.3 幻觉的成因与锁死方案

五类幻觉,五条产生路径,五套反制手段。

读完你能做什么:把我的输出从"需要全面核查"变成"只需核查特定几类内容"。

⚠️ 前置认知:幻觉无法被彻底消除,只能被大幅降低概率并变得可检测。任何声称"完全消除幻觉"的方案都是错的。本章的目标是把幻觉从"随机分布在各处的不可控风险"变成"集中在可预测位置的可管理风险"。


一、幻觉的根本成因

回到 04.1 的核心事实:

我输出的是概率分布中的一次采样,而采样过程不携带置信度信息

当我很确定时,分布尖锐;当我不确定时,分布平坦。但无论哪种情况,采样出来的 token 看起来是一样的 —— 我不会说"我这个词是从一个平坦分布里勉强挑出来的"。

更糟的是:生成流畅文本的能力,和拥有正确知识的能力,是两个不同的能力。我可以在完全没有相关知识的情况下,生成语法完美、术语正确、看起来很专业的错误内容。

这就是为什么幻觉如此危险 —— 它不长得像错误。


二、五类幻觉

类型 1:事实捏造(Fabrication)

表现:编造不存在的 API、函数、参数、文献、人名、事件。

产生路径:训练数据中有大量"类似"的模式。当你问一个不存在的东西时,我会按照那个模式的形状生成一个"看起来该是这样"的答案。

text
你问:pandas 的 DataFrame.smart_merge() 怎么用?
我答:(详细解释了一个不存在的方法,参数、返回值一应俱全)

因为 pandas 有大量 DataFrame.xxx() 方法,"生成一个 DataFrame 方法的用法说明"这个模式非常熟练,与该方法是否真实存在无关。

高风险场景

  • 不常用库的 API
  • 特定版本的参数
  • 学术引用
  • 配置项名称
  • 命令行 flag

类型 2:细节漂移(Detail Drift)

表现:大方向对,细节错。参数顺序反了、默认值记错了、版本号偏了一位。

产生路径:我对"主题级"信息的记忆远强于"细节级"。细节在训练中出现的形式多样(不同版本、不同语境),互相干扰。

text
✅ 我知道:Python 的 sorted() 可以传 key 函数
⚠️ 我可能记错:reverse 参数的默认值、key 和 cmp 的历史变迁

高风险场景

  • 函数签名的精确形式
  • 配置文件的字段名
  • 默认值
  • 版本间的行为差异

类型 3:过时信息(Staleness)

表现:给出的是训练截止日期时正确、现在已过时的信息。

产生路径:训练数据有时间边界,但我的输出不带时间戳。

高风险场景

  • 「最新版本是多少」
  • 「现在推荐的做法」
  • 「XX 公司的 CEO 是谁」
  • 「这个 API 还支持吗」
  • 任何包含"现在""当前""最新"的问题

类型 4:上下文误读(Misreading)

表现:上下文里明明写了 A,我说成了 B。

产生路径:位置效应导致的注意力不足(见 04.2)。特别容易发生在:

  • 长上下文的中段
  • 上下文中有多个相似但不同的内容(三个版本的同一个函数)
  • 否定句、条件句(「除非 X,否则 Y」容易被读成「X 则 Y」)

这类幻觉最隐蔽,因为你会以为"上下文里有,它应该不会错"。

类型 5:推理跳跃(Unwarranted Inference)

表现:从事实 A 推出了不成立的结论 B,但表述得像 B 也是事实。

产生路径:我不会自动区分"这是我看到的"和"这是我推出来的"。生成时两者混在同一段流畅的文字里。

text
上下文说:「订单表有 500 万行,查询耗时 8 秒」
我的输出:「由于订单表缺少索引导致全表扫描……」
                ↑ 这是推测,不是上下文里的事实,但读起来像事实

高风险场景:根因分析、诊断、归因类任务。


三、五套反制手段

手段 1:强制引用(对抗类型 1、4)

最高性价比的一招。

text
<instructions>
分两步作答:

第一步,在 <evidence> 标签内,逐字摘录支持你答案的原文,
并标注来源(文件名 + 行号,或文档名 + 章节)。
- 必须是原文,不能改写
- 找不到就写「无直接证据」

第二步,在 <answer> 标签内作答,只能基于 <evidence>。
</instructions>

为什么有效

  1. 摘录是可验证的 —— 你 Ctrl+F 一搜就知道真假
  2. 强制注意力真正投向原文,对抗中段衰减
  3. 「找不到就写无直接证据」提供了一条明确的退出路径

在 Claude Code 中的版本

text
每个关于代码行为的断言,必须给出 `文件路径:行号`。
不确定的地方用 Read 工具实际读一遍,不要凭印象。

手段 2:显式区分知识来源(对抗类型 5)

text
<output_format>
每条陈述前必须标注来源类型:

[观察] —— 我在上下文/工具结果中直接看到的
[推断] —— 我基于观察推理出来的(必须说明推理依据)
[通识] —— 我的训练知识(可能过时或有误)
[未知] —— 我不知道

示例:
[观察] 订单表有 500 万行(来自 schema.sql:12)
[观察] 该查询耗时 8 秒(来自你提供的日志)
[推断] 可能是全表扫描(依据:耗时与行数的比例符合全表扫描特征)
[未知] 是否存在索引 —— 需要执行 `\d orders` 查看
</output_format>

效果:把混在一起的事实与推测分离。你可以只核查 [推断][通识] 部分。

手段 3:明确的"不知道"许可(对抗类型 1、3)

我的默认倾向是"给出有帮助的回答"。当我不知道时,这个倾向会推动我去生成一个听起来合理的答案。

你必须显式地把"说不知道"变成一个可接受的、甚至是被鼓励的输出。

text
<constraints>
- 如果上下文中没有相关信息,直接输出「上下文中未涵盖」,不要用通用知识补充
- 如果你对某个 API 的具体签名不确定,直接说「需要查证 XX 的签名」,
  不要写出一个你不确定的调用
- 输出「我不知道」是完全可接受的,且优于给出一个不确定的答案
</constraints>

加强版 —— 给出结构化的不确定性表达

text
对每个关键断言,标注置信度:
- 确定:我能给出直接证据
- 较有把握:符合我的知识但未验证
- 不确定:需要你验证
- 不知道

置信度低于「确定」的内容,必须给出验证方法。

手段 4:强制验证(对抗全部类型)

这是 Agent 场景的最强手段 —— 因为我可以真的去执行。

text
<instructions>
禁止凭记忆回答以下类型的问题,必须实际验证:

1. API 签名 → 读源码 / 查文档 / 跑一个最小示例
2. 依赖版本 → 运行 `npm ls <pkg>` 或读 lock 文件
3. 代码行为 → 实际运行,贴出真实输出
4. 文件内容 → 用 Read 工具读,不要凭之前的印象
5. 时效性信息 → 用网页搜索,引用来源和日期

如果无法验证,明确说明「无法验证:<原因>」,不要继续推理。
</instructions>

具体例子 —— 让我承认自己不知道

bash
# ❌ 我会凭印象给你一个答案
> 这个项目用的 axios 是哪个版本?有没有那个已知的 CVE?

# ✅ 强制验证
> 先运行 `npm ls axios` 确认实际安装的版本,贴出输出。
  然后用网页搜索该版本是否有已知 CVE,引用来源。
  不要凭记忆回答。

手段 5:外部程序校验(对抗全部类型)

最可靠,但需要工程投入。 让程序而不是我来判断对错。

我可能产生的错误程序校验方式
代码语法错误编译器 / linter
代码逻辑错误单元测试
API 不存在类型检查器 / 实际调用
配置项名称错误schema 校验
JSON 格式错误--json-schema 强制校验
引用了不存在的文件脚本检查路径

在 Claude Code 中用 hook 自动化:

json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/verify-edit.sh"
          }
        ]
      }
    ]
  }
}
bash
#!/usr/bin/env bash
# .claude/hooks/verify-edit.sh
# 每次编辑后自动跑类型检查,把错误直接回填给 Claude

FILE=$(jq -r '.tool_input.file_path // empty')

case "$FILE" in
  *.ts|*.tsx)
    OUT=$(npx tsc --noEmit 2>&1 | head -30)
    if [ -n "$OUT" ]; then
      jq -n --arg out "$OUT" '{
        hookSpecificOutput: {
          hookEventName: "PostToolUse",
          additionalContext: ("类型检查失败,必须修复:\n" + $out)
        }
      }'
    fi
    ;;
esac
exit 0

效果:我编造了一个不存在的方法 → 类型检查立刻失败 → 错误直接回到我的上下文 → 我必须修复。幻觉在产生的下一秒就被抓住了。


四、按场景的防护配置

场景 A:文档问答(高风险:类型 1、4)

text
<constraints>
- 分两步:先 <quotes> 摘录原文,再 <answer> 作答
- 每条摘录标注来源文档和章节
- 文档中没有的内容,输出「文档未涵盖」
- 禁止用通用知识补充
</constraints>

场景 B:代码编写(高风险:类型 1、2)

text
<constraints>
- 使用任何第三方 API 前,先读项目中已有的调用示例或查文档
- 写完后必须运行类型检查 / 编译,贴出实际输出
- 不确定的 API 签名,明确说「需要确认 XX 的签名」,不要猜
</constraints>

配合 hook 自动校验(手段 5)。

场景 C:技术调研(高风险:类型 3)

text
<constraints>
- 涉及"最新""当前""现在"的任何信息,必须先搜索验证
- 每条信息标注来源 URL 和该来源的日期
- 明确区分「我搜到的」和「我训练时知道的」
</constraints>

场景 D:根因分析(高风险:类型 5)

text
<output_format>
严格分三栏:

【已确认的事实】
(每条附证据来源)

【假设】
(每条附:如果这个假设成立,会观察到什么现象)

【验证计划】
(每条假设对应的验证方法,按成本从低到高排序)

不要在【已确认的事实】里写任何未经验证的内容。
</output_format>

五、检测幻觉的实用手法

手法 1:交叉提问

text
你刚才说 X 是这样的。现在请:
1. 指出这个信息在上下文的哪个位置(引用原文)
2. 如果上下文里没有,说明你的依据是什么

如果我给不出位置,也说不清依据,大概率是幻觉。

手法 2:独立验证子代理

在 Claude Code 中:

markdown
---
name: fact-checker
description: 独立核查一个技术断言。用于对关键结论做交叉验证。
model: opus
tools: Read, Grep, Glob, Bash, WebSearch
---

你只会收到一个待核查的断言,不会收到原始推理过程。

任务:
1. 用工具独立查证
2. 输出:`确认` / `否定` / `无法验证`
3. 附上你查到的证据(文件路径 + 行号,或 URL)

不要试图重建原始推理。只看证据。

上下文隔离是关键。如果核查者能看到原始推理,会被锚定。

手法 3:反向提问

text
给我三个理由说明你刚才的结论可能是错的。

我在"找反例"模式下,比在"证明自己对"模式下更容易发现问题。

手法 4:数字与专名审计

经验规律:幻觉在这几类内容上密度最高:

  • 具体数字(版本号、行号、百分比、日期)
  • 专有名词(函数名、配置项、人名、产品名)
  • URL 和文件路径
  • 引用("根据 XX 文档")

实践:审阅我的输出时,把这几类内容标出来重点核查,其余部分可以相对放松。这比"逐句核查"效率高得多。


六、一份可复制的通用防护模板

text
<epistemic_rules>
1. 区分知识来源,每条陈述标注:
   [观察] 上下文/工具结果中直接可见
   [推断] 基于观察的推理(必须说明依据)
   [通识] 训练知识(可能过时/有误)
   [未知] 不知道

2. 涉及以下内容必须实际验证,不得凭记忆:
   - API 签名与参数
   - 依赖版本
   - 代码运行结果
   - 任何"当前/最新"类信息

3. 找不到依据时,输出「无法确认:<具体缺什么>」。
   输出「不知道」优于输出一个不确定的答案。

4. 引用上下文时必须给出精确位置(文件:行号 / 文档:章节)。

5. 禁止用通用知识补充上下文中不存在的项目特定信息。
</epistemic_rules>

把它放进 CLAUDE.md[Custom Instructions] (自定义指令),长期生效。


七、期望管理

即使做到以上全部,幻觉仍然会发生,只是概率大幅降低且更易检测。

最终的防线永远是

风险等级验证要求
会进生产的代码必须有自动化测试通过
对外的事实陈述必须逐条核对来源
财务/法律/医疗相关必须由专业人士审核
一次性的探索/头脑风暴可以放松
内部工具、原型中等验证即可

不要因为输出看起来专业就降低验证标准。流畅度和正确性是两回事。


延伸阅读

基于 VitePress 构建 · 内容采用原作者授权