Schema 即防線:為何 LLM 時代的系統穩定性,來自 Runtime 而非編譯期

LLM 的不確定性不會因為你寫了型別就自動消失。本文從 Zod 與 structured output 實務切入,說明為何真正保護 AI 系統穩定性的,不是編譯期宣告,而是 runtime 對結構、欄位與語義的持續驗證。

Schema 即防線:為何 LLM 時代的系統穩定性,來自 Runtime 而非編譯期

大型語言模型(LLM)的輸出本質上具有不確定性,這對追求系統穩定性的軟體工程是一大挑戰。許多開發者習慣用 TypeScript 的靜態型別來約束資料結構,期望獲得編譯期的安全感,但在面對 LLM 時,這種安全感相當脆弱。真正的穩定性,並非來自於我們在 IDE 中撰寫的型別定義,而是來自於系統在運行時(runtime)對每一筆來自 AI 的輸入,進行嚴格的結構與語義驗證。在 LLM 時代,我們必須重新認識 schema 的角色:它不再是靜態的文件,而是一道抵禦非預期輸入、確保系統正常運作的動態防線。

為什麼 TypeScript 的靜態型別還不夠?

在傳統的 Web 開發中,我們習慣與行為可預測的 API 互動。後端回傳的資料結構是固定的,前端只要依照 API 文件定義好 TypeScript 的 interfacetype,就能在開發階段捕捉到大部分的型別錯誤。例如,你定義了一個使用者物件:

interface User {
  id: string;
  name: string;
  email: string;
}

只要後端 API 遵守這個契約,編譯器就能保護你,避免你誤用 user.emial 這類拼寫錯誤的屬性。然而,TypeScript 的型別系統運作在編譯期(compile time),它會在程式碼轉換成 JavaScript 時進行檢查,但轉換完成後,所有型別資訊都會被抹除。它無法驗證程式在運行時(run time)實際收到的外部資料。

當資料來源從固定的 API 變成一個 LLM 時,問題就浮現了。即使你透過 prompt engineering 要求模型以 JSON 格式回傳,並提供範例,你仍然無法 100% 保證它的輸出:

  • 欄位缺失:模型可能漏掉 email 欄位。
  • 欄位多餘:模型可能自行增加一個 commentary 欄位。
  • 型別錯誤:模型可能回傳 { id: 123, ... },將 id 從字串變成了數字。
  • 格式錯誤:在未使用 JSON mode 的情況下,輸出可能包含額外的文字或不合法的 JSON 結構。

在這些情況下,單純的型別斷言(type assertion),例如 const data = response.json() as User;,不僅無法提供實質保護,反而會成為潛在的錯誤來源,導致應用程式在運行時因非預期資料而崩潰。

Zod 如何成為 Runtime 的守門人?

為了解決這個問題,我們需要在程式的邊界——也就是接收 LLM 回應的地方——建立一個驗證層。這就是 Zod 這類 schema 驗證工具發揮價值的地方。Zod 允許你用程式碼定義一個資料結構的「藍圖」(schema),然後用這個藍圖去解析(parse)未知的輸入資料。

同樣是使用者物件,用 Zod 定義會是這樣:

import { z } from 'zod';

const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1),
  email: z.string().email(),
});

這個 schema 不僅定義了欄位與型別,還加入了更豐富的語義規則,例如 id 必須是 UUID 格式,name 不可為空,email 必須符合 email 格式。當收到 LLM 的資料時,我們不再盲目信任,而是讓 Zod 進行驗證:

const result = UserSchema.safeParse(llmOutput);

if (result.success) {
  // 資料驗證成功,result.data 的型別是安全的
  console.log('Valid user:', result.data);
} else {
  // 資料驗證失敗,result.error 包含詳細錯誤訊息
  console.error('Validation failed:', result.error.flatten());
}

使用 .safeParse() 的好處是它不會在驗證失敗時拋出錯誤中斷程式,而是回傳一個包含成功狀態與結果(或錯誤)的物件。這讓我們的 AI 應用程式能優雅地處理來自 LLM 的異常輸出,例如進行重試、回退到預設值,或記錄錯誤供後續分析。

更棒的是,Zod 讓我們可以從 schema 推導出 TypeScript 型別(type User = z.infer<typeof UserSchema>;),確保了 schema 是唯一的真實來源(Single Source of Truth)。

在 LLM 時代,schema 不只是文件,而是活的防線。它定義了我們與一個不可預測的智慧體之間的互動契約,並在每一次互動中強制執行。

從型別檢查到「契約工程」的思維轉變

將 Zod 整合進工作流,不僅僅是增加一個工具,而是一種思維模式的轉變:從被動的「型別檢查」轉向主動的「契約工程」(Contract Engineering)。即使是像 OpenAI GPT-4 Turbo 的 JSON modeAnthropic Claude 3 的工具使用(function calling),這些功能大幅提升了結構化輸出的穩定性,但官方文件依然提醒,模型仍可能產生無法解析的 JSON 或不符合 schema 的內容。這些功能是「盡力而為」,而非「絕對保證」。

這份契約的核心是,我們承認 LLM 的輸出本質上是不可信的,因此必須在系統邊界建立一個強健的驗證機制。這個機制不只檢查資料型別,更要驗證業務邏輯,例如數值的範圍、字串的格式、欄位之間的依賴關係等。這種在 runtime 進行的驗證,才是打造可信任、可預測 AI 系統的基石。正如一篇關於大型模型作為工具製造者的研究中所探討的,讓模型可靠地使用工具(例如輸出特定格式的 JSON),本身就是一個需要嚴謹工程方法來解決的問題。

總結來說,當我們將 LLM 整合到應用程式中時,不能再將靜態型別視為安全的終點。編譯期的檢查是必要的,但遠遠不夠。我們必須將重心轉移到 runtime,透過 Zod 這類的工具建立明確、可執行的 schema 契約。唯有如此,我們才能在充滿不確定性的 AI 浪潮中,打造出真正穩定、可靠的系統。

延伸閱讀

我是江中喬,專注於 AI Agent Architecture、Memory Governance 與 Cognitive Diversity,持續研究如何打造能夠長期協作、可信任且可治理的 AI 系統。