上一篇《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。