這一課完成後:你能定義 JSON Schema、要求模型回 structured data、把結果 parse 成 JavaScript object,再做 application-level validation;也能處理 incomplete / refusal / provider error,而不是假設「API 回 200 就一定有可用 JSON」。
先看最脆弱的做法:用字串切模型答案
假設你要求模型:
請回覆:
Title: ...
Summary: ...
Difficulty: ...
然後程式:
const lines = text.split("\n");
const title = lines[0].replace("Title:", "");
只要模型多一行、改標點、把欄位順序換掉,程式就可能壞。
→ 猜格式
→ fragile parser
如果下游是程式,不應該要求程式去猜自然語言格式。
JSON 只是第一步,Schema 才是契約
單純要求「請回 JSON」比純文字好,但仍然可能得到:
{
"title": "API",
"difficulty": "kind of easy",
"extra_field": "???"
}
真正需要的是先定義你允許什麼:
{
"type": "object",
"properties": {
"title": { "type": "string" },
"summary": { "type": "string" },
"difficulty": {
"type": "string",
"enum": ["beginner", "intermediate", "advanced"]
},
"key_points": {
"type": "array",
"items": { "type": "string" }
}
},
"required": [
"title",
"summary",
"difficulty",
"key_points"
],
"additionalProperties": false
}
這就是資料 contract。
JSON Schema 在描述「資料可以長什麼樣」
先掌握幾個最常用的:
properties → object 有哪些欄位
required → 哪些欄位不能缺
enum → 只能是哪些固定值
items → array 裡的元素型別
additionalProperties → 能不能出現沒定義的欄位
不需要第一天就學完整 JSON Schema 規格。先把產品真正需要的資料結構寫清楚。
Structured Outputs 和「叫模型輸出 JSON」不同
目前 OpenAI Responses API 的 text.format 可以設定 json_schema。官方把這稱為 Structured Outputs,目的是讓支援的模型輸出符合你提供的 schema。
OpenAI Responses API — Structured Outputs
舊的 JSON mode 主要保證是有效 JSON;官方目前建議在支援的模型上優先使用 json_schema,因為它還多了 schema constraint。
Structured Outputs → valid structure that follows supplied schema
把 schema 放進 model request
沿用前面的 model client:
const conceptSchema = {
type: "object",
properties: {
title: { type: "string" },
summary: { type: "string" },
difficulty: {
type: "string",
enum: ["beginner", "intermediate", "advanced"]
},
key_points: {
type: "array",
items: { type: "string" }
}
},
required: [
"title",
"summary",
"difficulty",
"key_points"
],
additionalProperties: false
};
Request:
const response = await client.responses.create({
model: env.OPENAI_MODEL,
instructions: `
Analyze the user's concept for a learning product.
Use Traditional Chinese for title, summary and key points.
Do not invent specific facts when the input is unclear.
`,
input: userInput,
text: {
format: {
type: "json_schema",
name: "concept_explanation",
strict: true,
schema: conceptSchema
}
}
});
這裡的 schema 是 API contract,不需要再把同一份 JSON 格式完整複製進 prompt。
取得結果後,先 parse,不要直接相信字串
SDK 的 output_text 仍然是文字表示,因此你可以:
const data = JSON.parse(response.output_text);
現在 data 才是 JavaScript object。
但產品程式仍然要把 provider response 視為 external dependency,而不是「因為模型說是 JSON 就完全不用檢查」。
Schema validation 和 Business validation 是兩層
Schema 可以驗證:
key_points 是否為 array
title 是否為 string
必要欄位是否存在
但它不一定知道你的產品規則,例如:
summary 最多只能顯示 280 字。
key_points 最多只取 5 個。
這個 user 是否有權限把結果寫進某個 project。
資料裡的某個 id 是否真的存在於 database。
所以:
Application validation → 產品語意與權限正確
即使 provider 有 Structured Outputs,仍要保留 validation boundary
原因很簡單:你的 backend 不應該把任何外部服務回傳值直接當內部可信資料。
最小 validator 可以先手寫:
function isConceptData(value) {
return Boolean(
value &&
typeof value === "object" &&
typeof value.title === "string" &&
typeof value.summary === "string" &&
["beginner", "intermediate", "advanced"]
.includes(value.difficulty) &&
Array.isArray(value.key_points) &&
value.key_points.every(
(item) => typeof item === "string"
)
);
}
專案變大後可以換成成熟 schema validator;重點是 validation boundary 本身不能消失。
不要只處理 JSON.parse error
Model request 還可能:
provider request 失敗。
response status 是 incomplete。
模型拒絕某個 request。
輸出因 token limit 沒完成。
你的 schema 本身寫錯或模型不支援該格式。
所以 structured data pipeline 應該是:
↓ check response state
Extract output
↓ parse JSON
Validate schema / business rules
↓
Use application data
把 parse 和 model call 分開
不要把所有事情塞在 route 裡:
function parseConceptOutput(text) {
let value;
try {
value = JSON.parse(text);
} catch {
throw new Error("MODEL_OUTPUT_NOT_JSON");
}
if (!isConceptData(value)) {
throw new Error("MODEL_OUTPUT_INVALID");
}
return value;
}
Model adapter:
async function generateConceptData(env, input) {
const response = await createConceptResponse(env, input);
return parseConceptOutput(response.output_text);
}
Route 只需要處理產品流程,不需要知道每個 provider output 的細節。
Retry 不是「錯了就無限再問一次」
可以重試的例子:
暫時性 provider 5xx。
短暫 network error。
你明確判斷為可恢復的 output failure。
不該盲目重試:
使用者輸入本來就不合法。
Auth / permission 失敗。
模型明確 refusal。
Schema 定義本身有 bug。
Retry 要有上限,否則一個錯誤 request 可以變成三倍、十倍成本。
Fallback 要先定義「失敗時產品怎麼活著」
如果 structured extraction 失敗,你可以:
或要求使用者補資訊
或回到較簡單功能
或排入人工 review
不要悄悄把 invalid output 寫進正式 database,只因為「至少有東西」。
不要讓模型替你產生真正的 Database ID
例如你要分類 task,模型可以回:
{
"category": "school"
}
但不要要求模型幻想:
{
"category_id": 9472
}
如果 9472 必須對應真實 row,應由程式查 database、驗證或透過 tool 執行。
Program → authoritative ID / permission / side effect
Schema 不要設計成「什麼都 optional」
如果所有欄位都可有可無:
{}
也可能合法,那 schema 幾乎沒提供 contract。
真正需要的欄位就列入 required;允許沒有資料時,可以設計明確狀態,例如:
{
"status": "insufficient_information",
"reason": "The input does not identify a concept."
}
「不知道」也可以是一種正式資料狀態,而不是靠缺欄位猜。
Enum 很適合 UI 與 workflow
例如:
"difficulty": {
"type": "string",
"enum": [
"beginner",
"intermediate",
"advanced"
]
}
Frontend 可以直接根據固定值渲染,不必處理:
easy
Easy
簡單
超簡單
beginner-ish
Enum 是把自然語言自由度收斂成程式可以依賴的狀態。
Schema 也要版本化
今天的 response:
{
"title": "...",
"summary": "..."
}
下週多一個:
"key_points": [...]
這已經是 API contract 變更。
最小做法:
const OUTPUT_SCHEMA_VERSION = "concept-v1";
Log / database 可以記錄 schema version,讓之後知道某筆 AI data 是用哪個 contract 產生的。
Structured Output 很適合做「抽取」而不是只有回答
例如把一段學生專案描述轉成:
{
"project_name": "...",
"goal": "...",
"skills": ["..."],
"evidence": ["..."],
"missing_information": ["..."]
}
這種輸出可以進 UI、DB、搜尋索引或下一個 workflow。
但每個欄位仍然是模型根據 context 產生的判斷;如果某欄位需要真實世界驗證,就要另外查 authoritative source。
小挑戰:把 Explain 升級成 Concept Analyzer
需求 1:建立固定 JSON Schema,至少有 title、summary、difficulty、key_points。
需求 2:difficulty 使用 enum。
需求 3:拒絕 schema 以外的額外欄位。
需求 4:使用 Structured Outputs,而不是只在 prompt 寫「請回 JSON」。
需求 5:Backend parse 後再次做 application validation。
需求 6:invalid / incomplete / provider error 都有自己的處理路徑。
需求 7:Retry 最多固定次數,不可以無限 retry。
需求 8:Frontend 直接使用 object fields render,不再 split 文字。
需求 9:加入 OUTPUT_SCHEMA_VERSION。
測試 Structured Output 時,故意挑難的 input
至少測:
正常概念:「什麼是 API?」
很模糊:「那個東西是什麼?」
極短 input。
很長但合法的 input。
包含 prompt injection 文字的 input。
要求模型多加 schema 沒有的欄位。
要求 difficulty 回一個 enum 外的值。
你要驗證的是 contract 有沒有守住,而不是只看正常案例漂亮不漂亮。
最後收斂:模型輸出先是外部資料,再是產品資料
↓
Model + JSON Schema
↓
Structured model output
↓ parse
↓ validate
↓ business rules / authorization
Trusted application data
Structured Output 的目的不是讓模型「看起來更像 API」,而是把不穩定的自然語言邊界收斂成程式可以明確處理的 contract。
下一課會把資料來源往外擴:RAG。當答案需要自己的 PDF、文章、知識庫或資料文件時,不是把整份文件硬塞進 context,而是先檢索最相關的片段,再讓模型回答。
完成條件:你能用 JSON Schema 定義 AI output contract,使用 Structured Outputs 取得符合結構的結果,再經 parse 與 application validation 後才交給 frontend / database;並能處理 retry、fallback、schema version 與 provider failure。