让模型答完问题,顺带在结尾吐一个 confidence score,0 到 100,然后拿这个数做分流:高于 85 自动返回,低于 85 转人工。我见过不止一个团队真这么干。听起来有模有样。实际上这是拿一支从没校对过的温度计在量体温,量出来 38 度 5 就给人开退烧药。
前阵子有篇博客在 HN 首页挂了一阵,专门讲这个,结论说 LLM 生成置信度不可信。结论对,但根没挖够。大多数人以为那个数字是模型从自己内部的“把握仪表”上读出来的。完全不是。LLM 生成“0.85”这串字符的方式,跟它生成前面那串答案的方式一模一样:自回归,逐 token,续写。它是一个词一个词“写”出来的,不是“量”出来的。
这段我慢点讲。预训练的时候模型见过海量文本里“confidence: 95%”这种片段。它学到的是:这类数字通常出现在一段陈述之后,通常在某个范围里,通常前面有“显然”“我确定”“答案是”这种前置语。但它从来没有在任何一个训练步骤里学过另一件事——这个数字必须等于将来被检验时的正确率。next-token prediction 的优化目标里没有 calibration 这一项,从来没有。所以那 0.85 到底是什么?是当前上下文里“最像置信度的一个续写”。不是测量值,是文本。一个从来没对标过正确率的数字,你把它当把握度放进工作流里去卡人的决策,这是赌博。
有人会说新模型应该好一点。可能。我也有段时间没逐条测了。要说服我,拿校准数据来,别拿发布会 demo。
先看症状。症状不会骗人。
同一个问题问两遍,第一遍给你 0.85,第二遍 0.7,中间什么都没变,就随机种子换了一轮。真实的测量不会这样抖动——你在同一支温度计上量同一杯水,读两次不会差出 15%。
措辞也影响它。你问“你有把握吗”,它回 0.8;换个说法“你确定吗”,它改口 0.65。报告把握?察言观色还差不多。一个不会因为问法而变的数才配叫测量值;会变的,叫续写。
最要命的是前文。你在问题里先写一句“在我看来这显然是对的答案”,它后面几乎必然给你 0.9 起步。因为续写的目标是一致性,不是准确性。它配合你的语气生成一个“你应该想看到的数字”。这不是缺陷,这是 next-token prediction 的本能——你给它一个高把握的前奏,它给你一个高把握的高潮。我在一个项目里干过这事,同一道题改了一句引导语,分数从 0.72 跳到 0.93,答案一个字没变。看到这个结果我就知道,这数字一根毛都不能用来做决策。
还有一个更隐蔽的问题,比抖动和措辞依赖更麻烦。它输出的数字永远精确。0.87 和 0.23 长得一样具体、一样有模有样。一个错误的置信度和一个正确的置信度,不拿到答案去对,你根本分不出来。很多团队用它的方式就是:模型说 0.9 就放行,说 0.4 就转人工。他们从不回头统计——这 0.9 的档实际对了多少?0.4 的档又对了多少?绝大多数人跳过这一步,因为他们要的本来就不是可信的数字,他们要的是“一个数字”。买了把尺子从来不校对,却到处拿去划尺寸,同一回事。
结论不复杂:LLM 自己报的置信度,不能当置信度用。
那怎么办?办法不是再换个更会“报数”的模型,而是把置信度从模型里拿出来,放在外面算。这背后是个很老的工程原则:任何需要拿去负责的测量值,都必须是可审计的,必须能说清楚它是怎么来的。模型给不了你审计链,但你自己能。
拿一个客服 support agent 的例子讲。不是现炒的例子,是我这两年反复在用的思路:模型负责查帮助中心、起草回复,置信度由外面一套规则来算。
第一步,选特征。必须是可观察、可验证的特征。这个回复匹配到帮助文档没有?匹配了几篇?类似问法历史上处理过没有?同一个问题让模型独立跑两遍,答案一致吗?这些特征都是模型之外的事实,日志里查得到、几行代码就算得出来,不依赖模型怎么“感觉”。
第二步,定 base rate。不是拍脑袋,是从已有记录里统计出来的。过去 200 条回复审下来 150 条对,那一条新回复的起步置信度就是 75%。没别的证据进来之前,75% 已经是最好的估计。
第三步,按特征分组回溯。把历史记录按特征组合分桶,每个桶的置信度等于这个桶的观察准确率。比如“匹配到文档且两次运行一致”那组,历史上 95% 是对的;“没匹配到文档”那组,只有 40% 对。那一条新回复的置信度,不是模型拍出来的,是它所属那个桶的历史准确率。最小实现几行就够:
history = {
(True, True): [True] * 95 + [False] * 5, # 匹配且两次一致
(False, False): [True] * 40 + [False] * 60 # 无匹配
}
def rule_confidence(features):
key = (features["has_match"], features["two_runs_agree"])
bucket = history.get(key)
if not bucket:
return 0.75 # 没历史,回落到 base rate
return sum(bucket) / len(bucket)
95% 和 40% 这两个数哪来的?就是你上一步回溯出来的,不是谁预设的。它来自记录,不来自模型。这个顺序不能反。
第四步,周期校准。每周或者每几百条回复跑一次校准表:90% 那一档实际对了多少、70% 那一档又对了多少。一旦发现 90% 的桶实际只对了 76%,就回头修规则——可能某个特征失效了,也可能 base rate 变了。置信度从来不是一次拍死的事,是持续维护的工程活。谁跟你说“这个数跑一次就永远准”,谁就是在骗你。
这套东西跟 LLM 自报的分数差别在哪?同样输入给同样数,跟措辞无关;校准的是你们自己那类问题,不是满世界压箱底文本;记录越滚越厚,数越滚越准。但有个不能忽略的前提:你必须记、必须复查。日志里不记特征、不记规则给的多少分、不记后来实际上对不对,那过俩月你手里拿的又是一个没测量过的数字,跟模型直接吐出来的没有本质区别。这一步欠的债,迟早在生产上要回来。
回到最开头那句话。LLM 给你的置信度是写的,不是量的。工具能替你产出很多东西,唯独不能替你为后果负责。置信度这事就是把“负责”具体化——拿它决定放行还是人工、决定这个订单能不能自动处理,这个数就必须有据可查。模型说多少不算,你算出来多少才算。
我的态度很明确:LLM 输出的 confidence score,别拿来做任何需要承担后果的决策,一个也别。模型干它生成回复的活,置信度由外面这套规则来算,两者分开。先把特征、base rate、分组回溯、周期校准这四件事做起来,再谈怎么用 LLM 提效。顺序反过来就是本末倒置——把一个设计成续写的数字当成工程设计里最要紧的读数在用。这话我承认说得有点重。但道理没变——半桶水晃荡最响的时候,往往就是该停下来补基本功的时候。先把“置信度必须可审计”这条基本功补上,再谈别的。
