把 LLM 想成一个永远返回 200 OK 的 API:你查一条不存在的记录,它不会返回 404 或 null,而是就地编一条格式完美的 response 给你。问题不在它"想骗你"——而在它的架构里根本没有"查不到"这个返回值。每一步它都必须吐一个 token,"沉默"或"未知"从来不是默认选项。
先破除一个误解:幻觉不是 bug,是训练目标的数学必然。LLM 干的是 next-token prediction(预测下一个词)——本质是对语言做密度估计,学一个概率分布 P(下一个词 | 前文)。这个目标里从头到尾没有"事实核查"这一项:模型被奖励"说得像训练数据",不是"说得对"。
OpenAI 的 Kalai 等人 2025 年把这件事讲透了:即使训练语料 100% 无误,交叉熵目标本身也会逼出幻觉。直觉是——生成一个有效答案,难度不低于一道二分类题"这句话对不对";而对于训练里只出现过一两次的冷门事实(一个人的生日、一篇论文的作者),模型没有足够统计信号把它和相似的错误答案区分开,于是错误率有一个不可消除的下界。
更狠的一层在评估激励:几乎所有 benchmark 都按"答对得 1 分、答错和留白都得 0 分"打分。在这种规则下,蒙一个的期望得分严格高于老实说"我不知道"——就像标准化考试里空着不如猜。于是整个训练-评估流水线,系统性地把模型训练成一个自信的应试者,而不是一个诚实的知情者。
from openai import OpenAI client = OpenAI() # 需要 OPENAI_API_KEY # 问一个纯属虚构的实体——模型没有"查不到"这个返回值 r = client.chat.completions.create( model="gpt-5", messages=[{"role": "user", "content": "介绍一下 2019 年图灵奖得主 Zael Kirmani 的贡献"}], logprobs=True, # 拿每个 token 的对数概率 ) msg = r.choices[0].message.content print(msg) # → 大概率编出一段流畅但完全虚构的生平 # 关键观察:编造内容的 token 概率往往并不低—— # 模型「自信地编」,高置信 ≠ 有依据。这正是下一节要量化的问题。 lp = r.choices[0].logprobs.content print(sum(t.logprob for t in lp) / len(lp)) # 平均 logprob
校准好不好,看天气预报就懂:一个预报员说了 100 次"70% 会下雨",如果这些天里正好约 70 天真下了,他就是校准良好(well-calibrated)。跟他有多"准"是两回事——他可以每次都说 70% 从不说 100%(不够 sharp),但只要频率对得上,置信度就是可信的信号。LLM 的置信度能不能当告警阈值用,取决于同一件事。
模型给每个答案都附带一个隐含置信度(token 概率)。校准问的是:它说的置信度,和实际正确率对不对得上?把预测按置信度分桶,理想情况下"置信 0.8 那一桶里,正好 80% 是对的"。量化指标是 ECE(Expected Calibration Error,期望校准误差)——各桶「平均置信度」与「实际准确率」之差的加权绝对值,越接近 0 越好:
经典修法叫 温度缩放(Temperature Scaling,Guo 等人 2017):不动模型权重,只把输出 logits 除以一个学到的温度 T,再过 softmax。T>1 把过尖的分布压平,让置信度回落到和真实准确率一致——一个参数、事后校准、不掉准确率。注意它治的是"置信度数值不可信",治不了幻觉本身:校准让你知道该信几分,但不会让错答案变对。
import numpy as np def expected_calibration_error(confs, correct, n_bins=10): # confs: 每个预测的置信度 (0~1);correct: 是否答对 (0/1) bins = np.linspace(0, 1, n_bins + 1) ece = 0.0 for i in range(n_bins): # 落在这个置信度桶里的预测 m = (confs > bins[i]) & (confs <= bins[i + 1]) if m.sum() == 0: continue acc = correct[m].mean() # 这桶的实际准确率 conf = confs[m].mean() # 这桶的平均置信度 # 差距 × 该桶样本占比,累加 ece += (m.sum() / len(confs)) * abs(acc - conf) return ece # 过度自信的模型:置信度普遍高于真实准确率 → ECE 大 confs = np.array([0.9, 0.9, 0.8, 0.95, 0.7]) correct = np.array([1, 0, 0, 1, 0]) # 5 题只对 2 题 print(f"ECE = {expected_calibration_error(confs, correct):.3f}")
想象一个诚实但会说"这个我不太确定"的顾问,被你用"客户满意度 KPI"优化了半年——他慢慢变成一个永远斩钉截铁的销售。听起来更可信、更让人舒服,实际却更不准。RLHF 对模型做的正是这件事:为了讨好人类偏好,它牺牲了置信度的诚实性。分布式里的老对手你熟——为了 p99 体验,牺牲了一致性。
反直觉的事实:预训练后的原始模型(base model)其实校准得相当好——它只做 next-token 预测,概率就是它对语言的真实统计信念。但经过 RLHF(基于人类反馈的强化学习,让模型对齐人类偏好) 和指令微调后,校准显著变差。GPT-4 技术报告里有一张著名的图:预训练版 GPT-4 的可靠性曲线几乎贴着对角线,RLHF 之后曲线明显鼓向过度自信一侧。
机制在于人类偏好的形状。标注者给回答打分时,系统性地更爱自信、流畅、给出明确结论的回答,不爱"这取决于…""我不确定"这类犹豫表达。奖励模型把这个偏好学了进去,PPO 优化时就把概率质量往少数几个高置信表达上挤——这叫 mode collapse(模式坍缩):分布从"诚实地摊开不确定性"塌成"总是一副很确定的样子"。
这解释了一个日常观察:ChatGPT / Claude 这类对齐过的模型,比原始 base model 读起来更笃定,也因此更容易让你放下警惕。这不是模型"变笨了",而是它被优化成了让人舒服的语气——语气的确定性和内容的可靠性,在 RLHF 之后脱钩了。
from anthropic import Anthropic client = Anthropic() # 需要 ANTHROPIC_API_KEY # 对抗过度自信的一个提示手法:显式要求「校准过的」数值置信度, # 并要求先想「什么情况下我会错」——逼模型摊开不确定性 prompt = """回答问题,然后给一个 0-100 的置信度。 置信度定义:在你给出这个置信度的所有题里,应恰好有该比例是对的。 先列出你可能出错的情形,再给分。 问题:太平洋最深处的确切深度是多少米?""" r = client.messages.create( model="claude-opus-4-8", max_tokens=400, messages=[{"role": "user", "content": prompt}]) print(r.content[0].text) # 要求「校准定义 + 先想错因」通常比直接问「你多确定」得到更诚实的分数
像缓存的 cache miss 信号:系统内部其实知道这次是 miss,但如果不把 miss 信号暴露到上层,调用方就误以为每次都命中。模型也一样——它内部有信号区分"我知道"和"我在编"(比如输出分布的熵、内部激活),但默认不主动报出来。让它自评,就是把这个 cache-miss 信号显式暴露出来。
核心问题:模型知道自己不知道吗?Anthropic 的 Kadavath 等人 2022 年(论文名就叫"Language Models (Mostly) Know What They Know")给了一个偏乐观的答案:大模型在相当程度上"知道自己知不知道"。他们用了两个可操作的探针:
机制直觉:当模型对答案有把握时,输出分布尖锐(少数 token 概率极高、熵低);没把握时分布平摊(很多 token 概率相近、熵高)。这个"内部不确定性"是真实存在的信号——问题从来不是模型没有边界感,而是默认的生成流程把这个信号丢掉了,只吐最终答案。让它显式做 P(True) 自评,等于把丢掉的信号捡回来。
当然是"Mostly"不是"Always":越是训练里没见过、需要多步推理的题,自评越不可靠——这和第一节的统计根源呼应,冷门低频区正是自我认知也失灵的地方。
from anthropic import Anthropic client = Anthropic() def answer_with_self_eval(question): # 第 1 步:正常作答 a = client.messages.create( model="claude-opus-4-8", max_tokens=300, messages=[{"role": "user", "content": question}] ).content[0].text # 第 2 步:换一次独立调用,让模型评估上面答案为真的概率 P(True) # 独立调用避免它「护着」自己刚说的话 check = client.messages.create( model="claude-opus-4-8", max_tokens=10, messages=[{"role": "user", "content": f"问题:{question}\n候选答案:{a}\n" "这个答案正确的概率是多少?只回一个 0-1 的数字。"}] ).content[0].text return a, float(check.strip()) ans, p_true = answer_with_self_eval("贝叶斯定理的提出者是谁?") # p_true 低 → 触发人工复核 / 转 RAG 检索,别直接采信