上一篇《AI Agent 為什麼離不開 JSON》順著一次呼叫拆了 Tool Calling 與 MCP 的資料流。本文換到模型對使用者(或下游程式)的最終答覆:如何讓 Gemini 直接吐出可解析、可校驗、可入庫的 JSON,而不是「看起來像 JSON 的散文」。
這是 Gemini 檔案裡的 Structured Outputs(結構化輸出 / 受控生成)。它和 Function Calling 共用同一套 Schema 思想,但目標不同:前者約束最終載荷,後者約束工具引數。把兩者分清,Agent 才不會把「抽發票」和「調支付介面」寫成同一種呼叫。
三種做法,一層比一層硬
團隊裡常見三種「讓 Gemini 出 JSON」的辦法,可靠性差一個數量級:
| 做法 | 你控制什麼 | 適合什麼 |
|---|---|---|
| 只在提示詞裡寫「請輸出 JSON」 | 軟約束,模型仍可能加 markdown 圍欄或註釋 | 探索、一次性指令碼 |
responseMimeType: application/json | 輸出必須是合法 JSON 文字 | 形狀多變、先保證能 parse |
MIME + responseSchema / responseJsonSchema | 欄位、型別、列舉、必填都被約束 | 生產抽取、填表、Agent 間傳遞 |
第一層失敗形態人人見過:```json 程式碼塊、末尾多一段解釋、單引號、尾逗號。第二層能 JSON.parse,但 price 可能變成字串,items 可能缺。第三層才是本教程的重點——把 JSON Schema 交給 API,讓解碼器在生成每個 token 時就避開非法路徑。
約束解碼:為什麼 Schema 比提示詞更硬
提示詞只能提高「模型願意遵守」的機率。Structured Output 把 Schema 編譯進生成過程:下一步若會破壞 JSON 語法或偏離 Schema(例如該輸出 " 卻要開始一個未宣告欄位),該 token 的機率會被壓掉。所以你拿到的 response.text 通常已經是一份可解析物件,而不必先寫正則剝圍欄。
2025 年起 Gemini API 在原有 OpenAPI 3.0 風格 responseSchema 之外,補上了對標準 JSON Schema 的支援(請求裡常對應 responseJsonSchema)。這意味著 Python 的 Pydantic model_json_schema()、TypeScript 的 Zod 匯出,可以幾乎原樣送給 Gemini。Gemini 2.5 及之後還會盡量保持 Schema 裡的欄位順序,方便和下游表格、CSV 列對齊。
分類任務還有一條旁路:responseMimeType: text/x.enum,模型只吐列舉字串(如 Keyboard),連花括號都沒有。需要物件時用 application/json;只需要一個標籤時用 enum MIME。
Python:google-genai 完整示例
推薦官方新 SDK google-genai(from google import genai),不要和舊包 google-generativeai 混用。環境變數 GEMINI_API_KEY 配好後:
from google import genai
from pydantic import BaseModel, Field
class LineItem(BaseModel):
name: str = Field(description="商品名稱")
qty: int = Field(description="數量,正整數")
unit_price_cents: int = Field(description="單價,分")
class Invoice(BaseModel):
vendor: str
currency: str = Field(description="ISO 4217,如 CNY")
items: list[LineItem]
total_cents: int
client = genai.Client()
response = client.models.generate_content(
model="gemini-2.5-flash",
contents="從下面文字抽取發票:Acme 賣了 2 個鍵盤,每個 199 元人民幣。",
config={
"response_mime_type": "application/json",
"response_schema": Invoice,
},
)
print(response.text) # JSON 字串
invoice = response.parsed # Invoice 例項(Pydantic 路徑)
print(invoice.total_cents)
response.parsed 只有在 response_schema 傳入 Pydantic / SDK 型別時才有意義。若你只傳原始 JSON Schema 字典(見下一節),請 json.loads(response.text) 後再自己校驗。
多條記錄用 list[Invoice] 或外層再包一個帶 invoices: list[Invoice] 的物件。陣列根型別在部分模型上不如「永遠返回 object」穩,生產裡更常見後者。
response_schema 與 JSON Schema
兩條設定不要混著猜:
- response_schema:Pydantic 模型、Python Enum、或 SDK 的 Schema 物件。SDK 會轉換成線上的 OpenAPI 子集。
- response_json_schema:一份 JSON Schema 物件(字典)。適合
Invoice.model_json_schema()、Zod 的toJSONSchema(),以及additionalProperties、minimum/maximum、prefixItems等更完整的約束。
schema = {
"type": "object",
"properties": {
"vendor": { "type": "string" },
"currency": { "type": "string", "enum": ["CNY", "USD", "EUR"] },
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": { "type": "string" },
"qty": { "type": "integer", "minimum": 1 },
"unit_price_cents": { "type": "integer", "minimum": 0 }
},
"required": ["name", "qty", "unit_price_cents"],
"additionalProperties": False
}
},
"total_cents": { "type": "integer" }
},
"required": ["vendor", "currency", "items", "total_cents"],
"additionalProperties": False
}
response = client.models.generate_content(
model="gemini-2.5-flash",
contents="抽取發票:……",
config={
"response_mime_type": "application/json",
"response_json_schema": schema,
},
)
REST 檔案裡舊式 responseSchema 常用大寫型別名(OBJECT、STRING、ARRAY、INTEGER)。JSON Schema 路徑則是小寫 object / string。不要把兩套關鍵字交叉貼上。欄位說明寫進 description:它會進模型上下文,決定「qty 是件數還是箱數」這種語義,光靠型別擋不住。
REST 請求長什麼樣
Gemini Developer API 的 generateContent 把結構化輸出放在 generationConfig。金鑰走 x-goog-api-key 或查詢引數,不要寫進前端倉庫。
POST https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent
{
"contents": [
{
"role": "user",
"parts": [{ "text": "從文字抽取發票:……" }]
}
],
"generationConfig": {
"responseMimeType": "application/json",
"responseJsonSchema": {
"type": "object",
"properties": {
"vendor": { "type": "string" },
"total_cents": { "type": "integer" }
},
"required": ["vendor", "total_cents"]
}
}
}
成功時候選文字在 candidates[0].content.parts[0].text,內容是 JSON 字串。Vertex AI 上欄位名相同,只是 endpoint 與鑑權換成 GCP。圖片、PDF 一樣可以當輸入:Schema 約束的是輸出,不限制多模態輸入。
和 Function Calling 怎麼分工
兩者都是「用 Schema 管 JSON」,但發生在對話的不同跳:
| Structured Output | Function Calling / Tool Calling | |
|---|---|---|
| 約束物件 | 最終答覆 JSON | 工具引數 JSON |
| 誰執行副作用 | 沒有;只是資料 | 宿主 / MCP Server |
| 典型設定 | responseMimeType + Schema | tools[].parameters / inputSchema |
| 失敗怎麼迴流 | 校驗失敗則重試或人工 | 把錯誤寫成 tool 訊息再問模型 |
發票抽取、內容稽核標籤、把會議紀要變成任務列表:Structured Output。查庫存、建立工單、讀倉庫檔案:工具呼叫,細節見《資料流解析》和《JSON Schema 與 MCP 演進》。不要用 Structured Output 假裝成「已經調過支付 API」——模型沒有真的調。
落地校驗與常見坑
約束解碼不是業務正確性保證。建議固定兩道閘:
- 語法與 Schema:
json.loads後用同一份 JSON Schema 再校驗(必填、enum、minimum)。 - 業務不變數:例如
sum(item.qty * item.unit_price_cents) == total_cents。這層 Schema 表達不了,必須自己寫。
常見坑:
- 關鍵字不被支援:把完整 Draft 2020-12 一股腦塞進去,部分關鍵字會被忽略,表現像「Schema 沒生效」。先從 type / properties / required / enum / items 做起,再加 additionalProperties、min/max。
- 根型別用 array:部分路徑更穩的是
{ "items": [ ... ] }這種物件根。 - 把 Markdown 說明和 JSON 混在同一段輸出:開了 JSON MIME 後不要再要求「先解釋再輸出 JSON」。
- 超長輸出截斷:提高 maxOutputTokens,或把任務拆成「先列表、再逐條補全」。
- 金鑰進前端:結構化輸出常被放進瀏覽器演示,API Key 會洩漏。Schema 可以公開,金鑰只能在服務端。
開發階段把 Schema 與 2~3 組正/反例存進倉庫,用 JSON 工具箱本機看結構、做 Diff。這和測 REST 契約同一思路,消費者換成了 Gemini。
常見問題 FAQ
只設 application/json 和同時給 Schema 有什麼區別?
只設 responseMimeType 為 application/json,模型會盡量輸出合法 JSON,但欄位名、型別、是否缺欄位不受約束。加上 responseSchema 或 responseJsonSchema 後,解碼階段會按藍圖約束 token,形狀才穩定,才能直接入庫或餵給下一跳 Agent。
response_schema 和 response_json_schema 怎麼選?
Python 裡用 Pydantic 模型或 SDK Schema 時走 response_schema,SDK 可把結果放到 response.parsed。需要完整 JSON Schema(additionalProperties、min/max、prefixItems 等)或把 Pydantic/Zod 的 model_json_schema() 直接送進 API 時,用 response_json_schema。二者都要配合 response_mime_type=application/json。
結構化輸出能替代 Function Calling 嗎?
不能互相替代。結構化輸出約束的是模型對使用者可見的最終 JSON;Function Calling / Tool Calling 約束的是工具引數 JSON,還要由宿主真正執行工具。抽取、分類、填表用 Structured Output;查天氣、寫檔案、調 MCP 用工具呼叫。一條 Agent 鏈路常常兩者都用。
模型能保證 100% 符合 Schema 嗎?
約束解碼大幅降低語法錯誤和型別漂移,但仍可能出現語義幻覺(欄位型別對、值是編的)、截斷、或個別不受支援的 Schema 關鍵字被忽略。生產環境應把同一份 Schema 再跑一遍校驗器,失敗則重試或降級。
支援巢狀物件、陣列和列舉嗎?
支援。物件、陣列、字串列舉是最常用的組合。分類任務也可以把 MIME 設為 text/x.enum,讓模型只輸出列舉值而不是 JSON 物件。巢狀層級過深或迴圈引用仍可能被拒絕,應把 Schema 寫扁一些。
如何在本機校驗 Schema 與樣例輸出?
把 responseJsonSchema 和兩三組模型輸出樣例存成 JSON 檔案,用 JSON 工具箱在瀏覽器本機做語法校驗與結構對照,資料不上傳伺服器。上線後再用同一份 Schema 做執行時校驗。
總結
讓 Gemini 生成結構化 JSON,正確順序是:先定 Schema,再開 JSON MIME,最後才寫提示詞。提示詞負責語義(抽什麼);Schema 負責形狀(欄位長什麼樣)。Pydantic / Zod 是作者友好的前端,線上走 response_schema 或 response_json_schema。
建議你用一張真實發票或一段客服對話跑通:寫出 Schema → 打一次 generateContent → 把 response.text 貼進校驗器對照。對得上再接資料庫或下一跳 Agent。工具引數那一側仍然走 Function Calling / MCP,不要混成一種 API。