生活分享
模型之間交接資料:JSON Schema 與結構化輸出
把交接寫成 JSON Schema,上游模型的輸出先驗證再送下游,是讓多模型流程穩定下來的做法。本篇對照 OpenAI、Anthropic、Google 三家結構化輸出參數放在請求的哪個欄位,再用 jsonschema 套件示範驗證、失敗重試,以及下游只讀取驗證過欄位的完整流程,附兩段可以直接改寫的 Python 程式。
閱讀時間約 9 分鐘

本篇目錄
多模型工作流很容易卡住的地方,不是模型答得好不好,而是前一個模型的輸出換到下一個模型手上就對不上格式。這篇要處理的問題是:怎麼把『模型之間交接的資料』寫成一份雙方都遵守的契約,讓下游程式不用猜格式、也不必每次都手動修正。結論是把這份契約寫成 JSON Schema,讓上游模型的輸出先過 jsonschema 驗證,驗證失敗就帶著錯誤訊息重試,只有通過驗證的欄位才准許往下游送。
讀完你會有一份可以直接改用的 Schema 範例、一段呼叫上游模型並驗證輸出的程式,以及失敗重試的邏輯;下游函式只接受驗證過的欄位,不吃原始文字。動手跟著做需要 Python 3.11 以上、裝好 jsonschema 套件,以及一組可用的 OPENAI_API_KEY;不需要同時申請三家的金鑰,本篇的程式只呼叫一家,另外兩家的欄位名稱在文字裡對照說明。
為什麼要先講好契約
模型之間如果只靠一段自然語言文字交接資料,下一個模型必須重新『猜』上一個模型想表達的欄位是什麼,例如日期是不是用同一種格式、有沒有漏欄位、陣列裡是不是真的都是字串。這種猜測在提示詞寫得夠清楚時多半沒事,但只要輸入內容稍微特殊、模型換了版本,或者中間多了一次轉手,格式就可能悄悄跑掉,而且不會立刻報錯,往往是下游程式解析到一半才出問題。
這篇說的『交接契約』,指的是一份雙方都認得的 JSON Schema:上游模型被要求依照它輸出,下游程式也只按照它讀取欄位,兩邊不用另外靠文件或口頭約定對格式。但 schema 只能保證形狀對,也就是型別、必填欄位、列舉值這些邊界條件符合;它沒辦法保證內容是對的,例如模型有沒有正確理解旅客的原始留言。格式驗證跟業務驗證是兩件事,後面會示範怎麼讓下游只吃通過格式驗證的欄位,但這不代表這些欄位的內容已經被確認過。
OpenAI、Anthropic、Google 把這個設定放在哪裡
OpenAI 的文件把這個功能叫做 Structured Outputs。在 Chat Completions API 裡,設定放在 response_format 欄位下,型別是 json_schema,裡面再包一個 schema 欄位與一個 strict 旗標;同一個功能改用 Responses API 呼叫時,位置換成 text.format,Python SDK 也提供 text_format 這個參數,可以直接傳 Pydantic 類別進去,不用自己組 JSON Schema。OpenAI 的文件寫,這項功能是從 GPT-4o 起的較新模型支援,新專案建議直接用 gpt-6-astra。
Anthropic 的位置不一樣,設定放在 output_config.format 底下,一樣有 type 是 json_schema 與一個 schema 欄位;Anthropic 的文件寫,舊的 output_format 參數已經搬到 output_config.format,beta 標頭不再是必要的,舊標頭與舊欄位在過渡期還會被接受;同一段也註明 Python SDK v1.0 起在 client.beta.messages.create() 與 count_tokens() 上不接受舊的 output_format 寫法,會丟 TypeError。Anthropic 另外有一個容易搞混的 strict 旗標,放在工具定義裡面,管的是工具呼叫的參數符不符合 schema,跟這裡講的『整段回覆是不是合法 JSON』是兩件事。支援的模型上,Anthropic 的相容性表列的是系列名而不是 API id:Fable 5 與 5.1、Mythos 5、5.1 與 Preview、Opus 4.5、4.6、4.7、4.8 與 5、Sonnet 4.5、4.6 與 5、Haiku 4.5;頁面範例實際帶進 output_config 的 id 是 claude-opus-5。
Google 的欄位又是另一種放法。這一頁目前示範的呼叫方式只有 Interactions API 一種:呼叫 client.interactions.create 時,把 response_format 設成一個物件,type 固定是 text,mime_type 設成 application/json,schema 欄位放實際的 JSON Schema;REST 範例打的端點是 v1beta 路徑底下的 /interactions。Google 的文件也提醒,結構化輸出如果要跟 Google 搜尋、程式碼執行這類內建工具一起用,目前還是 Preview,只開放給 Gemini 3 系列模型,本篇不會用到這個組合。
如果原始資料來自旅客上傳的檔案,本站《API 檔案與 JSON:結構化輸出及驗證》示範的是同一個 Google 功能怎麼處理 PDF 輸入,並且額外強調『JSON 合法』跟『內容真的來自檔案』是兩回事,跟本篇格式驗證不等於內容正確的提醒互相呼應。
| 供應商 | 參數放在哪裡 | 型別與巢狀放法 | 現況 | 支援模型(節錄) |
|---|---|---|---|---|
| OpenAI | Chat Completions 的 response_format;Responses API 在 text.format | type 為 json_schema,schema 與 strict 包在裡面 | 頁面沒有標 beta 或預覽;文件寫從 GPT-4o 起的較新模型支援,新專案建議用 gpt-6-astra | gpt-6-astra;文件寫『較新的大型語言模型,從 GPT-4o 起』,與 JSON mode 的對照表另外列出相容的 gpt-4o 快照版本與之後的模型 |
| Anthropic | output_config.format | type 為 json_schema,schema 包在裡面;另有獨立的 strict 旗標管工具呼叫 | 文件寫 output_format 已移到 output_config.format、beta 標頭不再必要;舊標頭與舊欄位過渡期仍接受 | 文件列的是系列名:Fable 5、5.1;Mythos 5、5.1、Preview;Opus 4.5、4.6、4.7、4.8、5;Sonnet 4.5、4.6、5;Haiku 4.5 |
| Interactions API 的 response_format | type 固定 text,mime_type 設 application/json,schema 包在裡面 | REST 端點仍在 v1beta;搭配內建工具時是 Preview,只給 Gemini 3 系列 | 頁面範例用 gemini-3.8-flash |
先定義 Schema,拿到上游模型的輸出
接下來用一個貼近旅遊網站的例子:旅客寫信要求改班機,程式要把這段話轉成四個固定欄位,再讓後續流程照著處理。範例只接上 OpenAI 一家,把 Anthropic 或 Google 換進來主要是 HTTP 呼叫那幾行不同,驗證與重試的邏輯不用改;但三家文件都寫自己只支援 JSON Schema 的一個子集,換家前要照各自的限制確認 schema。金鑰一律從環境變數讀,程式不會把它印出來或寫進檔案。
import json
import os
import urllib.request
# 交接契約:下游只認得下列四個欄位,其餘一律視為未驗證資料
CHANGE_SCHEMA = {
"type": "object",
"properties": {
"booking_id": {"type": "string", "description": "訂位代號"},
"change_type": {
"type": "string",
"enum": ["reschedule", "cancel", "seat_upgrade"],
},
"new_date": {"type": ["string", "null"], "format": "date"},
"reason": {"type": "string", "description": "旅客提出的原因"},
},
"required": ["booking_id", "change_type", "new_date", "reason"],
"additionalProperties": False,
}
API_KEY = os.environ["OPENAI_API_KEY"]
ENDPOINT = "https://api.openai.com/v1/chat/completions"
def call_model(prompt: str, schema: dict, model: str) -> str:
"""呼叫上游或下游模型,要求輸出符合 schema 的 JSON 字串。"""
body = {
"model": model,
"messages": [
{"role": "system", "content": "只輸出符合 schema 的 JSON,不要多寫其他文字。"},
{"role": "user", "content": prompt},
],
"response_format": {
"type": "json_schema",
"json_schema": {"name": "change_request", "schema": schema, "strict": True},
},
}
request = urllib.request.Request(
ENDPOINT,
data=json.dumps(body).encode("utf-8"),
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
},
method="POST",
)
with urllib.request.urlopen(request, timeout=30) as response:
payload = json.load(response)
return payload["choices"][0]["message"]["content"]
上面的 call_model 只負責把提示詞跟 schema 送出去,回傳的是模型輸出的原始文字,這段文字有沒有真的符合 schema,程式目前還沒有檢查。因為 schema 已經把 additionalProperties 設成 false、required 又把四個欄位都列進去,符合 strict 模式的要求,理論上模型應該只會輸出這四個欄位;但『理論上』不是保證:OpenAI 的文件自己列了兩種例外,一是模型基於安全理由拒答,回覆會多出一個 refusal 欄位、不一定照 schema,二是碰到 max tokens 上限、回覆被截斷而不完整,這就是驗證要接手的地方。
用 jsonschema 驗證,失敗就帶著錯誤重試
jsonschema 套件的最上層函式是 validate(),一次呼叫做兩件事:先確認 schema 本身合法,再拿 instance 去比對,其中一步不通過就丟出例外,schema 本身有問題丟的是 jsonschema.exceptions.SchemaError,instance 不符合 schema 丟的是 jsonschema.exceptions.ValidationError。官方文件寫,如果你已經確定 schema 本身沒問題、又要拿同一份 schema 驗很多筆資料,多半會偏好直接用 Draft202012Validator 這類版本化的驗證器物件,先建立一次再重複呼叫,不用每次都重新檢查 schema 本身;它的 iter_errors() 方法會把所有不符合的地方一次列完,不是抓到第一個錯誤就停,每個錯誤物件都有一個 message 屬性,是給人看的說明文字,很適合直接夾回提示詞裡讓模型自己修正。要注意的是,同一頁也寫,JSON Schema 的 format 關鍵字預設不做驗證,要另外把 format_checker 掛到驗證器上才會生效,所以下面範例的 new_date 就算標了 format 是 date,預設也不會真的去驗那串日期。
- 把 schema 和提示詞一起送給上游模型
- 用 Draft202012Validator 檢查回傳的 JSON
- 有錯誤就把 message 夾回提示詞,重新送一次
- 重試達到上限(本篇範例設成三次)還沒過,就丟出例外並保留最後一次輸出
import json
from jsonschema import Draft202012Validator
from schema_and_upstream import CHANGE_SCHEMA, call_model # 延續上一段程式
MAX_ATTEMPTS = 3
UPSTREAM_MODEL = "gpt-6-astra"
# 這個範例上下游都接 OpenAI;換成別家只要改 call_model 裡的端點與標頭
DOWNSTREAM_MODEL = "gpt-6-astra"
def parse_and_validate(prompt: str) -> dict:
"""驗證失敗就把錯誤訊息夾回提示詞,最多重試 MAX_ATTEMPTS 次。"""
validator = Draft202012Validator(CHANGE_SCHEMA)
attempt_prompt = prompt
for attempt in range(1, MAX_ATTEMPTS + 1):
raw = call_model(attempt_prompt, CHANGE_SCHEMA, UPSTREAM_MODEL)
try:
data = json.loads(raw)
except json.JSONDecodeError as error:
attempt_prompt = f"{prompt}\n上一次輸出不是合法 JSON:{error},請重新輸出。"
continue
errors = sorted(validator.iter_errors(data), key=str)
if not errors:
# 只把 schema 定義過的欄位交給下游,其餘一律丟棄
return {key: data[key] for key in CHANGE_SCHEMA["properties"]}
messages = ";".join(error.message for error in errors)
attempt_prompt = f"{prompt}\n上一次輸出不符合欄位規則:{messages},請修正後重新輸出。"
raise ValueError(f"重試 {MAX_ATTEMPTS} 次後仍未通過驗證,最後一次輸出:{raw}")
def forward_to_downstream(validated: dict) -> str:
"""下游模型只讀 validated 這個字典,看不到任何未驗證過的原始文字。"""
reply_schema = {
"type": "object",
"properties": {"reply": {"type": "string"}},
"required": ["reply"],
"additionalProperties": False,
}
summary = (
f"訂位 {validated['booking_id']} 要求 {validated['change_type']},"
f"新日期 {validated['new_date']},原因:{validated['reason']}"
)
prompt = f"用一句客氣的繁體中文回覆旅客,內容只能根據:{summary}"
raw = call_model(prompt, reply_schema, DOWNSTREAM_MODEL)
return json.loads(raw)["reply"]
下游只吃驗證過的欄位
重試迴圈通過以後,parse_and_validate 回傳的字典只會有 schema 裡 properties 列出的四個鍵,其餘不管模型有沒有多說什麼,都不會被放進這個字典,也就不會被傳到 forward_to_downstream。這樣下游函式收到的一定是形狀固定的資料,不用另外寫防禦性的 if 判斷欄位存不存在;如果之後要用同一段輸出去跑資料庫寫入或發信,也只需要對這四個欄位做業務規則檢查,不用重新解析整段回覆。這裡討論的是『輸出格式』本身的契約,跟本站另一篇《工具呼叫(Tool Calling)是什麼:模型如何請程式做事》講的『模型請程式做事』是不同的機制,兩者常常一起出現,但解決的是不同的問題。
如果只有一個模型、不需要交接給別的模型,本站《Claude Code|把 claude -p 接進有驗證的 JSON 流程》示範的是同一個概念的單模型版本,把 claude -p 輸出的 JSON 接進自己寫的驗證邏輯,再決定要不要往下一步送。多模型的差別只在於,上游輸出經過驗證以後,還要決定要不要先摘要或者換句話說,才送進下一個模型的提示詞,畢竟下游模型看到的還是文字,不是真的 Python 物件。
常見問題
為什麼不能只跟模型說『請回傳 JSON』就好?
因為模型即使回傳一個空物件或漏了幾個欄位,語法上仍然是合法 JSON,這種要求沒辦法保證有哪些欄位、型別對不對。把要求寫成 JSON Schema,再搭配 OpenAI 的 strict 模式或 jsonschema 的驗證,才能在資料送進下游之前先確認它的形狀。
OpenAI、Anthropic、Google 的結構化輸出參數可以共用同一份 schema 嗎?
大致可以共用同一份 JSON Schema 物件,但放的位置不一樣:OpenAI 放在 response_format 或 text.format,Anthropic 放在 output_config.format,Google 放在 response_format 底下的 schema 欄位。換供應商時主要改的是外層怎麼包;不過三家文件都寫自己只支援 JSON Schema 的一個子集,schema 裡用到的關鍵字仍要照各自的限制確認。
驗證失敗要重試幾次比較合理?
本篇的範例把重試上限設在三次,超過就直接丟出例外,不會無限迴圈下去;實際次數要看提示詞好不好修正、每次呼叫要付多少錢,這種取捨在系列另一篇《成本、品質、延遲:多模型流程怎麼取捨》談得更完整。
驗證通過了,是不是就代表內容一定正確?
不是。JSON Schema 能檢查的是形狀:型別對不對、必填欄位在不在、值是不是列舉裡允許的選項,沒辦法檢查模型是不是正確理解了旅客的原始留言。格式正確跟內容正確是兩件事,下游程式在讀到驗證過的欄位以後,通常還是要另外做一次業務規則檢查。
結構化輸出(Structured Outputs)跟工具呼叫(Tool Calling)是同一件事嗎?
不是同一件事,但常常一起出現。結構化輸出管的是模型『回覆』的格式,工具呼叫管的是模型『請程式做什麼』;Anthropic 的 strict 旗標剛好把兩者放在一起講,因為它讓工具呼叫的參數也符合 schema,但這仍然是兩種各自獨立的機制。
上游模型一直輸出不合法的 JSON,程式要怎麼辦?
把驗證失敗的錯誤訊息夾回提示詞裡,明確告訴模型是哪個欄位、哪一種規則沒過,通常比只說『請重新輸出』更容易修正;如果設定的重試次數用完了還是沒過,程式應該直接丟出例外並記錄原始輸出,不要放行沒驗證過的資料,也不要無限重試下去。
多模型 AI 工作流教學:從拆任務到串接不同模型多模型 AI 工作流教學:從拆任務到串接不同模型這個系列教的是怎麼把一件工作拆開、交給合適的模型,再把結果接回同一條流程:判斷該不該拆、拆給誰,換供應商不改程式,設計便宜先試的路由與級聯,讓模型之間用結構化輸出交接資料,再到代理式工具怎麼分工、同一套工具怎麼給多個客戶端共用、Claude Code 與 Codex 怎麼搭本機模型,以及上線後怎麼追蹤與防護。一般使用者可以從判斷該不該拆的觀念讀起,已經會寫 Python 的人能直接進到換供應商、寫路由與交接資料的幾篇。閱讀全文
API 檔案與 JSON:結構化輸出及驗證API 檔案與 JSON:結構化輸出及驗證Gemini API 可以讀取 PDF,再把結果整理成指定的 JSON 結構。本篇用虛構活動公告示範檔案輸入、欄位設計與本地驗證。學完後,你會知道「收到合法 JSON」與「內容確實來自檔案」是兩件需要分別檢查的事,並能保留缺漏資訊而不讓模型自行補齊。閱讀全文
同主題延伸閱讀
生活分享
Claude Code、Codex 搭本機模型:兩種接法怎麼選
Claude Code 與 Codex 搭配本機模型有兩種接法:代理照常連雲端、把大量雜務交給腳本或 MCP 工具去問本機模型,或是把代理的模型整個換成本機模型。這篇用資料能不能出門、上下文開得夠不夠長、工作的類型三個問題幫你選,並對照 Ollama、LM Studio、Anthropic 與 OpenAI 的官方文件,分清楚本機權重、Ollama 的 cloud 標籤與供應商端點是三種不同的東西。
引用本文的文章
最新旅遊情報攻略

