跳到內容

結構化輸出

結構化輸出讓模型的回應不再是自由格式的文字,而是嚴格符合你指定的 JSON 格式。RouteAPI 支援兩種結構化輸出模式:JSON mode(要求模型輸出合法 JSON)和 JSON Schema(保證輸出符合特定 schema)。

結構化輸出是一種控制模型回應格式的機制。不同於普通對話中模型自由生成文字,結構化輸出強制模型按你定義的格式生成 JSON 資料。這對於需要程式化處理模型輸出的場景(資料擷取、表單生成、API 回應解析)至關重要。

RouteAPI 提供兩種結構化輸出模式:

對比項JSON modeJSON Schema
保證輸出是合法的 JSON輸出符合指定的 schema
參數response_format: { type: "json_object" }response_format: { type: "json_schema", json_schema: {...} }
schema 定義不需要,但需在 prompt 中說明格式必須提供完整的 JSON Schema
嚴格程度僅保證可解析,不保證結構保證欄位、類型、必填項完全符合
適用場景格式簡單、模型能從 prompt 理解結構複雜巢狀結構、需要嚴格類型校驗

簡單理解:JSON mode 只保證「能被 JSON.parse 成功解析」,但不管裡面有什麼欄位;JSON Schema 不僅保證合法,還保證結構、欄位名、類型、必填項都符合你的定義。

場景說明推薦模式
資料擷取從非結構化文字中擷取結構化資訊(姓名、地址、日期)JSON Schema
表單生成讓模型生成表單初始值或設定物件JSON Schema
API 回應解析模型輸出需要對接到下游系統的 APIJSON Schema
簡單鍵值對只需要幾個欄位,結構簡單明確JSON mode
分類任務輸出固定列舉值(如情感分類:positive/negative/neutral)JSON Schema + enum

JSON mode 是最簡單的結構化輸出方式:你在請求中設定 response_format.type 為 "json_object",模型就會輸出合法的 JSON,而不是普通文字。

{
"model": "gpt-5.5",
"messages": [
{
"role": "system",
"content": "你是一個資料擷取助手。請從使用者輸入中擷取姓名、年齡、城市三個欄位,以 JSON 格式回傳。"
},
{
"role": "user",
"content": "我叫李明,今年 28 歲,住在上海。"
}
],
"response_format": { "type": "json_object" }
}
  1. 必須在 prompt 中說明 JSON 格式:模型不知道你要什麼欄位,必須透過 system message 或 user message 明確告訴它輸出哪些欄位、什麼類型。上面的例子中,"請從使用者輸入中擷取姓名、年齡、城市三個欄位,以 JSON 格式回傳" 就是格式說明。

  2. 不保證 schema 符合性:模型可能輸出 {"name": "李明", "age": 28, "city": "上海"},也可能輸出 {"姓名": "李明", "年齡": 28},甚至 {"person": {"name": "李明"}}。只要是合法 JSON,就算符合要求。

  3. 如何使用:適合格式簡單、欄位少、模型能從自然語言理解結構的場景。如果需要嚴格校驗欄位名或類型,用 JSON Schema。

請求(curl):

Terminal window
curl https://api.routeapi.ai/v1/chat/completions \
-H "Authorization: Bearer $ROUTEAPI_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.5",
"messages": [
{
"role": "system",
"content": "你是一個資料擷取助手。從使用者輸入中擷取 name(姓名)、age(年齡)、city(城市)三個欄位,以 JSON 格式回傳。"
},
{
"role": "user",
"content": "我叫李明,今年 28 歲,住在上海。"
}
],
"response_format": { "type": "json_object" }
}'

回應:

{
"id": "chatcmpl_xxx",
"object": "chat.completion",
"created": 1730000000,
"model": "gpt-5.5",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "{\"name\": \"李明\", \"age\": 28, \"city\": \"上海\"}"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 58,
"completion_tokens": 18,
"total_tokens": 76
}
}

注意 message.content 是一個 JSON 字串,需要你自己 JSON.parse / json.loads 解析。

