主题
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>为什么有效:
- 摘录是可验证的 —— 你
Ctrl+F一搜就知道真假 - 强制注意力真正投向原文,对抗中段衰减
- 「找不到就写无直接证据」提供了一条明确的退出路径
在 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] (自定义指令),长期生效。
七、期望管理
即使做到以上全部,幻觉仍然会发生,只是概率大幅降低且更易检测。
最终的防线永远是:
| 风险等级 | 验证要求 |
|---|---|
| 会进生产的代码 | 必须有自动化测试通过 |
| 对外的事实陈述 | 必须逐条核对来源 |
| 财务/法律/医疗相关 | 必须由专业人士审核 |
| 一次性的探索/头脑风暴 | 可以放松 |
| 内部工具、原型 | 中等验证即可 |
不要因为输出看起来专业就降低验证标准。流畅度和正确性是两回事。