主題: 學習筆記
Structured Output:AI 回了 JSON,接下來呢?
從固定格式、JSON mode 到 schema 約束,整理 Structured Output 的用途與限制;用 metadata 驗證、時間戳與 token 粗估反例,確認資料能否交給程式處理。
「只回 JSON,不要解釋。」
如果下一步要跑 JSON.parse(),模型多送一句「好的,以下是你要的資料」,這份貼心就可能直接把程式送進 catch。回覆很客氣,串接的程式大概不太領情。
這篇接著 Core Prompting Techniques,整理 Sabrina Goldfarb 課程的 Structured Output。我想把「輸出長什麼樣子」一路接到「拿到之後怎麼檢查」,用會議資料擷取和提示詞收藏庫的 metadata 當例子。
提示與預期 JSON 是本文自編,尚未呼叫模型;日期與欄位驗證、token 公式反例則有附上可執行檢查。
先分清楚:是在要求格式,還是在限制生成?
課程使用的 structured output 是廣義的格式要求。固定欄位的文字、文件模板或程式碼區塊,都能讓後續處理更清楚。應用情境包括資料擷取、測試案例、文件,以及多筆相似內容的生成。
接 API 時,還要分清楚不同機制:
| 做法 | 約束什麼 | 接收端仍要處理什麼 |
|---|---|---|
| 在 prompt 寫格式與示範 | 用指令引導模型回覆 | 偏離格式、多餘文字與解析失敗 |
| JSON mode(若 API 提供) | 有效 JSON,不保證指定欄位結構 | 欄位、型別與回應中斷等例外 |
| API 的 schema 約束輸出 | 依支援的 schema 限制生成結構 | 服務例外、內容正確性與業務規則 |
OpenAI 的文件明確區分 JSON mode 與符合 schema 的 Structured Outputs;Anthropic 的文件也說明解碼約束與支援範圍。把 schema 貼在 prompt 裡,不會自動啟用 API 的約束功能。
用一封信,把輸出要求寫具體
先做一個簡單的擷取提示:
從以下郵件擷取 dateText、timeText、location、topic。
四個欄位都必須出現,值只能是字串或 null。
沒有提到的資訊填 null,不要自行補齊。
日期與時間保留原文,不轉換成實際時間戳。
只回傳 JSON 物件,不附 Markdown 圍欄或解釋。
郵件:明天下午三點到會議室 C 討論新版首頁。
手寫的預期結果是:
{
"dateText": "明天",
"timeText": "下午三點",
"location": "會議室 C",
"topic": "新版首頁"
}
這個例子刻意保留「明天」。要變成真正的時間戳,還需要郵件的參考日期與適用時區;不能拿程式執行當天的日期,替原文偷偷補答案。
接著可以移除地點或主題,檢查是否填 null。這能分開觀察格式有沒有符合、擷取有沒有忠於原文。就算 JSON 很漂亮,裡面多了一間原文沒提過的會議室,還是要擋下來。
Schema 把欄位寫清楚,也要看誰負責驗證
課程接著要求 AI 產生提示詞收藏庫的 metadata 功能,包含 trackModel、updateTimestamps 與 estimateTokens。這是在指定產生程式碼的需求,不能直接當成已啟用執行時的 schema 約束。
我先取模型名稱與兩個時間欄位,做一個縮小版:
{
"model": "example-model-v1",
"createdAt": "2026-10-19T09:00:00.000Z",
"updatedAt": "2026-10-19T09:00:00.000Z"
}
對應的一般 JSON Schema 可以寫成:
{
"type": "object",
"properties": {
"model": { "type": "string", "minLength": 1, "maxLength": 100 },
"createdAt": { "type": "string", "format": "date-time" },
"updatedAt": { "type": "string", "format": "date-time" }
},
"required": ["model", "createdAt", "updatedAt"],
"additionalProperties": false
}
這份 schema 寫出必要欄位、型別與額外欄位政策,但還沒比較兩個時間,也沒有排除全部由空白組成的模型名稱。
JSON Schema 規範的 date-time 依據 RFC 3339;format 是否實際阻擋無效值,要看驗證器與設定。不同 API 也只支援部分關鍵字。例如查閱時的 Anthropic 文件列出 minLength/maxLength 不受原始 API schema 支援,需由接收端驗證。這份一般 schema 不能原封不動當成所有 API 的請求參數。
日期長得像時間,還要確認它真的存在
這次的小範例採以下規則:
| 欄位 | 本文示例的規則 |
|---|---|
| model | 必須含非空白文字,最多 100 個 Unicode code points;不限制固定模型清單 |
| createdAt、updatedAt | 接受實際存在、固定三位毫秒的 UTC 字串,例如 2026-10-19T09:00:00.000Z |
| 時間關係 | updatedAt 必須晚於或等於 createdAt |
這裡刻意選定較窄的時間格式,不是宣稱所有 ISO 8601 或 RFC 3339 表示法都只長這樣。帶 +08:00 的時間可有效表示時間,但這個範例要求呼叫端先正規化成 UTC。
下面節錄下載範例中的日期檢查,data 是已完成物件、欄位及模型名稱檢查的解析結果:
for (const field of ["createdAt", "updatedAt"]) {
const value = data[field];
if (
typeof value !== "string" ||
!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value) ||
!Number.isFinite(Date.parse(value)) ||
new Date(value).toISOString() !== value
) {
throw new Error(`${field} must be a real date in UTC millisecond format`);
}
}
if (Date.parse(data.updatedAt) < Date.parse(data.createdAt)) {
throw new Error("updatedAt must be at or after createdAt");
}
正則先檢查形狀,轉成日期再轉回相同格式,則能擋下像 2027-02-30 這種看起來很像日期的輸入。最後才比較建立與更新時間,兩者相等是允許的。
這些檢查也不會因為寫了 JSON.parse(text) as Metadata 就自動發生。TypeScript 文件說明型別斷言在編譯時會被移除。外部資料仍需 runtime validation;專案已有驗證工具時,就沿用它。
模型名稱保持彈性,是為了讓紀錄能容納新版本。若這個值要拿去決定真正呼叫哪個模型,支援清單與權限檢查仍是另一項責任。實際呼叫的模型、建立時間等應用程式已知資訊,也應由程式記錄,不用請模型猜自己的履歷。
Token 粗估:先看看最小值有沒有比最大值大
課程的 token estimator 使用這組啟發式公式,程式碼內容再乘上 1.3:
min = 0.75 × word_count
max = 0.25 × character_count
拿 a b c d 試一下,以空白分出四個字詞,含空白共七個字元:
| 計算 | 結果 |
|---|---|
| min = 0.75 × 4 | 3 |
| max = 0.25 × 7 | 1.75 |
| 程式碼倍率套用後 | min = 3.9,max = 2.275 |
還沒接模型,這個「範圍」就已經上下顛倒了。把兩個數字排序,只能讓畫面看起來合理,不能證明真正的 token 數落在中間。
所以我會把 1.3 保留為課程示範的粗估係數,不當成所有程式碼、語言與 tokenizer 的固定規律。需要做容量或成本預算時,使用對應模型的 tokenizer 或供應商計數介面,並核對完整請求與實際 usage。
課程把 high/medium/low 對應綠、黃、紅,並用 1,000 與 5,000 token 作為級距邊界。要實作前,還得定義比較哪一個估計值與邊界歸屬;這些標籤沒有經過校準,不能解讀成「估計有幾成準」。畫面也要保留文字,讓使用者不必只靠顏色判讀。
讓錯誤停在適合處理的地方
串接時,我會先看服務狀態,再解析正常完成的內容:
| 檢查層次 | 例子 | 處理方向 |
|---|---|---|
| 服務與完成狀態 | API 錯誤、拒絕回答、輸出被截斷 | 依服務回應分流,不當成正常資料 |
| JSON 語法 | 多餘說明、缺少括號 | 回報解析失敗 |
| 結構與欄位 | 缺欄位、型別不符、多出未允許欄位 | 回報驗證失敗 |
| 內容與業務規則 | 更新時間較早、擷取出不存在的地點 | 核對來源與業務條件 |
Anthropic 的文件列出拒絕與 token 上限等可能不符合 schema 的情況。因此不能只看到 HTTP 200,就把回應內容直接存進資料庫。需要重試時,也應設次數與停止條件;格式檢查失敗不等於可以無限重送。
同樣地,程式碼格式方便複製,仍然需要編譯、測試與適合該程式的安全檢查。Structured output 幫忙交代輸出契約,沒有代替這些工作。
跑一次接收端檢查
下載完整驗證範例,用 Node.js 執行:
node structured-output-validation.mjs
檔案使用 Node.js 內建 assertions,不需要額外套件。檢查包含合法資料、相等時間、閏日、無效日期、時間倒退、缺欄位與多欄位、空白或過長的模型名稱,以及前面的 token 公式反例。
這些檢查已在本機通過,驗證的是接收端的處理行為。模型是否能穩定擷取正確資料,還需要另外測試,不能拿這份通過結果代替。
參考資料與範圍
- Sabrina Goldfarb,Structured Output 課程:格式要求與 metadata 練習。原站文字內容為 CC BY-NC 4.0,程式範例與練習為 Apache 2.0。
- OpenAI,Structured model outputs:JSON mode 與 schema 約束的區分。
- Anthropic,Structured outputs:支援的 schema 子集、SDK 驗證與回應例外。
- JSON Schema,Validation specification:欄位驗證與 format 的處理。
- TypeScript,Type Assertions:型別斷言的執行時限制。
- Anthropic,Token counting:依指定模型計算輸入 token。
來源於 2026-10-19 查閱。本文使用自編提示、縮小版 metadata 與本機檢查,非課程逐字翻譯;API 支援仍應以實際使用的模型、端點與 SDK 版本確認。