JSON Schema 模式讓你精確定義輸出的結構,模型保證生成的 JSON 完全符合你的 schema 定義。

{
"model": "gpt-5.5",
"messages": [
{
"role": "user",
"content": "我叫李明,今年 28 歲,住在上海。"
}
],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "person_extraction",
"strict": true,
"schema": {
"type": "object",
"properties": {
"name": { "type": "string", "description": "姓名" },
"age": { "type": "integer", "description": "年齡" },
"city": { "type": "string", "description": "城市" }
},
"required": ["name", "age", "city"],
"additionalProperties": false
}
}
}
}
欄位類型必填說明
typestring是固定為 "json_schema"
json_schema.namestring是schema 名稱,用於識別,字母、數字、底線、連字號
json_schema.strictboolean否是否啟用嚴格模式,預設 false
json_schema.schemaobject是標準 JSON Schema 定義
模式strict行為
嚴格模式true模型必須完全按 schema 生成,欄位名、類型、必填項、additionalProperties 都嚴格遵守
非嚴格模式false模型盡力符合 schema,但不保證完全一致,可能缺少欄位或新增額外欄位

推薦:生產環境使用 strict: true,這是 JSON Schema 模式的核心價值。非嚴格模式的行為接近 JSON mode + prompt 說明,意義不大。

schema 欄位遵循標準 JSON Schema 規範(Draft 2020-12),常用欄位:

欄位說明
type資料類型:"object", "array", "string", "number", "integer", "boolean", "null"
properties物件的欄位定義(type 為 "object" 時使用)
required必填欄位名陣列
additionalProperties是否允許未定義的額外欄位(嚴格模式下建議 false)
items陣列元素的 schema(type 為 "array" 時使用)
enum列舉值清單,限定取值範圍
description欄位說明,幫助模型理解語義
{
"type": "object",
"properties": {
"name": { "type": "string" },
"age": { "type": "integer" },
"score": { "type": "number" },
"is_active": { "type": "boolean" },
"notes": { "type": ["string", "null"] }
}
}

類型說明:

  • "string": 字串
  • "integer": 整數
  • "number": 數字(包括整數和小數)
  • "boolean": 布林值
  • "null": 空值
  • ["string", "null"]: 允許字串或 null(可選欄位)
{
"type": "object",
"properties": {
"user": {
"type": "object",
"properties": {
"name": { "type": "string" },
"email": { "type": "string" }
},
"required": ["name"]
},
"tags": {
"type": "array",
"items": { "type": "string" }
},
"scores": {
"type": "array",
"items": { "type": "number" }
}
}
}
{
"type": "object",
"properties": {
"name": { "type": "string" },
"age": { "type": "integer" },
"city": { "type": "string" }
},
"required": ["name", "age"]
}

required 陣列列出必須存在的欄位。上例中 name 和 age 必須有,city 可以沒有。

{
"type": "object",
"properties": {
"sentiment": {
"type": "string",
"enum": ["positive", "negative", "neutral"],
"description": "情感分類結果"
},
"priority": {
"type": "integer",
"enum": [1, 2, 3],
"description": "優先順序:1-低,2-中,3-高"
}
},
"required": ["sentiment"]
}

enum 限定欄位只能取清單中的值,模型不會生成其他值。

{
"type": "object",
"properties": {
"user": {
"type": "object",
"properties": {
"name": { "type": "string" },
"address": {
"type": "object",
"properties": {
"city": { "type": "string" },
"street": { "type": "string" }
},
"required": ["city"]
}
},
"required": ["name", "address"]
}
},
"required": ["user"]
}

物件可以無限巢狀,但過深的巢狀可能影響模型生成品質和效能。

{
"type": "object",
"properties": {
"date": {
"type": "string",
"description": "日期,格式 YYYY-MM-DD"
},
"amount": {
"type": "number",
"description": "金額,單位:元"
}
}
}

description 不是必填,但強烈建議加上。它幫助模型理解欄位語義、取值範圍和格式約定,能顯著提高生成準確度。

