結構化輸出
結構化輸出讓模型的回應不再是自由格式的文字,而是嚴格符合你指定的 JSON 格式。RouteAPI 支援兩種結構化輸出模式:JSON mode(要求模型輸出合法 JSON)和 JSON Schema(保證輸出符合特定 schema)。
1. 結構化輸出概述
Section titled “1. 結構化輸出概述”什麼是結構化輸出
Section titled “什麼是結構化輸出”結構化輸出是一種控制模型回應格式的機制。不同於普通對話中模型自由生成文字,結構化輸出強制模型按你定義的格式生成 JSON 資料。這對於需要程式化處理模型輸出的場景(資料擷取、表單生成、API 回應解析)至關重要。
JSON mode vs JSON Schema
Section titled “JSON mode vs JSON Schema”RouteAPI 提供兩種結構化輸出模式:
| 對比項 | JSON mode | JSON Schema |
|---|---|---|
| 保證 | 輸出是合法的 JSON | 輸出符合指定的 schema |
| 參數 | response_format: { type: "json_object" } | response_format: { type: "json_schema", json_schema: {...} } |
| schema 定義 | 不需要,但需在 prompt 中說明格式 | 必須提供完整的 JSON Schema |
| 嚴格程度 | 僅保證可解析,不保證結構 | 保證欄位、類型、必填項完全符合 |
| 適用場景 | 格式簡單、模型能從 prompt 理解結構 | 複雜巢狀結構、需要嚴格類型校驗 |
簡單理解:JSON mode 只保證「能被 JSON.parse 成功解析」,但不管裡面有什麼欄位;JSON Schema 不僅保證合法,還保證結構、欄位名、類型、必填項都符合你的定義。
| 場景 | 說明 | 推薦模式 |
|---|---|---|
| 資料擷取 | 從非結構化文字中擷取結構化資訊(姓名、地址、日期) | JSON Schema |
| 表單生成 | 讓模型生成表單初始值或設定物件 | JSON Schema |
| API 回應解析 | 模型輸出需要對接到下游系統的 API | JSON Schema |
| 簡單鍵值對 | 只需要幾個欄位,結構簡單明確 | JSON mode |
| 分類任務 | 輸出固定列舉值(如情感分類:positive/negative/neutral) | JSON Schema + enum |
2. JSON Mode
Section titled “2. JSON Mode”JSON mode 是最簡單的結構化輸出方式:你在請求中設定 response_format.type 為 "json_object",模型就會輸出合法的 JSON,而不是普通文字。
{ "model": "gpt-5.5", "messages": [ { "role": "system", "content": "你是一個資料擷取助手。請從使用者輸入中擷取姓名、年齡、城市三個欄位,以 JSON 格式回傳。" }, { "role": "user", "content": "我叫李明,今年 28 歲,住在上海。" } ], "response_format": { "type": "json_object" }}-
必須在 prompt 中說明 JSON 格式:模型不知道你要什麼欄位,必須透過 system message 或 user message 明確告訴它輸出哪些欄位、什麼類型。上面的例子中,
"請從使用者輸入中擷取姓名、年齡、城市三個欄位,以 JSON 格式回傳"就是格式說明。 -
不保證 schema 符合性:模型可能輸出
{"name": "李明", "age": 28, "city": "上海"},也可能輸出{"姓名": "李明", "年齡": 28},甚至{"person": {"name": "李明"}}。只要是合法 JSON,就算符合要求。 -
如何使用:適合格式簡單、欄位少、模型能從自然語言理解結構的場景。如果需要嚴格校驗欄位名或類型,用 JSON Schema。
請求(curl):
curl https://api.routeapi.ai/v1/chat/completions \ -H "Authorization: Bearer $ROUTEAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-5.5", "messages": [ { "role": "system", "content": "你是一個資料擷取助手。從使用者輸入中擷取 name(姓名)、age(年齡)、city(城市)三個欄位,以 JSON 格式回傳。" }, { "role": "user", "content": "我叫李明,今年 28 歲,住在上海。" } ], "response_format": { "type": "json_object" } }'回應:
{ "id": "chatcmpl_xxx", "object": "chat.completion", "created": 1730000000, "model": "gpt-5.5", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "{\"name\": \"李明\", \"age\": 28, \"city\": \"上海\"}" }, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 58, "completion_tokens": 18, "total_tokens": 76 }}注意 message.content 是一個 JSON 字串,需要你自己 JSON.parse / json.loads 解析。
3. JSON Schema(Structured Outputs)
Section titled “3. JSON Schema(Structured Outputs)”JSON Schema 模式讓你精確定義輸出的結構,模型保證生成的 JSON 完全符合你的 schema 定義。
{ "model": "gpt-5.5", "messages": [ { "role": "user", "content": "我叫李明,今年 28 歲,住在上海。" } ], "response_format": { "type": "json_schema", "json_schema": { "name": "person_extraction", "strict": true, "schema": { "type": "object", "properties": { "name": { "type": "string", "description": "姓名" }, "age": { "type": "integer", "description": "年齡" }, "city": { "type": "string", "description": "城市" } }, "required": ["name", "age", "city"], "additionalProperties": false } } }}response_format 結構
Section titled “response_format 結構”| 欄位 | 類型 | 必填 | 說明 |
|---|---|---|---|
type | string | 是 | 固定為 "json_schema" |
json_schema.name | string | 是 | schema 名稱,用於識別,字母、數字、底線、連字號 |
json_schema.strict | boolean | 否 | 是否啟用嚴格模式,預設 false |
json_schema.schema | object | 是 | 標準 JSON Schema 定義 |
嚴格模式 vs 非嚴格模式
Section titled “嚴格模式 vs 非嚴格模式”| 模式 | strict | 行為 |
|---|---|---|
| 嚴格模式 | true | 模型必須完全按 schema 生成,欄位名、類型、必填項、additionalProperties 都嚴格遵守 |
| 非嚴格模式 | false | 模型盡力符合 schema,但不保證完全一致,可能缺少欄位或新增額外欄位 |
推薦:生產環境使用 strict: true,這是 JSON Schema 模式的核心價值。非嚴格模式的行為接近 JSON mode + prompt 說明,意義不大。
schema 定義規範
Section titled “schema 定義規範”schema 欄位遵循標準 JSON Schema 規範(Draft 2020-12),常用欄位:
| 欄位 | 說明 |
|---|---|
type | 資料類型:"object", "array", "string", "number", "integer", "boolean", "null" |
properties | 物件的欄位定義(type 為 "object" 時使用) |
required | 必填欄位名陣列 |
additionalProperties | 是否允許未定義的額外欄位(嚴格模式下建議 false) |
items | 陣列元素的 schema(type 為 "array" 時使用) |
enum | 列舉值清單,限定取值範圍 |
description | 欄位說明,幫助模型理解語義 |
4. Schema 定義指南
Section titled “4. Schema 定義指南”{ "type": "object", "properties": { "name": { "type": "string" }, "age": { "type": "integer" }, "score": { "type": "number" }, "is_active": { "type": "boolean" }, "notes": { "type": ["string", "null"] } }}類型說明:
"string": 字串"integer": 整數"number": 數字(包括整數和小數)"boolean": 布林值"null": 空值["string", "null"]: 允許字串或 null(可選欄位)
{ "type": "object", "properties": { "user": { "type": "object", "properties": { "name": { "type": "string" }, "email": { "type": "string" } }, "required": ["name"] }, "tags": { "type": "array", "items": { "type": "string" } }, "scores": { "type": "array", "items": { "type": "number" } } }}{ "type": "object", "properties": { "name": { "type": "string" }, "age": { "type": "integer" }, "city": { "type": "string" } }, "required": ["name", "age"]}required 陣列列出必須存在的欄位。上例中 name 和 age 必須有,city 可以沒有。
{ "type": "object", "properties": { "sentiment": { "type": "string", "enum": ["positive", "negative", "neutral"], "description": "情感分類結果" }, "priority": { "type": "integer", "enum": [1, 2, 3], "description": "優先順序:1-低,2-中,3-高" } }, "required": ["sentiment"]}enum 限定欄位只能取清單中的值,模型不會生成其他值。
{ "type": "object", "properties": { "user": { "type": "object", "properties": { "name": { "type": "string" }, "address": { "type": "object", "properties": { "city": { "type": "string" }, "street": { "type": "string" } }, "required": ["city"] } }, "required": ["name", "address"] } }, "required": ["user"]}物件可以無限巢狀,但過深的巢狀可能影響模型生成品質和效能。
{ "type": "object", "properties": { "date": { "type": "string", "description": "日期,格式 YYYY-MM-DD" }, "amount": { "type": "number", "description": "金額,單位:元" } }}description 不是必填,但強烈建議加上。它幫助模型理解欄位語義、取值範圍和格式約定,能顯著提高生成準確度。
5. Schema 範例
Section titled “5. Schema 範例”使用者資訊擷取
Section titled “使用者資訊擷取”從非結構化文字中擷取使用者資訊:
{ "name": "user_info_extraction", "strict": true, "schema": { "type": "object", "properties": { "name": { "type": "string", "description": "使用者姓名" }, "age": { "type": "integer", "description": "年齡" }, "email": { "type": ["string", "null"], "description": "電子郵箱,如果文字中沒有則為 null" }, "phone": { "type": ["string", "null"], "description": "手機號碼,如果文字中沒有則為 null" }, "city": { "type": "string", "description": "所在城市" } }, "required": ["name", "age", "city"], "additionalProperties": false }}商品清單生成
Section titled “商品清單生成”讓模型生成一個商品陣列:
{ "name": "product_list", "strict": true, "schema": { "type": "object", "properties": { "products": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string", "description": "商品名稱" }, "price": { "type": "number", "description": "價格,單位:元" }, "category": { "type": "string", "enum": ["electronics", "clothing", "food", "other"], "description": "商品分類" }, "in_stock": { "type": "boolean", "description": "是否有貨" } }, "required": ["name", "price", "category", "in_stock"], "additionalProperties": false } }, "total_count": { "type": "integer", "description": "商品總數" } }, "required": ["products", "total_count"], "additionalProperties": false }}複雜巢狀物件
Section titled “複雜巢狀物件”訂單資訊擷取,包含多層巢狀:
{ "name": "order_extraction", "strict": true, "schema": { "type": "object", "properties": { "order_id": { "type": "string", "description": "訂單號" }, "customer": { "type": "object", "properties": { "name": { "type": "string" }, "phone": { "type": "string" }, "address": { "type": "object", "properties": { "province": { "type": "string" }, "city": { "type": "string" }, "street": { "type": "string" } }, "required": ["province", "city", "street"], "additionalProperties": false } }, "required": ["name", "phone", "address"], "additionalProperties": false }, "items": { "type": "array", "items": { "type": "object", "properties": { "product_name": { "type": "string" }, "quantity": { "type": "integer" }, "unit_price": { "type": "number" } }, "required": ["product_name", "quantity", "unit_price"], "additionalProperties": false } }, "total_amount": { "type": "number", "description": "訂單總金額" } }, "required": ["order_id", "customer", "items", "total_amount"], "additionalProperties": false }}6. 各模型支援情況
Section titled “6. 各模型支援情況”支援 JSON mode 的模型
Section titled “支援 JSON mode 的模型”大多數 RouteAPI 聚合的主流模型都支援 JSON mode(response_format: { type: "json_object" }),包括:
- OpenAI GPT 系列(gpt-4o, gpt-4-turbo, gpt-3.5-turbo 等)
- Claude 系列(claude-3.5-sonnet, claude-3-opus, claude-3-haiku 等)
- Gemini 系列(gemini-2.0-flash, gemini-1.5-pro 等)
- 其他支援 OpenAI 格式的模型
支援 JSON Schema 的模型
Section titled “支援 JSON Schema 的模型”JSON Schema(response_format: { type: "json_schema" })對模型能力要求更高,目前支援的模型:
- OpenAI: gpt-4o 系列、gpt-4-turbo 系列(2024-08-06 及以後版本)
- Claude: claude-3.5-sonnet 系列、claude-3-opus 系列
- Gemini: gemini-2.0-flash-exp、gemini-1.5-pro 系列
- 其他:部分新一代模型
如何確認:呼叫前透過 Models 介面 查詢模型能力,檢查 supports_response_format 欄位。
模型能力對比表
Section titled “模型能力對比表”| 能力維度 | JSON mode | JSON Schema | 說明 |
|---|---|---|---|
| 支援模型範圍 | 廣泛(幾乎所有主流模型) | 有限(新一代模型) | JSON mode 支援度更高 |
| schema 複雜度 | 不適用 | 建議不超過 3 層巢狀 | 過深影響生成品質 |
| 最大欄位數 | 不適用 | 建議不超過 50 個頂層欄位 | 欄位過多影響效能 |
| 嚴格保證 | 僅保證合法 JSON | 保證符合 schema | 嚴格模式下 100% 符合 |
| 效能 | 快 | 相對較慢 | schema 校驗有額外開銷 |
| Token 消耗 | 低 | 稍高 | schema 定義會佔用 prompt tokens |
限制和注意事項
Section titled “限制和注意事項”-
schema 大小限制:單個 schema 定義建議不超過 10KB,過大的 schema 可能被截斷或拒絕。
-
巢狀深度限制:建議巢狀層數不超過 3-4 層,過深的巢狀會降低模型生成品質和速度。
-
效能影響:JSON Schema 模式的回應時間通常比普通請求慢 10%-30%,因為模型需要在生成過程中即時校驗結構。
-
不支援的 schema 特性:部分進階 JSON Schema 特性(如
$ref、allOf、anyOf、oneOf、正規表示式)可能不被所有模型支援。 -
串流輸出:JSON Schema 模式支援串流輸出(
stream: true),但content是逐片段回傳的,完整 JSON 需要拼接後再解析。
7. 完整應用範例
Section titled “7. 完整應用範例”範例 1:從非結構化文字擷取結構化資訊
Section titled “範例 1:從非結構化文字擷取結構化資訊”場景:從客服對話中擷取客戶資訊和問題分類。
curl:
curl https://api.routeapi.ai/v1/chat/completions \ -H "Authorization: Bearer $ROUTEAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-4o", "messages": [ { "role": "user", "content": "客戶李明(手機 13800138000)反映在北京市朝陽區的訂單 ORD-2024-001 一直沒發貨,比較著急。" } ], "response_format": { "type": "json_schema", "json_schema": { "name": "customer_inquiry", "strict": true, "schema": { "type": "object", "properties": { "customer_name": { "type": "string", "description": "客戶姓名" }, "phone": { "type": ["string", "null"], "description": "客戶手機號碼" }, "location": { "type": ["string", "null"], "description": "客戶所在地" }, "order_id": { "type": ["string", "null"], "description": "訂單號" }, "issue_category": { "type": "string", "enum": ["delivery", "quality", "refund", "other"], "description": "問題分類:delivery-物流,quality-品質,refund-退款,other-其他" }, "urgency": { "type": "string", "enum": ["low", "medium", "high"], "description": "緊急程度" } }, "required": ["customer_name", "issue_category", "urgency"], "additionalProperties": false } } } }'回應:
{ "choices": [ { "message": { "role": "assistant", "content": "{\"customer_name\":\"李明\",\"phone\":\"13800138000\",\"location\":\"北京市朝陽區\",\"order_id\":\"ORD-2024-001\",\"issue_category\":\"delivery\",\"urgency\":\"high\"}" }, "finish_reason": "stop" } ]}Python:
import osimport jsonfrom openai import OpenAI
client = OpenAI( api_key=os.environ["ROUTEAPI_KEY"], base_url="https://api.routeapi.ai/v1",)
response = client.chat.completions.create( model="gpt-4o", messages=[ { "role": "user", "content": "客戶李明(手機 13800138000)反映在北京市朝陽區的訂單 ORD-2024-001 一直沒發貨,比較著急。", } ], response_format={ "type": "json_schema", "json_schema": { "name": "customer_inquiry", "strict": True, "schema": { "type": "object", "properties": { "customer_name": {"type": "string", "description": "客戶姓名"}, "phone": {"type": ["string", "null"], "description": "客戶手機號碼"}, "location": {"type": ["string", "null"], "description": "客戶所在地"}, "order_id": {"type": ["string", "null"], "description": "訂單號"}, "issue_category": { "type": "string", "enum": ["delivery", "quality", "refund", "other"], "description": "問題分類", }, "urgency": { "type": "string", "enum": ["low", "medium", "high"], "description": "緊急程度", }, }, "required": ["customer_name", "issue_category", "urgency"], "additionalProperties": False, }, }, },)
data = json.loads(response.choices[0].message.content)print(f"客戶: {data['customer_name']}")print(f"問題類型: {data['issue_category']}")print(f"緊急程度: {data['urgency']}")Node.js:
import OpenAI from 'openai';
const client = new OpenAI({ apiKey: process.env.ROUTEAPI_KEY, baseURL: 'https://api.routeapi.ai/v1',});
const response = await client.chat.completions.create({ model: 'gpt-4o', messages: [ { role: 'user', content: '客戶李明(手機 13800138000)反映在北京市朝陽區的訂單 ORD-2024-001 一直沒發貨,比較著急。', }, ], response_format: { type: 'json_schema', json_schema: { name: 'customer_inquiry', strict: true, schema: { type: 'object', properties: { customer_name: { type: 'string', description: '客戶姓名' }, phone: { type: ['string', 'null'], description: '客戶手機號碼' }, location: { type: ['string', 'null'], description: '客戶所在地' }, order_id: { type: ['string', 'null'], description: '訂單號' }, issue_category: { type: 'string', enum: ['delivery', 'quality', 'refund', 'other'], description: '問題分類', }, urgency: { type: 'string', enum: ['low', 'medium', 'high'], description: '緊急程度', }, }, required: ['customer_name', 'issue_category', 'urgency'], additionalProperties: false, }, }, },});
const data = JSON.parse(response.choices[0].message.content);console.log(`客戶: ${data.customer_name}`);console.log(`問題類型: ${data.issue_category}`);console.log(`緊急程度: ${data.urgency}`);範例 2:生成表單資料
Section titled “範例 2:生成表單資料”場景:讓模型根據自然語言描述生成表單初始值。
Python:
response = client.chat.completions.create( model="gpt-4o", messages=[ { "role": "system", "content": "你是一個表單填充助手。根據使用者描述生成表單資料。", }, { "role": "user", "content": "建立一個新員工到職表單:張偉,男,1995 年出生,大學學歷,軟體工程師職位,月薪 15000。", }, ], response_format={ "type": "json_schema", "json_schema": { "name": "employee_form", "strict": True, "schema": { "type": "object", "properties": { "name": {"type": "string"}, "gender": {"type": "string", "enum": ["male", "female"]}, "birth_year": {"type": "integer"}, "education": { "type": "string", "enum": ["high_school", "bachelor", "master", "phd"], }, "position": {"type": "string"}, "salary": {"type": "number", "description": "月薪,單位:元"}, }, "required": ["name", "gender", "birth_year", "education", "position", "salary"], "additionalProperties": False, }, }, },)
form_data = json.loads(response.choices[0].message.content)print(json.dumps(form_data, ensure_ascii=False, indent=2))輸出:
{ "name": "張偉", "gender": "male", "birth_year": 1995, "education": "bachelor", "position": "軟體工程師", "salary": 15000}範例 3:API 回應解析
Section titled “範例 3:API 回應解析”場景:讓模型從第三方 API 的非結構化回應中擷取關鍵資訊。
Node.js:
const apiResponse = `訂單狀態:已發貨物流公司:順豐速運運單號:SF1234567890預計送達:2024 年 1 月 20 日目前位置:北京市分撥中心`;
const response = await client.chat.completions.create({ model: 'gpt-4o', messages: [ { role: 'user', content: `從以下物流資訊中擷取結構化資料:\n${apiResponse}`, }, ], response_format: { type: 'json_schema', json_schema: { name: 'logistics_info', strict: true, schema: { type: 'object', properties: { status: { type: 'string', enum: ['pending', 'shipped', 'in_transit', 'delivered'], }, carrier: { type: 'string', description: '物流公司' }, tracking_number: { type: 'string', description: '運單號' }, estimated_delivery: { type: ['string', 'null'], description: '預計送達日期,格式 YYYY-MM-DD', }, current_location: { type: ['string', 'null'], description: '目前位置' }, }, required: ['status', 'carrier', 'tracking_number'], additionalProperties: false, }, }, },});
const data = JSON.parse(response.choices[0].message.content);console.log(data);// 輸出: { status: 'shipped', carrier: '順豐速運', tracking_number: 'SF1234567890', estimated_delivery: '2024-01-20', current_location: '北京市分撥中心' }8. 錯誤處理
Section titled “8. 錯誤處理”Schema 驗證失敗
Section titled “Schema 驗證失敗”如果你的 schema 定義本身有問題(語法錯誤、不支援的特性),請求會直接回傳 400 錯誤:
{ "error": { "message": "Invalid JSON schema: ...", "type": "invalid_request_error", "param": "response_format.json_schema.schema", "code": "invalid_json_schema" }}解決:
- 檢查 schema 語法是否符合 JSON Schema 規範
- 移除不支援的進階特性(如
$ref、allOf) - 簡化過深的巢狀結構
模型無法生成符合 schema 的輸出
Section titled “模型無法生成符合 schema 的輸出”極少數情況下,模型可能無法生成符合 schema 的內容(如使用者輸入與 schema 要求完全衝突),此時會回傳錯誤或降級為普通文字輸出。錯誤範例:
{ "error": { "message": "Failed to generate valid output matching the provided schema after maximum retries.", "type": "model_error", "code": "schema_generation_failed" }}解決:
- 檢查 prompt 是否與 schema 要求一致
- 簡化 schema,移除過於嚴格的限制
- 在 prompt 中明確說明輸出要求
- 對於可選欄位,使用
["string", "null"]類型而不是僅"string"
對於偶發的生成失敗,可以實作自動重試:
import time
def call_with_retry(client, **kwargs): max_retries = 3 for attempt in range(max_retries): try: response = client.chat.completions.create(**kwargs) content = response.choices[0].message.content data = json.loads(content) # 驗證是否能解析 return data except (json.JSONDecodeError, Exception) as e: if attempt == max_retries - 1: raise time.sleep(2 ** attempt) # 指數退避9. 最佳實務
Section titled “9. 最佳實務”Schema 設計原則
Section titled “Schema 設計原則”-
從簡單開始:先用 JSON mode 驗證可行性,確認需要嚴格校驗後再升級到 JSON Schema。
-
明確必填和可選:必填欄位放
required陣列,可選欄位用["type", "null"]或乾脆不放required。 -
用 enum 限定列舉:凡是取值有限的欄位(狀態、分類、優先順序),都用
enum顯式列出,能大幅降低錯誤率。 -
加 description:每個欄位都加
description,說明含義、格式、取值範圍,模型據此生成更準確。 -
設定 additionalProperties: false:嚴格模式下,加這個能防止模型輸出未定義的額外欄位。
避免過於複雜的 schema
Section titled “避免過於複雜的 schema”| 問題 | 說明 | 建議 |
|---|---|---|
| 巢狀過深 | 超過 4 層巢狀會降低生成品質 | 拆分為多個扁平物件,或用多輪對話分步擷取 |
| 欄位過多 | 單個物件超過 50 個欄位 | 按業務邏輯分組,拆分為多個子物件 |
| 過度限制 | 所有欄位都是必填,沒有彈性 | 只把核心欄位標為必填,其他欄位允許 null |
| 無 description | 模型不理解欄位語義 | 每個欄位都加 description,說明清楚 |
-
減少 schema 大小:schema 定義會佔用 prompt tokens,過大的 schema 會增加延遲和成本。
-
快取 schema 定義:同一個 schema 反覆使用時,在程式碼中定義為常數,避免每次請求都重新建構。
-
選擇合適的模型:不是所有任務都需要最強的模型,簡單的結構化擷取可以用 gpt-3.5-turbo + JSON mode。
-
批次處理:如果有多個相似任務,可以設計一個陣列 schema,讓模型一次處理多筆資料。
| 因素 | 影響 | 最佳化建議 |
|---|---|---|
| schema 大小 | schema 定義佔用 prompt tokens | 精簡 description,移除冗餘欄位 |
| 模型選擇 | JSON Schema 通常需要較強模型 | 簡單任務用 JSON mode + 弱模型 |
| 回應長度 | JSON 輸出通常比文字更長(鍵名、引號、括號) | 縮短欄位名,用列舉代替長字串 |
| 重試次數 | 生成失敗重試會翻倍計費 | 最佳化 schema 和 prompt 降低失敗率 |
實用 tip:對於高頻呼叫的場景,先用 JSON mode 測試,確認模型能穩定輸出正確格式後再升級到 JSON Schema。JSON mode 的 token 消耗和成本通常比 JSON Schema 低 10%-20%。