如何透過模型共識讓 LLM 的數值判斷變得安全
單一模型可能會產生看似正確實則錯誤的數字。請交叉比對兩個獨立的模型,並在程式碼中重新計算結果,然後才加以信任。
一個總結段落的語言模型,即使有點錯誤也仍然有用。一個計算閾值、驗證金額或檢查兩個數字是否一致的模型,則沒有那樣的寬容度。數字要嘛是正確的,要嘛就不是,而一個看似合理的錯誤數字比一個明顯的錯誤更糟糕,因為它能通過審查。這篇文章描述了一種模式,將模型的數字輸出視為待查證的聲明,而非事實的來源:透過兩個獨立的模型運行相同的判斷,只有當它們完全一致時才接受該數字,並且在它影響到任何重要事物之前,用純程式碼重新計算結果。
語言是寬容的,數字則不然
大型語言模型(LLM)擅長的大部分事情都存在於一個容錯空間中。如果摘要遺漏了細微差別,或改寫時選了一個稍微奇怪的詞,讀者仍然能理解其意,且下游的任何環節都不會出錯。自然語言內建了冗餘性,因此小錯誤會被沖淡。
數值判斷則沒有那樣的彈性空間。當模型判定一筆 1,050,000 的轉帳在 1,000,000 的限額內,或者一個折扣價應四捨五入到某個值,而實際上應是另一個值時,是沒有部分分數可言的。輸出會饋入閘門、帳本或控制流程分支,一個錯誤的數字就會完全改變結果。
造成這種危險的失敗模式是具體且可重複的:
- 幻覺般的自信。無論數字是正確推導出來的還是憑空捏造的,模型都以同樣流暢肯定的語氣陳述。從語氣中無法判斷是哪種情況。
- 數字與數量級錯誤。模型可能會漏掉一個零、對調兩個數字,或得出一個差了一個數量級的答案,而周圍的句子讀起來卻完全通順。
- 單位與貨幣混淆。將百分比讀成小數,將分鐘讀成秒,將一種貨幣當成另一種。其算術在內部是一致的,但因為單位錯誤,結果仍然是錯的。
- 邊界錯誤。在閾值處出現差一錯誤(off-by-one),或將包含性限制視為排除性限制,導致應被拒絕的值得以通過。
這些正是流暢的單一答案最能隱藏的錯誤。句子結構良好,推理聽起來正確,但數字卻是錯的。
將模型輸出視為一種主張,而非答案
核心的轉變是,停止向模型索取答案,並開始要求它提出一個你接著會去驗證的主張。四條規則能將一個機率生成器轉變為一個安全閘門。
- 交叉比對兩個獨立的模型,並且只在它們意見一致時才接受。
- 在確定性程式碼中重新計算數字,並拒絕任何不符之處。
- 強制使用結構化輸出,以便能精確地比較答案。
- 將模型隔離,使其只做純粹的判斷,不使用工具,也沒有外部狀態。
這些方法沒有一個是單獨信任模型的。每一個都假設輸出可能是錯的,並圍繞這個假設建立一個檢查機制。這篇文章的其餘部分將會逐一探討這些規則。
共識閘門:達成一致或停止
第一條規則是「全體一致」的閘門,而非投票。將完全相同的判斷送往兩個獨立的模型(兩者之間沒有共享的上下文),並比較它們的答案。如果兩者產生相同的數字和單位,則視為暫時接受。如果它們有任何差異,就停止並將該案例擱置,交由人工或備用路徑處理。
「全體一致」這個詞很重要。這不是多數決,不是最常見的答案獲勝。在數字安全性方面,不一致是整個流程中最有價值的輸出。這意味著至少有一個模型在這裡出錯了,而你還無法判斷是哪一個。選擇多數方來解決這種分歧,就等於丟棄了你花費代價換來的精確信號。閘門應採安全關閉機制:有疑慮時,就不讓該數字通過。
獨立性是這個閘門力量的來源。來自同一家族的兩個模型,或對同一個模型查詢兩次,往往會在同一個地方犯同樣的錯誤,因此它們之間的一致性幾乎無法證明什麼。兩個在不同資料上訓練的真正不同的模型,有著不同的盲點,因此兩者偶然產生完全相同的錯誤數字的可能性要低得多。你不是在計票,你是在檢查兩個獨立的估計器是否落在同一個點上。
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),這需要由人工或備用方案(fallback)來處理。對於大量、低價值的判斷流,這些額外開銷可能會超過其效益。
所以,請根據風險高低來匹配模式。當一個錯誤數字的代價高昂且難以挽回時,完整的關卡就值回成本:
- 金錢路徑。金額、限額、退款,任何移動價值或管制交易的事物。
- 門檻與控制。一個計算出的截止值,用來決定某事是否被允許。
- 對帳(Reconciliation)。檢查兩個獨立的數字是否一致,若錯誤地通過,會隱藏實際的斷點。
- 單向輸出。一個被寫入帳本或傳送給合作夥伴,且事後無法悄悄更正的數字。
對於相反的情況,就跳過它。一個模型建議一個粗略估計供人快速檢視、一份報告的摘要、一份反正會有人編輯的草稿,這些都不需要兩個模型和一個小數檢查器。在一個一次性的估計上運行完整的關卡,是浪費延遲和開銷。
誠實地說,其限制在於共同的盲點。共識降低了出現錯誤數字的機率,但並未將其降至零,因為兩個模型可能都基於同樣有缺陷的模式進行訓練,並一起重複這個錯誤。這就是為什麼確定性重檢(deterministic recheck)會設在關卡之後,以及為什麼會有人工留在意見不合的路徑上。模型擅長產生候選答案,但在保證其精確性方面卻很弱。共識、確定性重算(deterministic recomputation)以及故障關閉處理(fail-closed handling),能將那薄弱的保證轉變為一個你可以放在必須正確的數字前的關卡。