從非結構化文字中擷取使用者資訊:

{
"name": "user_info_extraction",
"strict": true,
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "使用者姓名"
},
"age": {
"type": "integer",
"description": "年齡"
},
"email": {
"type": ["string", "null"],
"description": "電子郵箱,如果文字中沒有則為 null"
},
"phone": {
"type": ["string", "null"],
"description": "手機號碼,如果文字中沒有則為 null"
},
"city": {
"type": "string",
"description": "所在城市"
}
},
"required": ["name", "age", "city"],
"additionalProperties": false
}
}

讓模型生成一個商品陣列:

{
"name": "product_list",
"strict": true,
"schema": {
"type": "object",
"properties": {
"products": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "商品名稱"
},
"price": {
"type": "number",
"description": "價格,單位:元"
},
"category": {
"type": "string",
"enum": ["electronics", "clothing", "food", "other"],
"description": "商品分類"
},
"in_stock": {
"type": "boolean",
"description": "是否有貨"
}
},
"required": ["name", "price", "category", "in_stock"],
"additionalProperties": false
}
},
"total_count": {
"type": "integer",
"description": "商品總數"
}
},
"required": ["products", "total_count"],
"additionalProperties": false
}
}

訂單資訊擷取,包含多層巢狀:

{
"name": "order_extraction",
"strict": true,
"schema": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "訂單號"
},
"customer": {
"type": "object",
"properties": {
"name": { "type": "string" },
"phone": { "type": "string" },
"address": {
"type": "object",
"properties": {
"province": { "type": "string" },
"city": { "type": "string" },
"street": { "type": "string" }
},
"required": ["province", "city", "street"],
"additionalProperties": false
}
},
"required": ["name", "phone", "address"],
"additionalProperties": false
},
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"product_name": { "type": "string" },
"quantity": { "type": "integer" },
"unit_price": { "type": "number" }
},
"required": ["product_name", "quantity", "unit_price"],
"additionalProperties": false
}
},
"total_amount": {
"type": "number",
"description": "訂單總金額"
}
},
"required": ["order_id", "customer", "items", "total_amount"],
"additionalProperties": false
}
}

大多數 RouteAPI 聚合的主流模型都支援 JSON mode(response_format: { type: "json_object" }),包括:

  • OpenAI GPT 系列(gpt-4o, gpt-4-turbo, gpt-3.5-turbo 等)
  • Claude 系列(claude-3.5-sonnet, claude-3-opus, claude-3-haiku 等)
  • Gemini 系列(gemini-2.0-flash, gemini-1.5-pro 等)
  • 其他支援 OpenAI 格式的模型

JSON Schema(response_format: { type: "json_schema" })對模型能力要求更高,目前支援的模型:

  • OpenAI: gpt-4o 系列、gpt-4-turbo 系列(2024-08-06 及以後版本)
  • Claude: claude-3.5-sonnet 系列、claude-3-opus 系列
  • Gemini: gemini-2.0-flash-exp、gemini-1.5-pro 系列
  • 其他:部分新一代模型

如何確認:呼叫前透過 Models 介面 查詢模型能力,檢查 supports_response_format 欄位。

能力維度JSON modeJSON Schema說明
支援模型範圍廣泛(幾乎所有主流模型)有限(新一代模型)JSON mode 支援度更高
schema 複雜度不適用建議不超過 3 層巢狀過深影響生成品質
最大欄位數不適用建議不超過 50 個頂層欄位欄位過多影響效能
嚴格保證僅保證合法 JSON保證符合 schema嚴格模式下 100% 符合
效能快相對較慢schema 校驗有額外開銷
Token 消耗低稍高schema 定義會佔用 prompt tokens
  1. schema 大小限制:單個 schema 定義建議不超過 10KB,過大的 schema 可能被截斷或拒絕。

  2. 巢狀深度限制:建議巢狀層數不超過 3-4 層,過深的巢狀會降低模型生成品質和速度。

  3. 效能影響:JSON Schema 模式的回應時間通常比普通請求慢 10%-30%,因為模型需要在生成過程中即時校驗結構。

  4. 不支援的 schema 特性:部分進階 JSON Schema 特性(如 $ref、allOf、anyOf、oneOf、正規表示式)可能不被所有模型支援。

  5. 串流輸出:JSON Schema 模式支援串流輸出(stream: true),但 content 是逐片段回傳的,完整 JSON 需要拼接後再解析。

