教程

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 型別 +responseSchemaresponseJsonSchema 形狀、型別、必填與列舉按 Schema 約束

只開 JSON 模式時,官方也提醒這更像強提示,仍有畸形輸出的小機率。要接近「總能 parse 成物件」,應同時給出 Schema。Schema 本身會計入輸入 token,所以不要把說明文字再抄一遍進 prompt:重複描述會拉低質量,也浪費配額。

responseSchema 還是 responseJsonSchema

responseSchema走的是 OpenAPI 3.0 風格的 Schema 子集,REST 裡型別名常見大寫,如OBJECTSTRING。它適合扁平物件、列舉分類、以及需要propertyOrdering釘死鍵順序的場景。短處是不吃$ref / $defs,遞迴樹、複用定義只能靠內聯,很快會膨脹。

responseJsonSchema面向 Gemini 2.5 及更新模型,吃的是更接近標準的 JSON Schema,覆蓋anyOf$refminimum / maximumadditionalPropertiestype: nullprefixItems等。你用 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。扁平任務用responseSchemaresponseJsonSchema都行;要$ref、聯合型別或從 Pydantic/Zod 生成,走後者。拿到響應後仍要本地校驗,並用格式化、Schema、Diff 把失敗樣例變成可迴歸的測試。

下一步:把你現在最容易碎的那一個介面,改成「一份 Schema、一次請求、一次本地 validate」。先讓這一條安靜下來,再複製到其他抽取任務。