跳至主要內容

LLM 結構化產出 TypeScript 驗證教學

實作

LLM API 與 Structured Outputs:讓程式分清成功與失敗

實作客服分類器,把合法 JSON、格式遵循、模型拒絕、截斷、逾時與應用程式驗證視為不同狀態。

難度
入門
預估時間
105 分鐘
更新日期
2026-10-02
文案複核
speak-human-tw
兩輪
本課內容
  1. 01先把定義說清楚
  2. 02現場情境
  3. 03實作範例
  4. 04照著做,每一步都有檢查點
  5. 05實務脈絡
  6. 06動手實作
  7. 07故障診間
  8. 08正式上線前的邊界
  9. 09資料來源與主張限制

這一課會完成什麼

  • 最終資料用結構化輸出,應用程式動作用函式呼叫
  • 在供應商與應用程式邊界各自執行窄而嚴格的 schema
  • 分開處理模型拒絕、未完成、逾時、速率限制與語意不合格
  • 建立能承受模型或提示詞變更的固定測試資料測試集

開始前先準備

  • • Node.js 20 以上與 TypeScript
  • • 熟悉非同步 function、JSON Schema 與環境變數

先把定義說清楚

有型別的模型輸出

把模型接進程式,先定義請求與回傳資料的格式,再檢查實際值是否符合業務規則。符合 schema 的 JSON,仍可能引用不存在的資料,或填入沒有權限使用的識別碼。模型拒絕、內容截斷、暫時故障與驗證失敗也要各有處理路徑。下游取得封閉聯集型別,便能逐一處理所有結果,無須從一段文字猜測這次呼叫能不能繼續。

結構化產出可在支援的情況下約束回應 schema,卻不會替你確認欄位是否真實、有權限、夠新,或真的能支援業務決策。型別解決形狀,語意與政策仍是應用程式責任。

正式呼叫端不能只處理正常流程。模型拒絕、輸出上限、暫時傳輸方式錯誤,以及格式正確但證據捏造的分類,需要不同的復原方式。全部塞進重試,只會把問題洗掉,甚至產生重複資料。

現場情境

Harbor 客服需求分類

具名合成情境。Harbor Cloud、客服單文字、標記、筆數與門檻都是教學測試資料,與任何真實客服營運無關。

負責人
你負責實作客服單進入 Harbor 分流佇列前的分類邊界。
要做的決策
設計供應商 schema 與應用程式驗證器,產出能安全送進佇列的分類紀錄。
目前狀態
佇列收到 billing(帳務)、access(存取)、incident(事故)與 unknown 四類需求。舊版正規表示式會把語意模糊的客服單分進 billing,卻沒有記錄支持判斷的原文。團隊準備接 LLM,同時要求下游先驗證回傳內容,再接受 JSON。
預期成果
10 筆測試資料各自得到有型別的紀錄或具名失敗狀態。任何未驗證解析、捏造證據或模糊錯誤都不能進分流佇列。

限制條件

  • • 分類器只能標標記與轉交人工,不能回覆使用者或修改帳號資料。
  • • unknown 是有效結果,不能被當成傳輸方式錯誤重試。
  • • 客服單可能含惡意指令,永遠屬於不受信任的使用者資料。
  • • 應用程式要求證據必須逐字出現在原客服單,找不到就拒絕。

實作範例

JSON 合法,證據是編的,照樣拒絕

證據類型: 具名模擬情境

客服單 HBR-07 原文是「I was charged twice after changing plans.」,意思是換方案後被收費兩次。預期 category 為 billing、urgency 為 normal,正確 evidence 片段是「charged twice」。供應商回傳符合 schema 的物件,類別也是 billing,evidence 卻寫「duplicate subscription charge」;這段文字不在原客服單,因此仍要拒絕。

供應商 schema 驗證先通過,Harbor 的應用程式驗證器隨後回傳 EVIDENCE_NOT_IN_INPUT,記錄這次嘗試,並把原客服單送到審查佇列。系統不重試;第二次模型呼叫可能換一段同樣沒有根據的文字,反而掩蓋語意缺陷。

測試結果落在 6 個終止狀態之一:accepted(接受)、human_review(人工審查)、provider_refusal(模型拒絕)、incomplete(未完成)、transient_error(暫時錯誤)或 invalid_output(輸出不合法)。交付本機測試報告,不宣稱模型達到特定正確率。

主張限制