範例 1:從非結構化文字擷取結構化資訊

Section titled “範例 1:從非結構化文字擷取結構化資訊”

場景:從客服對話中擷取客戶資訊和問題分類。

curl:

Terminal window
curl https://api.routeapi.ai/v1/chat/completions \
-H "Authorization: Bearer $ROUTEAPI_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o",
"messages": [
{
"role": "user",
"content": "客戶李明(手機 13800138000)反映在北京市朝陽區的訂單 ORD-2024-001 一直沒發貨,比較著急。"
}
],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "customer_inquiry",
"strict": true,
"schema": {
"type": "object",
"properties": {
"customer_name": { "type": "string", "description": "客戶姓名" },
"phone": { "type": ["string", "null"], "description": "客戶手機號碼" },
"location": { "type": ["string", "null"], "description": "客戶所在地" },
"order_id": { "type": ["string", "null"], "description": "訂單號" },
"issue_category": {
"type": "string",
"enum": ["delivery", "quality", "refund", "other"],
"description": "問題分類:delivery-物流,quality-品質,refund-退款,other-其他"
},
"urgency": {
"type": "string",
"enum": ["low", "medium", "high"],
"description": "緊急程度"
}
},
"required": ["customer_name", "issue_category", "urgency"],
"additionalProperties": false
}
}
}
}'

回應:

{
"choices": [
{
"message": {
"role": "assistant",
"content": "{\"customer_name\":\"李明\",\"phone\":\"13800138000\",\"location\":\"北京市朝陽區\",\"order_id\":\"ORD-2024-001\",\"issue_category\":\"delivery\",\"urgency\":\"high\"}"
},
"finish_reason": "stop"
}
]
}

Python:

import os
import json
from openai import OpenAI
client = OpenAI(
api_key=os.environ["ROUTEAPI_KEY"],
base_url="https://api.routeapi.ai/v1",
)
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "user",
"content": "客戶李明(手機 13800138000)反映在北京市朝陽區的訂單 ORD-2024-001 一直沒發貨,比較著急。",
}
],
response_format={
"type": "json_schema",
"json_schema": {
"name": "customer_inquiry",
"strict": True,
"schema": {
"type": "object",
"properties": {
"customer_name": {"type": "string", "description": "客戶姓名"},
"phone": {"type": ["string", "null"], "description": "客戶手機號碼"},
"location": {"type": ["string", "null"], "description": "客戶所在地"},
"order_id": {"type": ["string", "null"], "description": "訂單號"},
"issue_category": {
"type": "string",
"enum": ["delivery", "quality", "refund", "other"],
"description": "問題分類",
},
"urgency": {
"type": "string",
"enum": ["low", "medium", "high"],
"description": "緊急程度",
},
},
"required": ["customer_name", "issue_category", "urgency"],
"additionalProperties": False,
},
},
},
)
data = json.loads(response.choices[0].message.content)
print(f"客戶: {data['customer_name']}")
print(f"問題類型: {data['issue_category']}")
print(f"緊急程度: {data['urgency']}")

Node.js:

import OpenAI from 'openai';
const client = new OpenAI({
apiKey: process.env.ROUTEAPI_KEY,
baseURL: 'https://api.routeapi.ai/v1',
});
const response = await client.chat.completions.create({
model: 'gpt-4o',
messages: [
{
role: 'user',
content: '客戶李明(手機 13800138000)反映在北京市朝陽區的訂單 ORD-2024-001 一直沒發貨,比較著急。',
},
],
response_format: {
type: 'json_schema',
json_schema: {
name: 'customer_inquiry',
strict: true,
schema: {
type: 'object',
properties: {
customer_name: { type: 'string', description: '客戶姓名' },
phone: { type: ['string', 'null'], description: '客戶手機號碼' },
location: { type: ['string', 'null'], description: '客戶所在地' },
order_id: { type: ['string', 'null'], description: '訂單號' },
issue_category: {
type: 'string',
enum: ['delivery', 'quality', 'refund', 'other'],
description: '問題分類',
},
urgency: {
type: 'string',
enum: ['low', 'medium', 'high'],
description: '緊急程度',
},
},
required: ['customer_name', 'issue_category', 'urgency'],
additionalProperties: false,
},
},
},
});
const data = JSON.parse(response.choices[0].message.content);
console.log(`客戶: ${data.customer_name}`);
console.log(`問題類型: ${data.issue_category}`);
console.log(`緊急程度: ${data.urgency}`);

場景:讓模型根據自然語言描述生成表單初始值。

Python:

response = client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "system",
"content": "你是一個表單填充助手。根據使用者描述生成表單資料。",
},
{
"role": "user",
"content": "建立一個新員工到職表單:張偉,男,1995 年出生,大學學歷,軟體工程師職位,月薪 15000。",
},
],
response_format={
"type": "json_schema",
"json_schema": {
"name": "employee_form",
"strict": True,
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"gender": {"type": "string", "enum": ["male", "female"]},
"birth_year": {"type": "integer"},
"education": {
"type": "string",
"enum": ["high_school", "bachelor", "master", "phd"],
},
"position": {"type": "string"},
"salary": {"type": "number", "description": "月薪,單位:元"},
},
"required": ["name", "gender", "birth_year", "education", "position", "salary"],
"additionalProperties": False,
},
},
},
)
form_data = json.loads(response.choices[0].message.content)
print(json.dumps(form_data, ensure_ascii=False, indent=2))

輸出:

{
"name": "張偉",
"gender": "male",
"birth_year": 1995,
"education": "bachelor",
"position": "軟體工程師",
"salary": 15000
}

場景:讓模型從第三方 API 的非結構化回應中擷取關鍵資訊。

Node.js:

const apiResponse = `
訂單狀態:已發貨
物流公司:順豐速運
運單號:SF1234567890
預計送達:2024 年 1 月 20 日
目前位置:北京市分撥中心
`;
const response = await client.chat.completions.create({
model: 'gpt-4o',
messages: [
{
role: 'user',
content: `從以下物流資訊中擷取結構化資料:\n${apiResponse}`,
},
],
response_format: {
type: 'json_schema',
json_schema: {
name: 'logistics_info',
strict: true,
schema: {
type: 'object',
properties: {
status: {
type: 'string',
enum: ['pending', 'shipped', 'in_transit', 'delivered'],
},
carrier: { type: 'string', description: '物流公司' },
tracking_number: { type: 'string', description: '運單號' },
estimated_delivery: {
type: ['string', 'null'],
description: '預計送達日期,格式 YYYY-MM-DD',
},
current_location: { type: ['string', 'null'], description: '目前位置' },
},
required: ['status', 'carrier', 'tracking_number'],
additionalProperties: false,
},
},
},
});
const data = JSON.parse(response.choices[0].message.content);
console.log(data);
// 輸出: { status: 'shipped', carrier: '順豐速運', tracking_number: 'SF1234567890', estimated_delivery: '2024-01-20', current_location: '北京市分撥中心' }

如果你的 schema 定義本身有問題(語法錯誤、不支援的特性),請求會直接回傳 400 錯誤:

{
"error": {
"message": "Invalid JSON schema: ...",
"type": "invalid_request_error",
"param": "response_format.json_schema.schema",
"code": "invalid_json_schema"
}
}

解決:

  • 檢查 schema 語法是否符合 JSON Schema 規範
  • 移除不支援的進階特性(如 $ref、allOf)
  • 簡化過深的巢狀結構