情報
2026 韓國楓葉預測:雪嶽山 10 月 20 日、首爾近郊 10 月底、內藏山與漢拏山 11 月上旬
韓國山林廳 2026 年 9 月 22 日公布的楓紅高峰預測:雪嶽山 10 月 20 日,春川、國立樹木園到首爾植物園落在 10 月 28 日到 11 月 2 日,內藏山 11 月 4 日、漢拏山 11 月 6 日,整體比最近 5 年晚約 0.8 天。整理各地楓樹與銀杏的預測日、首爾出發怎麼排,以及出發前去哪裡看即時楓況。2026 年 10 月查證。
- 季節活動
- 自然
- 觀景

攻略胡志明市
胡志明市到頭頓一日遊:白藤碼頭搭高速船、船票與班次,下船就是胡梅纜車與耶穌基督像
人在胡志明市挪一天去頭頓看海:市中心的白藤高速船碼頭搭船,航程 120 分鐘到頭頓的胡梅碼頭,平日成人 320,000 越南盾、週末 350,000,回程末班平日 15:00。下船就是胡梅纜車站,同一條路上有白宮,小山頂上是耶穌基督像。平日一天只有兩班船,整天要從末班船倒推著排。
- 交通
- 行程範例
- 海灘

攻略沖繩
沖繩不開車攻略:單軌只到浦添,美麗海水族館要坐兩個多小時的巴士,回那霸的最後一班直達車 17:22 就開走
不租車的沖繩怎麼移動:那霸市區靠沖繩都市單軌電車(ゆいレール),那霸機場站到終點てだこ浦西 19 站、17 公里、37 分鐘,一日券 1,000 日圓;美麗海水族館有那霸機場直達的高速巴士,單程 2,000 日圓起、官方時刻表上 2 小時上下,下車後還要走 10 分鐘;古宇利島要在今帰仁村役場轉車,當天來回光坐車就六個半小時;回程的最後一班直達車 17:22 就從記念公園前開走(2026 年 9 月查證)。
- 交通
- 行程範例
- 預算
資料來源
- Structured model outputs · 查證日期:
- Structured outputs (Claude) · 查證日期:
- Structured outputs (Gemini API) · 查證日期:
- jsonschema · 查證日期:
- Schema Validation · 查證日期:
- Handling Validation Errors · 查證日期: