這一課會完成什麼
- 最終資料用結構化輸出,應用程式動作用函式呼叫
- 在供應商與應用程式邊界各自執行窄而嚴格的 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 的請求及回應格式不同,因此起始範本只定義不綁供應商的轉接介面,不能假設兩家行為完全相同。
做法
照著做,每一步都有檢查點
現場情境
設計供應商 schema 與應用程式驗證器,產出能安全送進佇列的分類紀錄。
- 01凍結契約
- 02寫確定性的驗證器
- 03列完所有供應商結果
驗收條件
呼叫端回傳封閉結果聯集型別。下游分流程式碼不用解析說明文字,也不會把符合 schema 的資料誤認成語意與政策都安全的資料。
- 01
凍結契約
複製結果聯集型別與 JSON Schema,另在請求封套加入 schema 版本及請求 ID,不放寬分類欄位。先用成功和失敗樣本確認編譯期型別與執行期驗證遵循同一規格。
檢查點 · TypeScript 在編譯時拒絕未列出的類別;JSON Schema 在執行時拒絕額外欄位。
- 02
寫確定性的驗證器
供應商解析後,檢查列舉值、必要值、證據是否真的存在於原輸入,以及轉交人工的業務規則。不要把 TypeScript 斷言當成執行環境驗證。
檢查點 · HBR-07 即使物件符合供應商 schema,仍回傳 invalid_output 與 EVIDENCE_NOT_IN_INPUT。
- 03
列完所有供應商結果
轉接層分別處理模型拒絕、輸出截斷、逾時、速率限制、回應格式錯誤與成功,映射到封閉結果聯集。另存供應商請求 ID,不與客服單內容混在一起;每種結果都有對應測試。
檢查點 · 例外處理不把所有錯誤都轉成同一種重試;各案例回自己的具名狀態。
- 04
加入有上限的重試規則
只重試選定的暫時傳輸錯誤,加上隨機延遲與明確次數上限。供應商提供等待建議時依其指引;用盡次數就轉交人工,不重試語意錯誤或模型拒絕。
檢查點 · 語意拒絕與模型拒絕永不重試;模擬速率限制最多重試 2 次。
- 05
比較版本
先對模擬基準版本跑完所有測試資料。若啟用真實供應商,鎖定測試的模型識別碼,只存彙總結果,不多留不必要的客服單內容。
檢查點 · 報告列出 schema、提示詞、模型或模擬轉接層、測試集版本、已接受的/審查/錯誤筆數、延遲與預估成本。
實務脈絡
展示版之後
實作拆解
供應商 schema 與業務驗證器分開管理版本。前者約束回傳格式,後者檢查客服單證據、識別碼、日期、權限與業務條件。否則類別一改,就難以分清是傳輸契約還是佇列規則變了。執行報告同時記下請求 ID、schema、驗證器、轉接層版本與終止狀態。敏感客服單預設遮蔽,除錯時另走授權查閱。
測試集同時放成功與失敗案例:合法物件、額外欄位、錯誤列舉值、null、內容截斷、模型拒絕、逾時、速率限制與未知類別。HBR-07 則測格式合法、引述證據卻不在原文的情況。先定每案的預期終止狀態與可否重試,凍結為發布契約;看完候選版結果後不能改答案。供應商回應格式更新時,只改轉接層對應,再確認下游封閉聯集型別不受影響。
接真實 API 前,讓模擬轉接層能重播各種供應商結果,並測取消、期限與重試用盡。重試只包住單次供應商呼叫,避免重跑已完成的佇列狀態轉移。紀錄須回答哪個版本、請求與驗收失敗,以及後續處理位置。整包原始回應寫進遙測會暴露客服單,也讓操作人員難以辨認真正狀態。
營運檢查
實作時,把一次呼叫拆成四個可獨立測試的元件:請求組裝器只負責高優先指令與不受信任輸入的分層;供應商轉接器只負責送出請求、解析回應封套與保存請求識別碼;執行期驗證器檢查實際物件是否符合型別與業務限制;路由器則依終止狀態決定進佇列、人工審查或有限重試。四者不要共用一個巨大的例外處理區塊。若回傳被拒絕,轉接器應先辨認供應商文件定義的拒絕欄位,不能硬把拒絕文字塞進分類格式;若輸出因長度上限不完整,也要保留不完整原因,不能把缺欄位當一般格式錯誤。如此一來,換模型只需重測轉接器與固定資料集,下游仍只處理那六種封閉狀態。
正式審查時,請另一位工程師逐一回答:哪個元件保證額外欄位不會進系統、哪裡確認引文真的出現在原始需求、哪種錯誤可以重試、重試用盡後誰會接手、日誌能否在不保存完整客服內容的前提下還原版本。再把每個答案連到測試名稱與一筆失敗樣本。若只能回答『模型通常會照格式』,邊界尚未成立。這個檢查也能防止團隊把供應商保證延伸得太遠:結構符合規格,只能證明資料形狀通過;分類正確、內容有根據、使用者有權限與資料足夠新,都必須由另外的驗證或人工決定。
驗證與上線
交付前再做一次反向測試:刻意讓供應商轉接器回傳看似完整、實際缺乏根據的分類,確認只有應用程式驗證器會拒絕,路由器也不會把它改成可重試錯誤。接著關掉網路,以假轉接器跑完整十筆資料,證明核心測試不依賴當下模型服務。最後檢查每個終止狀態都有處理者與後續動作;若人工審查佇列沒有人負責,即使型別完美,也只是把失敗移到另一個沒人看的地方。
補強練習
交件時附一份狀態對照表:每種供應商回應會轉成哪個本地狀態、是否重試、是否進人工佇列、日誌保留什麼。請審查者隨機挑三筆失敗,從報表追到固定資料與驗證規則;追不到就補欄位,不用增加模型解釋。
延伸實作
版本發布時,將格式、語意與營運三類門檻分開簽核。格式門檻由型別與執行期驗證證明;語意門檻由引文、分類與不確定案例證明;營運門檻則確認重試上限、人工佇列、遮蔽與值班責任。任一類缺少證據就不接正式佇列。這個做法也讓事件處理更快:格式突然大量失敗先查供應商封套或格式版本;引文不符先查驗證與提示;等待暴增則查分類政策與人力,不把所有問題都歸咎模型。
動手實作
實作並測試 Harbor 分類器
先用可直接執行的 TypeScript 起始範本與模擬供應商轉接層,所有確定性的測試通過後,才選擇是否接真實供應商。
準備項目
- • 建立乾淨目錄,安裝 TypeScript 與偏好的測試執行器。
- • OPENAI_API_KEY 或 ANTHROPIC_API_KEY 不得進版控;預設實作完全不需要鍵值。
- • 把 10 筆客服單存成測試資料,未來換提示詞或模型仍用同一批輸入。
- • 設定明確測試逾時,語意不合格外層不得自動重試。
本課產出
一個不綁供應商的分類器模組、10 筆測試資料、確定性的單元測試、轉接層契約,以及統計各終止狀態的執行報告。
起始模板: 分類契約與驗證器
TypeScripttype 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 會沿用同一模式處理工具請求與工具結果。
驗收條件
- 0110 筆測試資料全部終止,沒有任何測試使用無上限重試迴圈。
- 02格式合法但證據捏造的測試資料必須在應用程式驗證失敗。
- 03模型拒絕、未完成、暫時錯誤、結果未知與接受分開呈現,下游可據此決定下一步。
- 04紀錄有請求與版本中繼資料,預設不保存完整客服單文字。
常見故障
故障診間
F1Parser 成功,下游卻因欄位缺少或多出而程序崩潰。
- 先檢查
- 對照供應商 schema、應用程式型別、原始輸出狀態與執行環境驗證器,並檢查 additionalProperties 是否過寬。
- 可能原因
- 應用程式只相信 JSON 語法或 TypeScript 斷言,沒有驗證執行環境值。
- 修復方式
- 使用供應商支援的嚴格 schema,分流前再驗證已解析的物件。
- 下次怎麼避免
- 每次改 schema 都在 CI 跑不合法、額外欄位、null、模型拒絕與遭截斷的測試資料。
F2有害或超出範圍的客服單只出現解析錯誤,看不到模型拒絕。
- 先檢查
- 解析結構化內容前,先檢查完整回應資料包。
- 可能原因
- 呼叫端假設供應商每次都必須回傳業務 schema。
- 修復方式
- 依文件先分流模型拒絕或未完成,再解析已完成內容。
- 下次怎麼避免
- Structured-output 測試集合永遠保留模型拒絕與輸出上限測試資料。
F3速率限制引發重試大量重試,佇列出現重複紀錄。
- 先檢查
- 查嘗試次數、隨機延遲、冪等鍵、佇列寫入執行軌跡與同時進行的工作單元行為。
- 可能原因
- 重試無上限,或整個工作流程連下游寫入一起重跑。
- 修復方式
- 只重試供應商呼叫並設上限,之後只做一次冪等佇列狀態轉移。
- 下次怎麼避免
- 接真實環境的流量前先測用盡、同時進行的重試與重複交付。
F4符合 schema 的證據在客服單原文中找不到。
- 先檢查
- 對不受信任輸入做精確或已標準化的 span 檢查,記錄不一致程式碼。
- 可能原因
- 結構化輸出只限制形式,模型仍可能生成沒有來源的內容。
- 修復方式
- 拒絕該紀錄,或要求應用程式可驗證的來源偏移。
- 下次怎麼避免
- 識別碼、引用、權限、日期與業務必須成立的條件都要有語意驗證器。
展示版之後
正式上線前的邊界
- 01鎖定並記錄 schema、提示詞、供應商轉接層與測過的模型版本。
- 02不受信任的使用者文字留在使用者輸入,不得混進高優先指令。
- 03供應商支援時使用嚴格 schema,應用程式驗證仍不可省。
- 04Unknown、模型拒絕、未完成、逾時、速率限制與不合法內容分開表示。
- 05重試要有限制、有隨機延遲,下游狀態轉移必須冪等。
- 06遮蔽敏感輸入,同時保留請求 ID 與授權診斷所需中繼資料。
- 07模型、提示詞、schema 或規則一改,就重跑固定回歸測試集合。
- 08未解案例送到有人值守的佇列,並設定明確處理時限與負責人。
證據類型
資料來源與主張限制
資料來源只支撐本課標示的主張,不代表換一個系統也會得到相同結果。
- [1]Structured model outputsschema 遵循 · refusal 處理 · 不完整輸出處理
OpenAI · 官方文件 · 2026-08-20
- [2]Function callingstrict function schema · 平行呼叫 · tool-call 狀態機
OpenAI · 官方文件 · 2026-08-20
- [3]Claude tool use工具定義 · tool result 迴圈 · 使用者端執行責任
Anthropic · 官方文件 · 2026-08-20
- [4]評測 agent workflowtrace grading · 資料集 · 可重複的 eval 執行
OpenAI · 官方文件 · 2026-08-20
延伸的 Tenten 資源