教程
Gemini API 如何輸出 JSON?Structured Output 與 JSON Schema 完整教學
不要再讓模型「盡量返回 JSON」。用 Structured Output 把形狀釘死,再用 JSON Schema 約束欄位,下游程式才能穩定解析。
你讓 Gemini 返回 JSON,它卻夾一段解釋、漏一個逗號,或把欄位名改成「更通順」的英文。提示詞裡寫「只輸出 JSON」只能降低機率,不能當契約。Structured Output 把契約放到解碼階段:先宣告 MIME 型別,再掛上 Schema,模型按形狀吐 token。讀完你可以判斷該用 JSON 模式還是 Schema 模式,寫得出能跑的請求,並知道返回值仍要本地校驗。
為什麼需要結構化輸出
下游一旦JSON.parse,失敗就不是文案問題,而是整條流水線中斷。分類介面需要固定列舉,抽取介面需要穩定鍵名,工具呼叫需要引數物件。這些場景裡,模型「自由發揮」的成本很高:一次壞 JSON 就要重試、打日誌、甚至讓使用者再點一次。
更隱蔽的失敗是「能 parse,但形狀不對」。例如你期望items是陣列,模型給了物件;你期望score是數字,模型給了"0.9"。程式碼讀到undefined,問題會拖到很後面才爆。Structured Output 解決的是形狀,不是對錯:它保證你拿到合法且符合 Schema 的 JSON,不保證分類一定正確。所以生產裡仍然要校驗業務規則,只是解析層會安靜很多。
官方能力說明見Gemini Structured output 文件。Google 也宣佈了對標準 JSON Schema 關鍵詞與欄位順序的增強,見Structured Outputs 更新說明。寫 Schema 前,建議對照Understanding JSON Schema,避免把「文件裡能寫的關鍵詞」和「當前模型實際約束的關鍵詞」混為一談。
JSON 模式與 Schema 模式
可以把它想成兩檔開關。第一檔只保證「這是一段能 parse 的 JSON」。第二檔保證「這是符合你宣告的那份 Schema 的 JSON」。程式碼要讀具名欄位時,用第二檔。只有探索性抽取、鍵名本身也交給模型時,才考慮第一檔。
| 檔位 | 你配置什麼 | 實際保證 |
|---|---|---|
| JSON 模式 | 僅responseMimeType: application/json |
大機率是合法 JSON,欄位名與巢狀仍由模型決定 |
| Schema 模式 | MIME 型別 +responseSchema或responseJsonSchema |
形狀、型別、必填與列舉按 Schema 約束 |
只開 JSON 模式時,官方也提醒這更像強提示,仍有畸形輸出的小機率。要接近「總能 parse 成物件」,應同時給出 Schema。Schema 本身會計入輸入 token,所以不要把說明文字再抄一遍進 prompt:重複描述會拉低質量,也浪費配額。
responseSchema 還是 responseJsonSchema
responseSchema走的是 OpenAPI 3.0 風格的 Schema 子集,REST 裡型別名常見大寫,如OBJECT、STRING。它適合扁平物件、列舉分類、以及需要propertyOrdering釘死鍵順序的場景。短處是不吃$ref / $defs,遞迴樹、複用定義只能靠內聯,很快會膨脹。
responseJsonSchema面向 Gemini 2.5 及更新模型,吃的是更接近標準的 JSON Schema,覆蓋anyOf、$ref、minimum / maximum、additionalProperties、type: null、prefixItems等。你用 Pydantic 或 Zod 生成 Schema 再塞進去,摩擦會小很多。鍵順序在較新模型上會按 Schema 宣告順序保留,對日誌 diff 和黃金集對比都友好。
選型可以記三條。只要扁平分類或抽取,兩種都能用。需要遞迴、共享定義或聯合型別,優先responseJsonSchema。必須釘死欄位出現順序,確認當前模型是否仍支援propertyOrdering,不要假設所有端點行為一致。
Schema 怎麼寫
先寫你真正要讀的物件,而不是「儘可能完整的世界模型」。每多一個可選欄位,模型就多一次填錯或填空的機會。必填項寫進required,列舉寫進enum,數字範圍用minimum / maximum。給屬性加description往往比在 prompt 裡再解釋一遍更穩,因為約束和解碼綁在一起。
下面這份 Schema 模擬工單分類:類別只能是三個值之一,優先順序是整數,摘要是字串。這就是 Schema 模式該管的事——形狀,而不是「這張工單在業務上該不該標緊急」。
{
"type": "object",
"properties": {
"category": {
"type": "string",
"enum": ["billing", "bug", "feature"],
"description": "Ticket category"
},
"priority": {
"type": "integer",
"minimum": 1,
"maximum": 5
},
"summary": {
"type": "string"
}
},
"required": ["category", "priority", "summary"],
"additionalProperties": false
}
陣列用items描述元素。元組感更強的固定長度列表,在 JSON Schema 路徑下看prefixItems。可空欄位不要只靠「不寫 required」,而要明確允許null,否則模型可能省略鍵,你的程式碼還在obj.field上假設鍵一定存在。
巢狀物件把子 Schema 內聯進去即可。只有出現第三次相同結構,或樹節點引用自身時,才值得上$defs + $ref。過早抽象會讓報錯更難讀:服務端拒絕 Schema 時,你要對著一份展開後的大物件排查。
Python 與 JavaScript 實操
下面用 Google 官方 SDK 的常見寫法。模型名請按你賬號裡實際可用的 2.5 / 更新型號替換,不要把示例裡的名字當成長期釘死的生產配置。
Python:MIME 型別 + JSON Schema
from google import genai
client = genai.Client()
schema = {
"type": "object",
"properties": {
"category": {"type": "string", "enum": ["billing", "bug", "feature"]},
"priority": {"type": "integer"},
"summary": {"type": "string"},
},
"required": ["category", "priority", "summary"],
}
response = client.models.generate_content(
model="gemini-2.5-flash",
contents="Classify this ticket: invoice PDF cannot be downloaded.",
config={
"response_mime_type": "application/json",
"response_json_schema": schema,
},
)
print(response.text)
若團隊已經用 Pydantic 建模,直接Model.model_json_schema()交給response_json_schema,再用model_validate_json(response.text)做本地二次校驗。第一層是 API 約束形狀,第二層是你的型別系統拒絕「看起來合法、語義荒唐」的值,例如優先順序寫成 99。
JavaScript:generationConfig
const response = await ai.models.generateContent({
model: "gemini-2.5-flash",
contents: "Classify this ticket: invoice PDF cannot be downloaded.",
config: {
responseMimeType: "application/json",
responseJsonSchema: {
type: "object",
properties: {
category: { type: "string", enum: ["billing", "bug", "feature"] },
priority: { type: "integer" },
summary: { type: "string" },
},
required: ["category", "priority", "summary"],
},
},
});
const data = JSON.parse(response.text);
REST 呼叫則把同樣的欄位放進generationConfig。OpenAPI 風格的responseSchema在部分端點仍用大寫型別名,拷文件示例時不要和 JSON Schema 的小寫object混用。接到響應後立刻JSON.parse,不要先用正則摳程式碼塊:既然你宣告瞭 JSON MIME,就按「整段都是 JSON」處理,少一層脆弱解析。
常見陷阱
- Prompt 和 Schema 各寫一遍結構,模型會在兩種描述之間搖擺,輸出質量下降。
- Schema 過大:複雜巢狀、過深
$ref、過寬anyOf可能被拒絕,或約束變弱。先拆成小物件跑通,再合併。 - 把「真實性」交給 Schema。列舉能限制類別集合,不能阻止模型選錯類別。關鍵路徑要抽檢或上規則。
- 本地型別與 Schema 漂移。Pydantic / Zod 改了欄位,請求裡的 Schema 還是舊的,線上會靜默出現新鍵或缺鍵。
- 只測快樂路徑。補兩三個邊界:空陣列、可空欄位、超長字串、非法列舉(應被擋住)。
還有一個工程問題:日誌裡不要只存response.text。把所用模型、Schema 雜湊、prompt 版本一起記下。結構化輸出出問題,多半是「換了模型預設值」或「Schema 微改導致拒絕」,沒有這三件套你會覺得「昨天還能用」。
落地:校驗、對照與返工
API 返回後,先當它是不可信位元組。解析、按同一份 Schema 再校驗、再對映到內部型別。失敗時記錄原始文字(注意脫敏),決定重試還是降級。重試不要換一套完全不同的 Schema,否則你無法判斷是模型不穩還是契約在變。
除錯階段把樣例 JSON 貼進本站工具最省事。先用JSON 格式化看清巢狀,再用JSON 校驗抓語法,最後把契約丟進JSON Schema看例項是否過關。欄位突然變多或變少時,用JSON Diff對比兩次響應,比肉眼掃日誌快。
如果你在設計抽取管道,也可以先手寫一份「理想輸出」,用 Schema 工具反推型別,再把這份 Schema 貼回 Gemini 請求。這樣契約的源頭只有一處:不是聊天記錄裡的口頭約定,而是一份能跑測試的文件。
常見問題 FAQ
只設 application/json 夠不夠?
探索階段可以。只要程式碼讀取固定欄位,就同時提供 Schema。否則你得到的是「像 JSON 的散文」,不是介面。
Structured Output 能替代函式呼叫嗎?
不能互相替代。函式呼叫是讓模型選工具並填引數;Structured Output 是約束這一次回答的形狀。要執行程式碼或打外部 API,用工具;只要一段型別化資料,用 Structured Output,少一圈往返。
為什麼 Schema 被拒絕?
常見原因是用了當前端點不支援的關鍵詞、遞迴過深、或把 responseSchema 與 responseJsonSchema 的方言混用。把 Schema 減到一個物件三個欄位,確認能通,再往上加。
輸出一定正確嗎?
不一定。它保證形狀合法,不保證事實。金額、郵箱、工單類別仍要規則或人工抽檢。
總結與下一步
穩定拿到 JSON,靠的不是更長的「請只輸出 JSON」,而是 MIME 型別加 Schema。扁平任務用responseSchema或responseJsonSchema都行;要$ref、聯合型別或從 Pydantic/Zod 生成,走後者。拿到響應後仍要本地校驗,並用格式化、Schema、Diff 把失敗樣例變成可迴歸的測試。
下一步:把你現在最容易碎的那一個介面,改成「一份 Schema、一次請求、一次本地 validate」。先讓這一條安靜下來,再複製到其他抽取任務。