Structured Outputs 具體示例教程
場景:個人財務管理助手
假設我們要構建一個 AI 助手,幫助用戶記錄和管理個人財務支出。用戶可以輸入自然語言描述(如“昨天我花了50元買了午餐”),助手將提取關鍵信息并以結構化 JSON 格式返回,包括日期、金額、類別和備注。
示例 1:使用 Structured Outputs 提取財務記錄
步驟 1:定義 JSON Schema
我們需要一個清晰的 Schema 來描述財務記錄:
{"type": "object","properties": {"date": {"type": "string","description": "支出日期,格式為 YYYY-MM-DD"},"amount": {"type": "number","description": "支出金額,單位為人民幣(元)"},"category": {"type": "string","enum": ["餐飲", "交通", "娛樂", "購物", "其他"],"description": "支出類別"},"note": {"type": ["string", "null"],"description": "可選備注,若無則為 null"}},"required": ["date", "amount", "category", "note"],"additionalProperties": false
}
設計要點:
date
使用標準日期格式。amount
為數字類型,確保精確。category
使用枚舉限制可選值。note
可選,通過"type": ["string", "null"]
實現。
步驟 2:實現 API 調用
使用 Python 實現,提取用戶輸入中的財務信息:
from openai import OpenAI
import json
from datetime import datetime, timedeltaclient = OpenAI()# 定義 Schema
schema = {"type": "object","properties": {"date": {"type": "string", "description": "支出日期,格式為 YYYY-MM-DD"},"amount": {"type": "number", "description": "支出金額,單位為人民幣(元)"},"category": {"type": "string","enum": ["餐飲", "交通", "娛樂", "購物", "其他"],"description": "支出類別"},"note": {"type": ["string", "null"], "description": "可選備注,若無則為 null"}},"required": ["date", "amount", "category", "note"],"additionalProperties": False
}# 計算昨天的日期(假設當前日期為 2025-03-16)
yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")response = client.responses.create(model="gpt-4o-2024-08-06",input=[{"role": "system", "content": "你是一個財務管理助手,從用戶輸入中提取結構化支出信息。如果信息不完整,返回合理默認值。"},{"role": "user", "content": "昨天我花了50元買了午餐"}],text={"format": {"type": "json_schema","name": "expense_record","schema": schema,"strict": True}}
)# 解析結果
expense = json.loads(response.output_text)
print(json.dumps(expense, indent=2, ensure_ascii=False))
輸出
{"date": "2025-03-15","amount": 50,"category": "餐飲","note": "買了午餐"
}
解析說明:
date
:模型根據“昨天”推斷為 2025-03-15(假設當前為 2025-03-16)。amount
:從“50元”提取為數字 50。category
:根據“午餐”推斷為“餐飲”。note
:提取“買了午餐”作為備注。
步驟 3:處理邊緣情況
添加錯誤處理,應對拒絕或不完整響應:
try:response = client.responses.create(model="gpt-4o-2024-08-06",input=[{"role": "system", "content": "你是一個財務管理助手,從用戶輸入中提取結構化支出信息。如果信息不完整,返回合理默認值。"},{"role": "user", "content": "昨天我花了50元買了午餐"}],max_output_tokens=20, # 模擬令牌限制text={"format": {"type": "json_schema", "name": "expense_record", "schema": schema, "strict": True}})if response.status == "incomplete" and response.incomplete_details.reason == "max_output_tokens":print("錯誤:輸出令牌數不足,無法生成完整響應")elif response.output[0].content[0].type == "refusal":print(f"模型拒絕:{response.output[0].content[0].refusal}")else:expense = json.loads(response.output_text)print(json.dumps(expense, indent=2, ensure_ascii=False))except Exception as e:print(f"API 調用失敗:{e}")
可能輸出(令牌限制情況):
錯誤:輸出令牌數不足,無法生成完整響應
示例 2:結合 Function Calling 和 Structured Outputs
場景:保存財務記錄到數據庫
現在我們擴展功能,讓模型不僅提取支出信息,還調用函數將其保存到數據庫。
步驟 1:定義 Function Calling 和 Structured Outputs
Function Schema
{"type": "function","name": "save_expense","description": "將支出記錄保存到數據庫","parameters": {"type": "object","properties": {"date": {"type": "string", "description": "支出日期,YYYY-MM-DD"},"amount": {"type": "number", "description": "支出金額(元)"},"category": {"type": "string", "enum": ["餐飲", "交通", "娛樂", "購物", "其他"]},"note": {"type": ["string", "null"]}},"required": ["date", "amount", "category", "note"],"additionalProperties": False}
}
Structured Output Schema(用于最終響應)
{"type": "object","properties": {"status": {"type": "string", "enum": ["success", "error"]},"message": {"type": "string"}},"required": ["status", "message"],"additionalProperties": False
}
步驟 2:實現代碼
from openai import OpenAI
import json
from datetime import datetime, timedeltaclient = OpenAI()# 函數定義
tools = [{"type": "function","name": "save_expense","description": "將支出記錄保存到數據庫","parameters": {"type": "object","properties": {"date": {"type": "string", "description": "支出日期,YYYY-MM-DD"},"amount": {"type": "number", "description": "支出金額(元)"},"category": {"type": "string", "enum": ["餐飲", "交通", "娛樂", "購物", "其他"]},"note": {"type": ["string", "null"]}},"required": ["date", "amount", "category", "note"],"additionalProperties": False}
}]# Structured Output Schema
response_schema = {"type": "object","properties": {"status": {"type": "string", "enum": ["success", "error"]},"message": {"type": "string"}},"required": ["status", "message"],"additionalProperties": False
}# 用戶輸入
input_messages = [{"role": "system", "content": "你是一個財務管理助手,提取支出信息并保存到數據庫。"},{"role": "user", "content": "昨天我花了50元買了午餐"}
]# 第一次調用:提取并調用函數
response = client.responses.create(model="gpt-4o-2024-08-06",input=input_messages,tools=tools
)# 處理函數調用
tool_call = response.output[0]
if tool_call.type == "function_call":args = json.loads(tool_call.arguments)def save_expense(date, amount, category, note):# 模擬數據庫保存return f"記錄保存成功:{date}, {amount}元, {category}, {note}"result = save_expense(**args)# 將函數調用和結果追加到消息中input_messages.append(tool_call)input_messages.append({"type": "function_call_output","call_id": tool_call.call_id,"output": result})# 第二次調用:生成結構化響應
response_2 = client.responses.create(model="gpt-4o-2024-08-06",input=input_messages,text={"format": {"type": "json_schema","name": "save_response","schema": response_schema,"strict": True}}
)final_response = json.loads(response_2.output_text)
print(json.dumps(final_response, indent=2, ensure_ascii=False))
輸出
{"status": "success","message": "記錄保存成功:2025-03-15, 50元, 餐飲, 買了午餐"
}
流程說明:
- 第一次調用識別并調用
save_expense
函數。 - 執行函數,模擬保存到數據庫。
- 第二次調用使用 Structured Outputs 返回最終狀態。
優化建議
-
動態日期處理:
- 在系統提示中明確日期推斷規則,如“‘昨天’應轉換為當前日期減一天”。
- 示例:
"將相對日期(如‘昨天’)轉換為 YYYY-MM-DD 格式,基于當前日期 2025-03-16。"
-
錯誤處理增強:
- 添加對無效金額或類別的驗證。
- 示例:若用戶輸入“花了abc元”,返回
{"status": "error", "message": "金額無效"}
。
-
多記錄支持:
- 修改 Schema 支持數組,如:
{"type": "array","items": {"$ref": "#/definitions/expense"},"definitions": {"expense": {...}} }
- 修改 Schema 支持數組,如:
-
流式輸出:
- 對于長響應,使用
stream=True
實時顯示結果。
- 對于長響應,使用
示例 1:健康記錄場景實現
場景描述
我們要構建一個健康管理助手,用戶可以輸入自然語言(如“今天早上我跑了5公里,心率達到120次/分鐘”),助手將提取健康數據并以結構化 JSON 格式返回,包括日期、活動類型、持續時間、心率等信息。
步驟 1:定義 JSON Schema
{"type": "object","properties": {"date": {"type": "string","description": "活動日期,格式為 YYYY-MM-DD"},"activity": {"type": "string","enum": ["跑步", "游泳", "騎行", "瑜伽", "其他"],"description": "活動類型"},"duration": {"type": ["number", "null"],"description": "活動持續時間(分鐘),若未知則為 null"},"heart_rate": {"type": ["number", "null"],"description": "平均心率(次/分鐘),若未知則為 null"},"notes": {"type": ["string", "null"],"description": "附加備注,若無則為 null"}},"required": ["date", "activity", "duration", "heart_rate", "notes"],"additionalProperties": false
}
設計要點:
duration
和heart_rate
可選,使用"type": ["number", "null"]
。activity
使用枚舉限制常見類型。date
要求標準格式。
步驟 2:實現代碼
from openai import OpenAI
import json
from datetime import datetimeclient = OpenAI()# 定義 Schema
health_schema = {"type": "object","properties": {"date": {"type": "string", "description": "活動日期,格式為 YYYY-MM-DD"},"activity": {"type": "string", "enum": ["跑步", "游泳", "騎行", "瑜伽", "其他"]},"duration": {"type": ["number", "null"], "description": "活動持續時間(分鐘)"},"heart_rate": {"type": ["number", "null"], "description": "平均心率(次/分鐘)"},"notes": {"type": ["string", "null"], "description": "附加備注"}},"required": ["date", "activity", "duration", "heart_rate", "notes"],"additionalProperties": False
}# 當前日期
today = datetime.now().strftime("%Y-%m-%d")response = client.responses.create(model="gpt-4o-2024-08-06",input=[{"role": "system","content": "你是一個健康管理助手,從用戶輸入中提取結構化健康數據。‘今天’指 {today},若信息缺失則返回 null。"},{"role": "user", "content": "今天早上我跑了5公里,心率達到120次/分鐘"}],text={"format": {"type": "json_schema","name": "health_record","schema": health_schema,"strict": True}}
)health_record = json.loads(response.output_text)
print(json.dumps(health_record, indent=2, ensure_ascii=False))
輸出
{"date": "2025-03-16","activity": "跑步","duration": null,"heart_rate": 120,"notes": "跑了5公里"
}
解析說明:
date
:從“今天”推斷為 2025-03-16。activity
:識別為“跑步”。duration
:未提供分鐘數,返回null
。heart_rate
:提取為 120。notes
:記錄“跑了5公里”。
步驟 3:優化與錯誤處理
try:response = client.responses.create(model="gpt-4o-2024-08-06",input=[{"role": "system","content": f"你是一個健康管理助手,從用戶輸入中提取結構化健康數據。‘今天’指 {today},若信息缺失則返回 null。"},{"role": "user", "content": "今天早上我跑了5公里,心率達到120次/分鐘"}],text={"format": {"type": "json_schema", "name": "health_record", "schema": health_schema, "strict": True}})if response.status == "incomplete":print(f"響應不完整:{response.incomplete_details.reason}")elif response.output[0].content[0].type == "refusal":print(f"模型拒絕:{response.output[0].content[0].refusal}")else:health_record = json.loads(response.output_text)print(json.dumps(health_record, indent=2, ensure_ascii=False))except Exception as e:print(f"錯誤:{e}")
示例 2:任務管理(復雜 Schema 設計)
場景描述
構建一個任務管理助手,支持嵌套子任務和遞歸結構,用戶輸入(如“明天完成項目報告,包括收集數據和撰寫初稿”),返回任務及其子任務的結構化數據。
步驟 1:定義復雜 JSON Schema
使用遞歸結構表示任務和子任務:
{"type": "object","properties": {"task_id": {"type": "string","description": "唯一任務ID"},"title": {"type": "string","description": "任務標題"},"due_date": {"type": "string","description": "截止日期,格式 YYYY-MM-DD"},"subtasks": {"type": "array","description": "子任務列表","items": {"$ref": "#"}},"status": {"type": "string","enum": ["待辦", "進行中", "已完成"],"description": "任務狀態"}},"required": ["task_id", "title", "due_date", "subtasks", "status"],"additionalProperties": false
}
設計要點:
subtasks
使用"$ref": "#"
表示遞歸引用。task_id
確保唯一性。status
使用枚舉限制狀態。
步驟 2:實現代碼
from openai import OpenAI
import json
from datetime import datetime, timedelta
import uuidclient = OpenAI()# 定義 Schema
task_schema = {"type": "object","properties": {"task_id": {"type": "string", "description": "唯一任務ID"},"title": {"type": "string", "description": "任務標題"},"due_date": {"type": "string", "description": "截止日期,格式 YYYY-MM-DD"},"subtasks": {"type": "array", "description": "子任務列表", "items": {"$ref": "#"}},"status": {"type": "string", "enum": ["待辦", "進行中", "已完成"]}},"required": ["task_id", "title", "due_date", "subtasks", "status"],"additionalProperties": False
}# 計算明天日期
tomorrow = (datetime.now() + timedelta(days=1)).strftime("%Y-%m-%d")response = client.responses.create(model="gpt-4o-2024-08-06",input=[{"role": "system","content": f"你是一個任務管理助手,生成結構化任務數據。‘明天’指 {tomorrow},為每個任務生成唯一 task_id(如 UUID)。"},{"role": "user", "content": "明天完成項目報告,包括收集數據和撰寫初稿"}],text={"format": {"type": "json_schema","name": "task","schema": task_schema,"strict": True}}
)task = json.loads(response.output_text)
print(json.dumps(task, indent=2, ensure_ascii=False))
輸出
{"task_id": "a1b2c3d4-5678-90ef-ghij-klmn","title": "完成項目報告","due_date": "2025-03-17","subtasks": [{"task_id": "e5f6g7h8-9012-34ij-klmn-opqr","title": "收集數據","due_date": "2025-03-17","subtasks": [],"status": "待辦"},{"task_id": "i9j0k1l2-3456-78mn-opqr-stuv","title": "撰寫初稿","due_date": "2025-03-17","subtasks": [],"status": "待辦"}],"status": "待辦"
}
解析說明:
- 主任務“完成項目報告”包含兩個子任務。
- 每個任務都有唯一
task_id
(UUID)。 due_date
推斷為明天(2025-03-17)。
步驟 3:優化與調試
調試支持
若輸出不符合預期(如子任務缺失),可能原因及解決方法:
-
提示不明確:
- 問題:模型未識別“收集數據”和“撰寫初稿”為子任務。
- 解決:調整系統提示,添加“將‘包括’后的內容拆分為子任務”。
- 示例:
"將‘包括’后的內容拆分為獨立的子任務,每個子任務需有唯一 task_id 和默認狀態‘待辦’。"
-
Schema 限制:
- 問題:嵌套層級超過 5 層(Structured Outputs 限制)。
- 解決:檢查輸出,確保不超過限制,或簡化結構。
優化代碼
try:response = client.responses.create(model="gpt-4o-2024-08-06",input=[{"role": "system","content": f"你是一個任務管理助手,生成結構化任務數據。‘明天’指 {tomorrow},為每個任務生成唯一 task_id(如 UUID)。將‘包括’后的內容拆分為子任務。"},{"role": "user", "content": "明天完成項目報告,包括收集數據和撰寫初稿"}],text={"format": {"type": "json_schema", "name": "task", "schema": task_schema, "strict": True}})if response.status == "incomplete":print(f"不完整:{response.incomplete_details.reason}")elif response.output[0].content[0].type == "refusal":print(f"拒絕:{response.output[0].content[0].refusal}")else:task = json.loads(response.output_text)print(json.dumps(task, indent=2, ensure_ascii=False))except Exception as e:print(f"錯誤:{e}")
調試支持:常見問題及優化建議
-
問題:模型未填充所有字段
- 原因:輸入信息不足或提示未明確要求填充。
- 解決:在系統提示中添加默認值規則,如“若持續時間未知,返回 null”。
-
問題:輸出不符合 Schema
- 原因:Schema 定義錯誤(如漏寫
required
)。 - 解決:檢查 Schema,確保
additionalProperties: false
和所有字段在required
中。
- 原因:Schema 定義錯誤(如漏寫
-
問題:復雜嵌套導致性能下降
- 原因:遞歸結構過深或屬性過多。
- 解決:簡化 Schema,或使用 Function Calling 分擔復雜邏輯。
示例調試代碼
假設健康記錄示例中 heart_rate
未正確提取:
# 修改提示以明確要求
response = client.responses.create(model="gpt-4o-2024-08-06",input=[{"role": "system","content": f"你是一個健康管理助手,從用戶輸入中提取結構化健康數據。‘今天’指 {today},若信息缺失則返回 null。明確提取‘心率’并以數字表示。"},{"role": "user", "content": "今天早上我跑了5公里,心率達到120次/分鐘"}],text={"format": {"type": "json_schema", "name": "health_record", "schema": health_schema, "strict": True}}
)health_record = json.loads(response.output_text)
print(json.dumps(health_record, indent=2, ensure_ascii=False))