OpenAI 的 Structured Outputs 文件說明格式遵循與獨立的 refusal 欄位。證據檢查、Harbor 分類和重試規則是本地設計。Anthropic 的請求及回應格式不同,因此起始範本只定義不綁供應商的轉接介面,不能假設兩家行為完全相同。

做法

照著做,每一步都有檢查點

有型別 LLM 請求的狀態圖,包含 schema 驗證、模型拒絕、未完成、應用程式證據檢查、有上限的重試、驗收與人工審查。

現場情境

設計供應商 schema 與應用程式驗證器,產出能安全送進佇列的分類紀錄。

  1. 01凍結契約
  2. 02寫確定性的驗證器
  3. 03列完所有供應商結果

驗收條件

呼叫端回傳封閉結果聯集型別。下游分流程式碼不用解析說明文字,也不會把符合 schema 的資料誤認成語意與政策都安全的資料。

這張圖要幫你看懂什麼用 Mermaid 或 SVG 來源產生確定性的請求狀態圖解。圖上必須分出供應商完成、模型拒絕、未完成、傳輸方式錯誤、應用程式拒絕、驗收與人工審查。
  1. 01

    凍結契約

    複製結果聯集型別與 JSON Schema,另在請求封套加入 schema 版本及請求 ID,不放寬分類欄位。先用成功和失敗樣本確認編譯期型別與執行期驗證遵循同一規格。

    檢查點 · TypeScript 在編譯時拒絕未列出的類別;JSON Schema 在執行時拒絕額外欄位。

  2. 02

    寫確定性的驗證器

    供應商解析後,檢查列舉值、必要值、證據是否真的存在於原輸入,以及轉交人工的業務規則。不要把 TypeScript 斷言當成執行環境驗證。

    檢查點 · HBR-07 即使物件符合供應商 schema,仍回傳 invalid_output 與 EVIDENCE_NOT_IN_INPUT。

  3. 03

    列完所有供應商結果

    轉接層分別處理模型拒絕、輸出截斷、逾時、速率限制、回應格式錯誤與成功,映射到封閉結果聯集。另存供應商請求 ID,不與客服單內容混在一起;每種結果都有對應測試。

    檢查點 · 例外處理不把所有錯誤都轉成同一種重試;各案例回自己的具名狀態。

  4. 04

    加入有上限的重試規則

    只重試選定的暫時傳輸錯誤,加上隨機延遲與明確次數上限。供應商提供等待建議時依其指引;用盡次數就轉交人工,不重試語意錯誤或模型拒絕。

    檢查點 · 語意拒絕與模型拒絕永不重試;模擬速率限制最多重試 2 次。

  5. 05

    比較版本

    先對模擬基準版本跑完所有測試資料。若啟用真實供應商,鎖定測試的模型識別碼,只存彙總結果,不多留不必要的客服單內容。

    檢查點 · 報告列出 schema、提示詞、模型或模擬轉接層、測試集版本、已接受的/審查/錯誤筆數、延遲與預估成本。

實務脈絡

展示版之後

G1

實作拆解

供應商 schema 與業務驗證器分開管理版本。前者約束回傳格式,後者檢查客服單證據、識別碼、日期、權限與業務條件。否則類別一改,就難以分清是傳輸契約還是佇列規則變了。執行報告同時記下請求 ID、schema、驗證器、轉接層版本與終止狀態。敏感客服單預設遮蔽,除錯時另走授權查閱。

測試集同時放成功與失敗案例:合法物件、額外欄位、錯誤列舉值、null、內容截斷、模型拒絕、逾時、速率限制與未知類別。HBR-07 則測格式合法、引述證據卻不在原文的情況。先定每案的預期終止狀態與可否重試,凍結為發布契約;看完候選版結果後不能改答案。供應商回應格式更新時,只改轉接層對應,再確認下游封閉聯集型別不受影響。

接真實 API 前,讓模擬轉接層能重播各種供應商結果,並測取消、期限與重試用盡。重試只包住單次供應商呼叫,避免重跑已完成的佇列狀態轉移。紀錄須回答哪個版本、請求與驗收失敗,以及後續處理位置。整包原始回應寫進遙測會暴露客服單,也讓操作人員難以辨認真正狀態。

G2

營運檢查

