飛得太靠近太陽的假設
1980 年代初期,NASA 面對一個至今仍困擾著安全關鍵工程領域的問題:你該如何容忍那些還沒被發現的漏洞?他們的答案是 N-version programming。把同一份規格交給三個獨立團隊。同時執行三份程式。對輸出結果進行投票。如果其中一個團隊寫出了漏洞,另外兩個會以多數票壓過它。
這聽起來像是數學上的常識。但它也是錯的。
1986 年,John Knight 與 Nancy Leveson 發表了一項由 NASA 資助的大型實驗結果。兩所大學的二十七名研究生針對同一份彈道飛彈攔截器規格撰寫了獨立實作。每份程式單獨來看都極度可靠。有六個版本從未失敗過。二十三個版本在一百萬筆隨機產生的測試案例中,通過率超過 99.9%。
但當多個版本在同一筆輸入上失敗時,它們集體失敗的頻率遠高於統計獨立性所預測的結果。z-score 高達 100.55。在 99% 信賴區間下,獨立性的虛無假設被徹底擊潰。NASA 賭的是人為錯誤是隨機的。Knight 與 Leveson 證明了它會聚集。
N-Version Programming 到底承諾了什麼
N-version programming 背後的邏輯借鏡自硬體冗餘。如果你在火箭上綁三個相同的陀螺儀,其中一個飄移了,另外兩個會以多數票壓過那個飄移。失敗是獨立的,因為陀螺儀是實體物件,各自承受獨立的製造變異、溫度梯度與振動模式。
軟體則不同。當你要求三個團隊解決同一個問題時,你得到的並非三次獨立的骰子擲出。你得到的是三個讀過同一份模糊規格、從同一本教科書學到相同演算法、並使用同一種語言與同一套標準函式庫寫程式的人類。他們的錯誤會相關,因為他們的輸入本身就相關。
N-version 架構在實務上看起來像這樣:
from typing import Callable, List, TypeVar
T = TypeVar('T')
def n_version_vote(
implementations: List[Callable[[float], T]],
input_value: float
) -> T:
"""Run N versions and return the majority output."""
results = [impl(input_value) for impl in implementations]
# Simple majority vote
from collections import Counter
counts = Counter(str(r) for r in results)
most_common = counts.most_common(1)[0][0]
# Return the actual value that matched the winning string
for r in results:
if str(r) == most_common:
return r
raise RuntimeError("No consensus")
這個 voter 是簡單的部分。困難的部分藏在它裡頭的假設:implementations[0]、implementations[1] 與 implementations[2] 會在輸入空間中不相關的子集上失敗。Knight 與 Leveson 證明了這個假設本身就是缺陷。
相關失敗從何而來
Knight-Leveson 實驗揭示了共同模式失敗的兩種不同機制,而兩者都無法靠要求團隊「更努力」來解決。
第一種是規格模糊性(specification ambiguity)。這份飛彈攔截器規格中包含了一些真正困難的邊界案例。二十七名程式設計師中,有八位錯誤處理了三個雷達點共線的情況。錯誤本身各不相同。一位在陣列索引中犯了 off-by-one 錯誤。一位使用了數值穩定性不佳的演算法。一位則完全遺漏了邊界案例。但他們的失敗都聚集在相同的困難輸入上,因為問題本身就很困難,而不是因為程式設計師粗心。
這就是「困難因子」(difficulty factor)。當輸入落在邊界條件上、需要棘手的 floating-point 運算、或涉及未明確定義的狀態轉換時,獨立團隊傾向於在同一個地方跌倒。他們的解法不同。但他們的失敗區域重疊。
第二種機制是共享的心智模型。受過相同課程訓練的程式設計師會套用相同的啟發法。他們會伸手去拿相同的排序演算法、相同的防禦性複製模式、相同的 epsilon 比較來判斷 floating-point 相等性。當這個共享的預設值對眼前的問題是錯誤的時候,每個團隊都會一起走向懸崖。
NASA 自家的《Software Safety Guidebook》最終也明確承認了這一點。書中指出「許多專業人士認為 N-Version programming 無效,甚至適得其反」。在一項 NASA 針對實驗飛行器的研究中,測試期間發現的每一個軟體問題都來自冗餘管理系統。主飛行控制軟體完美無缺。N-version 層反而是唯一會出錯的東西。
沒人談論的複雜度稅
即使 N-version programming 真的像宣稱的那樣運作,它也會索取沉重的代價。你要支付三份完整實作的費用。你要支付 voter 本身的邏輯成本。你要支付執行三個行程並協調它們輸出的營運開銷。
那個 voter 絕非瑣碎的程式碼。它必須處理平手、逾時、分歧的輸出格式,以及多數本身是錯誤的情況。Brunelle 與 Eckhardt 在 1985 年就以 SIFT 作業系統證明了這一點。兩個新的 N-version 以多數票壓過了原本正確的實作,並產生了錯誤的答案。這個冗餘系統製造了它本應預防的錯誤。
複雜度以難以量化的方式層層疊加,直到它反咬你一口。你現在有三個可部署版本要管理、三個測試矩陣要維護,以及三個團隊會在需求無可避免地變動時,對規格變更做出不同的詮釋。人為錯誤的表面積增長速度,比可靠性的提升還要快。
真正有效的替代方案
NASA 並沒有放棄容錯。他們放棄的是「多樣性來自獨立性」這個特定假設。現代高保證系統使用的是一套能夠處理 Knight 與 Leveson 所識別出的實際失效模式的技術組合。
**關鍵路徑上的形式化方法(Formal methods)。**與其祈禱三個團隊正確解讀規格,不如把規格寫成機器可驗證的形式。TLA+、Coq 與 SPIN 等工具能在任何人撰寫實作程式碼之前,就驗證設計是否符合其不變條件。NASA 自家的 Remote Agent Experiment 就使用 SPIN 找出了大量測試都未能發現的並發漏洞。
**透過相異技術堆疊達成的多樣性。**如果你需要冗餘,就要做得徹底。Airbus A330 的飛行控制系統在主通道與備援通道之間使用了相異的硬體架構、不同的程式語言,以及獨立的編譯器。目標不僅僅是獨立的團隊,而是堆疊每一層都擁有獨立的失效模式。
**簡化勝過複製。**NASA《Software Safety Guidebook》最終建議,N-version programming 只適用於「小型簡單函式」。這個教訓不華麗但有效:最可靠的系統,是那個簡單到可以直接推理的系統。每一行冗餘管理程式碼都是可能失敗的一行。
以下是簡化、以規格驅動的做法在實務中的樣貌。與其讓三個不透明的實作對輸出投票,不如把關鍵不變條件明確編碼,並在執行期檢查:
from dataclasses import dataclass
from typing import Optional
@dataclass(frozen=True)
class LaunchCommand:
thrust_level: float # 0.0 to 1.0
abort_flag: bool
def is_valid(self) -> bool:
"""Runtime invariant check derived from the formal spec."""
if not (0.0 <= self.thrust_level <= 1.0):
return False
if self.abort_flag and self.thrust_level > 0.0:
return False
return True
def execute_command(cmd: LaunchCommand) -> Optional[str]:
if not cmd.is_valid():
raise ValueError("Invariant violation: command violates safety spec")
# Execute only after the single, explicit check passes.
return f"Executing thrust={cmd.thrust_level}, abort={cmd.abort_flag}"
這不是 N-version programming。這是一份實作搭配一個明確、可測試的安全邊界。你的力氣應該花在邊界上。而不是花在指望三個團隊中有兩個做對。
現代的回聲
Knight 與 Leveson 1986 年的論文帶著一個每年愈發相關的警告。相關失敗不需要相關團隊。它們只需要相關的輸入與相關的困難度。隨著 AI 輔助編碼工具的普及,我們正在全球規模上進行一場新的 N-version 實驗。在重疊語料庫上訓練、被餵以相似模式的模型,會產生具有共享失效模式的程式碼。
近期針對 LLM 程式碼生成的研究顯示,AI 生成元件的共同錯誤率介於 15% 到 30% 之間。beta factor——歸因於共同原因而導致的失敗比例——可能已經超過 Knight 與 Leveson 測量到的人類程式設計師數值。我們正以更多的參與者、以及同一個有缺陷的假設,重複這場實驗。
NASA 是以昂貴的方式學到這一課的。你不需要三份實作。你需要的是一份實作,搭配一個你能夠陳述、檢查並信任的不變條件。其他的一切,都只是披上工程外衣的樂觀主義。
FAQ
什麼是 N-version programming?
N-version programming 是一種軟體容錯技術,由多個獨立團隊根據同一份規格進行實作。這些程式並行執行,再由一個 voter 選出多數輸出,其假設是獨立團隊會犯下獨立的錯誤。
NASA 真的用過 N-version programming 嗎?
是的。NASA 曾資助多版本軟體的早期研究,某些政策文件甚至在某個時期實際上強制要求容錯系統採用 N-version programming。太空梭(Space Shuttle)的引擎啟動序列就曾使用這項技術,不過 NASA 後來在實證研究揭露其限制之後,將其使用範圍限縮到小型、簡單的函式。
Knight-Leveson 實驗是什麼?
1986 年,John Knight 與 Nancy Leveson 讓二十七名程式設計師針對一份飛彈攔截器規格撰寫獨立實作。在一百多萬筆測試中,這些程式出現的共同失敗次數顯著高於統計獨立性所預測的結果,從而推翻了 N-version programming 的核心假設。
N-version programming 是否仍有值得使用的時候?
在有限的情況下可以。NASA 目前的指引認為,它可能適用於能夠強制實現真正多樣性的小型、定義明確的函式。對於一般應用程式開發而言,複雜度成本與相關失敗風險超過了效益。形式化方法、執行期不變條件檢查與簡化,通常是更好的投資。