如何利用模型共识保障 LLM 数值判断的安全性
单个模型可能会产生看似正确但实则错误的数字。在你信任它之前,请交叉核对两个独立的模型,并在代码中重新计算结果。
一个总结段落的语言模型,即使有点小错也仍然有用。一个计算阈值、验证金额或检查两个数字是否一致的模型则没有这种宽容度。数字要么正确,要么不正确,而一个看似合理的错误数字比一个显而易见的错误数字更糟糕,因为它能通过审查。这篇文章描述了一种模式,它将模型的数字输出视为待核实的声明,而非事实来源:通过两个独立的模型运行相同的判断,仅在它们完全一致时才接受该数字,并在该结果触及任何重要事物之前,用普通代码重新计算它。
语言是宽容的,数字则不然
LLM 擅长的大部分工作都存在于一个容错空间中。如果摘要遗漏了细微差别,或者改写时选用了稍显奇怪的词语,读者仍然能理解其意,下游环节也不会出问题。自然语言具有内在的冗余性,因此小错误会被冲淡。
数字判断则没有这种余地。当模型判定 1,050,000 的转账在 1,000,000 的限额内,或者折扣价应四舍五入为一个值而实际却舍入为另一个值时,不存在部分得分的情况。输出会馈入一个门控、一本分类账或一个控制流分支,一个错误的数字就会彻底改变结果。
导致这种危险的故障模式是具体且可复现的:
- 幻觉般的自信。模型无论是正确推导出数字还是凭空捏造,都会以同样流畅肯定的语气陈述。从语气中无法判断是哪种情况。
- 数字和量级错误。模型可能会漏掉一个零,调换两个数字的位置,或者得出一个与正确答案相差一个数量级的答案,而周围的句子读起来却完全通顺。
- 单位和货币混淆。将百分比读作分数,将分钟读作秒,将一种货币当作另一种货币。其算术在内部是自洽的,但由于单位错误,结果仍然是错误的。
- 边界错误。在阈值处出现差一行为,或将包含性限制当作排他性限制处理,从而导致本应被拒绝的值通过。
这些正是流畅的单一答案最能掩盖的错误。句子结构良好,推理听起来正确,但数字却是错的。
将模型输出视为一项声明,而非一个答案
核心的转变是,不再向模型索要答案,而是开始向它索要一项你随后会去验证的声明。四条规则将一个概率性生成器转变为一个安全的门控。
- 交叉核对两个独立的模型,并且仅在它们一致时才接受。
- 在确定性代码中重新计算数字,并拒绝任何不匹配的结果。
- 强制使用结构化输出,以便可以精确地比较答案。
- 将模型隔离,使其只做纯粹的判断,不使用工具,也没有外部状态。
这些方法没有一个是单独信任模型的。每一种方法都假设输出可能是错误的,并围绕这一假设建立一个检查机制。本文的其余部分将逐一介绍它们。
共识门:一致或停止
第一条规则是一致性门,而不是投票。将相同的判断任务发送给两个独立的模型,它们之间没有共享的上下文,然后比较它们的答案。如果两者都产生相同的数值和单位,就将其视为临时接受。如果它们有任何差异,就停止并将该案例搁置,以待人工处理或转入后备路径。
“全体一致”这个词很重要。这不是少数服从多数、最常见答案获胜的规则。在数字安全性方面,不一致是整个过程中最有价值的输出。这意味着至少有一个模型在这里出错了,而你还无法判断是哪一个。以支持多数派的方式解决这种分歧,会丢掉你花钱买来的精确信号。这个门应该以安全关闭的方式失败:当有疑问时,不要让该数字通过。
独立性是这个门的力量所在。来自同一家族的两个模型,或同一个模型被查询两次,往往会在同一个地方犯同样的错误,因此它们之间的一致性几乎不能证明什么。两个在不同数据上训练的、真正不同的模型有不同的盲点,因此,两者偶然产生完全相同的错误数字的可能性要小得多。你不是在计票,你是在检查两个独立的估算器是否落在同一点上。
def consensus_gate(task, model_a, model_b):
a = model_a.judge(task) # structured: {"value": ..., "unit": ...}
b = model_b.judge(task) # same task, no shared context
if a is None or b is None:
return HOLD # a model failed to answer, fail closed
if a.value != b.value or a.unit != b.unit:
return HOLD # disagreement is a risk signal, escalate
return Accept(a.value, a.unit) # both agree, provisional accept
请注意,此处的一致意见仅为临时接受,而非最终接受。两个模型可能共享同一个盲点,从而一起犯错。门控降低了这种可能性,但并不能完全消除它,这就是下一条规则存在的原因。
在确定性代码中重新计算数字
共识能过滤掉大多数单一模型的错误,但它仍然让你去相信一个由模型产生的数字。第二条规则消除了这种信任:每当答案可以通过普通计算得出时,就去计算它,并且只在真正需要判断的部分使用模型。
一个有用的思考方式是分工。模型擅长处理问题模糊的前端部分:读取杂乱的输入、决定哪些行是相关的、对模糊的字段进行分类。它不擅长保证总数是精确的。所以,让模型来做读取和选择的工作,然后自己进行算术计算,并用你自己的结果来核对模型的主张。
确定性层应该强制执行三种检查。将值解析为精确类型,对于金额,绝不使用浮点数。对照已知边界对其进行范围检查。并且断言那些无论任何模型说了什么都必须成立的不变量,例如部分加总等于整体。
from decimal import Decimal
def verify(value, unit, source_rows):
# 1. parse into an exact type, never a float for money
amount = Decimal(value)
# 2. range check against known bounds
if not (MIN_AMOUNT <= amount <= MAX_AMOUNT):
raise Reject("value is outside the allowed range")
# 3. invariant: the parts must sum to the whole
computed = sum(Decimal(r.amount) for r in source_rows)
if amount != computed:
raise Reject(f"sum mismatch: model said {amount}, code computed {computed}")
# 4. unit must match what the pipeline expects
if unit != EXPECTED_UNIT:
raise Reject(f"unexpected unit {unit}")
return amount
不匹配分支是重要的一环。当重新计算的总数与模型的数值不一致时,代码不会取二者的平均值,也不会偏好任何一方,而是会予以拒绝。出现差异意味着,要么是模型有误,要么是模型读取的输入与代码读取的输入不一致。这两种情况都需要人工介入,才能交付任何数值。这一层可以捕获两个模型碰巧都出现的数量级偏差。
强制结构化输出,以便可以比较数字
如果模型将数字埋藏在句子中,你就无法比较两个答案,也无法重新计算其中一个。第三条规则是要求使用固定模式的结构化输出,并禁止在承载值的字段中使用自由散文。
一个模式可以同时做三件事。它使共识门中的比较成为对类型化字段的精确匹配,而不是对英语的脆弱解析。它为确定性层提供一个干净的值来检查,而不是一个它必须解释的短语。它还缩小了模型可以发挥创造力的表面,而这正是数字漂移悄悄潜入的地方。
要求输出格式如下,并拒绝任何不符合格式的内容:
{
"value": "1050000",
"unit": "KRW",
"basis": "sum_of_line_items",
"confidence": "high"
}
在传输过程中将数值保留为字符串,并在到达时将其解析为精确的小数类型,这样在模型和您的检查之间就不会出现浮点舍入误差。如果响应缺少字段、添加了评论或用解释包裹数字,则将其视为失败的响应,与不一致的情况相同。不按要求格式作答的模型,视为未作答。
将模型与工具和状态隔离
最后一条规则是关于模型被允许接触的内容。对于数值判断,只向其提供输入和问题。不提供工具,不提供调用其他系统的能力,不与另一个审查者共享内存,也不提供对任何内容的写访问权限。
隔离带来了两个好处。它使两个模型真正保持独立,因为两者都无法看到对方的推理,也无法锚定于一个共享的中间结果,而这正是使其一致性有意义的原因。并且它将爆炸半径保持为零:一个只能返回结构化声明的模型无法根据错误的数字采取行动,它只能提出一个数字,并且每个提议在任何事情发生之前都会通过关卡和复核。模型是传感器,而不是执行器。它报告一个读数,然后由代码决定如何处理它。
单一模型与共识门
该模式增加了活动部件,因此有助于清楚地了解这些部件相对于单次调用所带来的好处。
| 维度 | 单一模型 | 双模型共识门 |
|---|---|---|
| 无提示的错误数字 | 流向下一环节 | 除非两者都同意且复核通过,否则会被阻止 |
| 自信的幻觉 | 通常被接受 | 除非两者出现完全相同的错误,否则会被捕获 |
| 每次判断的成本 | 一次模型调用 | 两次或更多次模型调用 |
| 延迟 | 一个模型 | 两者中较慢的一个 |
| 出现分歧时 | 无从察觉 | 被标记并交由人工处理 |
| 最适合 | 可逆、低风险的文本 | 金钱、阈值、对账 |
对于并非所有任务来说,单一模型这一行都是错误的。但对于那些一个看似合理的错误数字会造成实际损害的任务来说,它就是错误的。
成本与跳过时机
门禁并非免费。每次判断,你都需要为两次或更多的模型调用付费,而不是一次,并且需要等待其中最慢的一个返回结果,因为它们是并行运行的,但最终的步调由最晚完成的那个决定。同样也存在工程成本:需要维护一个 schema,编写一个 deterministic checker,以及一个用于处理分歧的 hold path,这些分歧需要由人工或备用方案来处理。对于大批量、低价值的判断流,这些开销可能会超过其带来的好处。
因此,请根据风险大小来匹配模式。当错误的数字代价高昂且难以撤销时,完整的门禁机制就物有所值:
- 资金路径。金额、限额、退款,任何转移价值或控制交易的环节。
- 阈值和控制。一个计算出的、用于决定某事是否被允许的截止值。
- 对账。检查两个独立的数字是否一致,错误的通过会掩盖真实存在的问题。
- 单向输出。一个被写入分类账或发送给合作伙伴且之后无法悄悄更正的数字。
对于相反的情况,则可以跳过它。一个为人类提供粗略估算以供目测的模型、一份报告的摘要、一份反正会由人来编辑的草稿——这些都不需要两个模型和一个 decimal checker。在一次性的估算上运行完整的门禁机制,是在浪费延迟和开销。
其真正的局限在于共同的盲点。共识降低了出现错误数字的几率,但并不能将其降至零,因为两个模型可能基于同样有缺陷的模式进行训练,并一同重复这个错误。这也就是为什么 deterministic recheck 位于门禁之后,以及为什么需要人工留在 disagreement path 上。模型擅长生成候选答案,但不擅长保证其精确性。共识、deterministic recomputation 和 fail-closed handling,将这种弱保证转变为一道门禁,你可以将其置于一个必须正确的数字之前。