實作時,把一次呼叫拆成四個可獨立測試的元件:請求組裝器只負責高優先指令與不受信任輸入的分層;供應商轉接器只負責送出請求、解析回應封套與保存請求識別碼;執行期驗證器檢查實際物件是否符合型別與業務限制;路由器則依終止狀態決定進佇列、人工審查或有限重試。四者不要共用一個巨大的例外處理區塊。若回傳被拒絕,轉接器應先辨認供應商文件定義的拒絕欄位,不能硬把拒絕文字塞進分類格式;若輸出因長度上限不完整,也要保留不完整原因,不能把缺欄位當一般格式錯誤。如此一來,換模型只需重測轉接器與固定資料集,下游仍只處理那六種封閉狀態。

正式審查時,請另一位工程師逐一回答:哪個元件保證額外欄位不會進系統、哪裡確認引文真的出現在原始需求、哪種錯誤可以重試、重試用盡後誰會接手、日誌能否在不保存完整客服內容的前提下還原版本。再把每個答案連到測試名稱與一筆失敗樣本。若只能回答『模型通常會照格式』,邊界尚未成立。這個檢查也能防止團隊把供應商保證延伸得太遠:結構符合規格,只能證明資料形狀通過;分類正確、內容有根據、使用者有權限與資料足夠新,都必須由另外的驗證或人工決定。

G3

驗證與上線

交付前再做一次反向測試:刻意讓供應商轉接器回傳看似完整、實際缺乏根據的分類,確認只有應用程式驗證器會拒絕,路由器也不會把它改成可重試錯誤。接著關掉網路,以假轉接器跑完整十筆資料,證明核心測試不依賴當下模型服務。最後檢查每個終止狀態都有處理者與後續動作;若人工審查佇列沒有人負責,即使型別完美,也只是把失敗移到另一個沒人看的地方。

G4

補強練習

交件時附一份狀態對照表:每種供應商回應會轉成哪個本地狀態、是否重試、是否進人工佇列、日誌保留什麼。請審查者隨機挑三筆失敗,從報表追到固定資料與驗證規則;追不到就補欄位,不用增加模型解釋。

G5

延伸實作

版本發布時,將格式、語意與營運三類門檻分開簽核。格式門檻由型別與執行期驗證證明;語意門檻由引文、分類與不確定案例證明;營運門檻則確認重試上限、人工佇列、遮蔽與值班責任。任一類缺少證據就不接正式佇列。這個做法也讓事件處理更快:格式突然大量失敗先查供應商封套或格式版本;引文不符先查驗證與提示;等待暴增則查分類政策與人力,不把所有問題都歸咎模型。

動手實作

實作並測試 Harbor 分類器

先用可直接執行的 TypeScript 起始範本與模擬供應商轉接層,所有確定性的測試通過後,才選擇是否接真實供應商。

準備項目

  • • 建立乾淨目錄,安裝 TypeScript 與偏好的測試執行器。
  • • OPENAI_API_KEY 或 ANTHROPIC_API_KEY 不得進版控;預設實作完全不需要鍵值。
  • • 把 10 筆客服單存成測試資料,未來換提示詞或模型仍用同一批輸入。
  • • 設定明確測試逾時,語意不合格外層不得自動重試。

本課產出

一個不綁供應商的分類器模組、10 筆測試資料、確定性的單元測試、轉接層契約,以及統計各終止狀態的執行報告。

起始模板: 分類契約與驗證器

TypeScript
type Category = "billing" | "access" | "incident" | "unknown";

type Classification = {
  category: Category;
  urgency: "normal" | "urgent";
  evidence: string;
  escalationReason: string | null;
};

type Result =
  | { status: "accepted"; value: Classification }
  | { status: "human_review"; code: string }
  | { status: "provider_refusal"; message: string }
  | { status: "incomplete"; reason: string }
  | { status: "transient_error"; retryAfterMs: number }
  | { status: "invalid_output"; code: string };

export const schema = {
  type: "object",
  additionalProperties: false,
  required: ["category", "urgency", "evidence", "escalationReason"],
  properties: {
    category: { type: "string", enum: ["billing", "access", "incident", "unknown"] },
    urgency: { type: "string", enum: ["normal", "urgent"] },
    evidence: { type: "string", minLength: 1 },
    escalationReason: { type: ["string", "null"] }
  }
} as const;

export function validateEvidence(ticket: string, value: Classification): Result {
  if (!ticket.includes(value.evidence)) {
    return { status: "invalid_output", code: "EVIDENCE_NOT_IN_INPUT" };
  }
  if (value.category === "unknown" || value.escalationReason) {
    return { status: "human_review", code: "CLASSIFICATION_UNCERTAIN" };
  }
  return { status: "accepted", value };
}

預期結果