極少數情況下,模型可能無法生成符合 schema 的內容(如使用者輸入與 schema 要求完全衝突),此時會回傳錯誤或降級為普通文字輸出。錯誤範例:

{
"error": {
"message": "Failed to generate valid output matching the provided schema after maximum retries.",
"type": "model_error",
"code": "schema_generation_failed"
}
}

解決:

  • 檢查 prompt 是否與 schema 要求一致
  • 簡化 schema,移除過於嚴格的限制
  • 在 prompt 中明確說明輸出要求
  • 對於可選欄位,使用 ["string", "null"] 類型而不是僅 "string"

對於偶發的生成失敗,可以實作自動重試:

import time
def call_with_retry(client, **kwargs):
max_retries = 3
for attempt in range(max_retries):
try:
response = client.chat.completions.create(**kwargs)
content = response.choices[0].message.content
data = json.loads(content) # 驗證是否能解析
return data
except (json.JSONDecodeError, Exception) as e:
if attempt == max_retries - 1:
raise
time.sleep(2 ** attempt) # 指數退避
  1. 從簡單開始:先用 JSON mode 驗證可行性,確認需要嚴格校驗後再升級到 JSON Schema。

  2. 明確必填和可選:必填欄位放 required 陣列,可選欄位用 ["type", "null"] 或乾脆不放 required。

  3. 用 enum 限定列舉:凡是取值有限的欄位(狀態、分類、優先順序),都用 enum 顯式列出,能大幅降低錯誤率。

  4. 加 description:每個欄位都加 description,說明含義、格式、取值範圍,模型據此生成更準確。

  5. 設定 additionalProperties: false:嚴格模式下,加這個能防止模型輸出未定義的額外欄位。

問題說明建議
巢狀過深超過 4 層巢狀會降低生成品質拆分為多個扁平物件,或用多輪對話分步擷取
欄位過多單個物件超過 50 個欄位按業務邏輯分組,拆分為多個子物件
過度限制所有欄位都是必填,沒有彈性只把核心欄位標為必填,其他欄位允許 null
無 description模型不理解欄位語義每個欄位都加 description,說明清楚
  1. 減少 schema 大小:schema 定義會佔用 prompt tokens,過大的 schema 會增加延遲和成本。

  2. 快取 schema 定義:同一個 schema 反覆使用時,在程式碼中定義為常數,避免每次請求都重新建構。

  3. 選擇合適的模型:不是所有任務都需要最強的模型,簡單的結構化擷取可以用 gpt-3.5-turbo + JSON mode。

  4. 批次處理:如果有多個相似任務,可以設計一個陣列 schema,讓模型一次處理多筆資料。

因素影響最佳化建議
schema 大小schema 定義佔用 prompt tokens精簡 description,移除冗餘欄位
模型選擇JSON Schema 通常需要較強模型簡單任務用 JSON mode + 弱模型
回應長度JSON 輸出通常比文字更長(鍵名、引號、括號)縮短欄位名,用列舉代替長字串
重試次數生成失敗重試會翻倍計費最佳化 schema 和 prompt 降低失敗率

實用 tip:對於高頻呼叫的場景,先用 JSON mode 測試,確認模型能穩定輸出正確格式後再升級到 JSON Schema。JSON mode 的 token 消耗和成本通常比 JSON Schema 低 10%-20%。

  • JSON mode 和 JSON Schema 的支援度取決於所選模型,請透過 Models 介面 查詢 supports_response_format 欄位確認。
  • response_format 與 tools(工具呼叫)互斥:同一個請求不能同時使用結構化輸出和工具呼叫。
  • 串流輸出(stream: true)下,content 是逐片段回傳的,需要完整拼接後再 JSON.parse。
  • 明確傳入 0 或 false 的可選參數會被視為使用者顯式設定,不會當作預設丟棄。
  • 記錄每次請求的 request ID、模型 ID、狀態碼和 token 用量,便於排查。錯誤結構詳見 錯誤與偵錯。