呼叫端回傳封閉結果聯集型別。下游分流程式碼不用解析說明文字,也不會把符合 schema 的資料誤認成語意與政策都安全的資料。

留給下一課

保留結果聯集型別、轉接層邊界、schema 版本與測試資料執行器。模組 02 會沿用同一模式處理工具請求與工具結果。

驗收條件

  1. 0110 筆測試資料全部終止,沒有任何測試使用無上限重試迴圈。
  2. 02格式合法但證據捏造的測試資料必須在應用程式驗證失敗。
  3. 03模型拒絕、未完成、暫時錯誤、結果未知與接受分開呈現,下游可據此決定下一步。
  4. 04紀錄有請求與版本中繼資料,預設不保存完整客服單文字。

常見故障

故障診間

F1Parser 成功,下游卻因欄位缺少或多出而程序崩潰。
先檢查
對照供應商 schema、應用程式型別、原始輸出狀態與執行環境驗證器,並檢查 additionalProperties 是否過寬。
可能原因
應用程式只相信 JSON 語法或 TypeScript 斷言,沒有驗證執行環境值。
修復方式
使用供應商支援的嚴格 schema,分流前再驗證已解析的物件。
下次怎麼避免
每次改 schema 都在 CI 跑不合法、額外欄位、null、模型拒絕與遭截斷的測試資料。
F2有害或超出範圍的客服單只出現解析錯誤,看不到模型拒絕。
先檢查
解析結構化內容前,先檢查完整回應資料包。
可能原因
呼叫端假設供應商每次都必須回傳業務 schema。
修復方式
依文件先分流模型拒絕或未完成,再解析已完成內容。
下次怎麼避免
Structured-output 測試集合永遠保留模型拒絕與輸出上限測試資料。
F3速率限制引發重試大量重試,佇列出現重複紀錄。
先檢查
查嘗試次數、隨機延遲、冪等鍵、佇列寫入執行軌跡與同時進行的工作單元行為。
可能原因
重試無上限,或整個工作流程連下游寫入一起重跑。
修復方式
只重試供應商呼叫並設上限,之後只做一次冪等佇列狀態轉移。
下次怎麼避免
接真實環境的流量前先測用盡、同時進行的重試與重複交付。
F4符合 schema 的證據在客服單原文中找不到。
先檢查
對不受信任輸入做精確或已標準化的 span 檢查,記錄不一致程式碼。
可能原因
結構化輸出只限制形式,模型仍可能生成沒有來源的內容。
修復方式
拒絕該紀錄,或要求應用程式可驗證的來源偏移。
下次怎麼避免
識別碼、引用、權限、日期與業務必須成立的條件都要有語意驗證器。

展示版之後

正式上線前的邊界

  1. 01鎖定並記錄 schema、提示詞、供應商轉接層與測過的模型版本。
  2. 02不受信任的使用者文字留在使用者輸入,不得混進高優先指令。
  3. 03供應商支援時使用嚴格 schema,應用程式驗證仍不可省。
  4. 04Unknown、模型拒絕、未完成、逾時、速率限制與不合法內容分開表示。
  5. 05重試要有限制、有隨機延遲,下游狀態轉移必須冪等。
  6. 06遮蔽敏感輸入,同時保留請求 ID 與授權診斷所需中繼資料。
  7. 07模型、提示詞、schema 或規則一改,就重跑固定回歸測試集合。
  8. 08未解案例送到有人值守的佇列,並設定明確處理時限與負責人。

證據類型

資料來源與主張限制

資料來源只支撐本課標示的主張,不代表換一個系統也會得到相同結果。

  1. [1]
    Structured model outputs

    OpenAI · 官方文件 · 2026-08-20

    schema 遵循 · refusal 處理 · 不完整輸出處理
  2. [2]
    Function calling

    OpenAI · 官方文件 · 2026-08-20

    strict function schema · 平行呼叫 · tool-call 狀態機
  3. [3]
    Claude tool use

    Anthropic · 官方文件 · 2026-08-20

    工具定義 · tool result 迴圈 · 使用者端執行責任
  4. [4]
    評測 agent workflow

    OpenAI · 官方文件 · 2026-08-20

    trace grading · 資料集 · 可重複的 eval 執行

延伸的 Tenten 資源

實作準備進正式環境時

帶著實作證據來,不用從空白摘要開始。

有效的實作審查,起點應該是任務測試資料、權限地圖、執行軌跡、評測報告、失敗案例與成本上限。Tenten 可以根據這些資料檢查整合與營運缺口,不必把課程裡已經證明過的決策全部重開。