Python AI結構化輸出實戰:讓LLM穩定回傳JSON的5個核心模式
AI与大数据
AI結構化輸出:為什麼LLM的「自由文字」是生產環境的噩夢
LLM預設輸出自由文字,你讓它回傳JSON,它給你夾帶解釋文字;你要求整數,它回傳字串"42";你定義了Schema,它隨機丟欄位。結構化輸出(Structured Output)就是讓LLM嚴格按照預定義的Schema回傳資料——這是AI從「聊天玩具」走向「生產系統」的關鍵一步。2026年,OpenAI已原生支援Structured Output、Instructor函式庫實作自動重試、Outlines透過約束解碼保證100%格式合規。
本文將從5個核心模式出發,帶你完成Pydantic Schema定義→Function Calling→Instructor重試→約束解碼→多模型適配的全鏈路實戰。
核心概念
| 概念 | 說明 |
|---|---|
| Structured Output | LLM按照預定義Schema輸出結構化資料(如JSON) |
| Pydantic Model | Python資料驗證函式庫,用型別註解定義輸出Schema |
| Function Calling | OpenAI API的函式呼叫機制,約束輸出格式 |
| Instructor | 基於Pydantic的LLM結構化輸出函式庫,支援自動重試 |
| Constrained Decoding | 約束解碼,在token層級限制輸出必須符合Schema |
| JSON Schema | 描述JSON資料結構的規範,是結構化輸出的基礎 |
| Output Validation | 對LLM輸出進行驗證,確保型別和欄位完整 |
| Multi-Model Adapter | 跨不同LLM提供商統一結構化輸出的適配層 |
問題分析:LLM非結構化輸出的5類痛點
- 格式不穩定:同一個Prompt,LLM有時回傳JSON,有時回傳Markdown包裹的JSON
- 型別不可靠:要求回傳integer,LLM可能回傳字串"42"或浮點數42.0
- 欄位缺失:Schema定義了10個欄位,LLM隨機省略2-3個
- 幻覺內容:LLM在JSON值中夾帶解釋性文字,如
"price": "約100元左右" - 解析崩潰:生產環境中JSON解析失敗率高達5-15%,每次崩潰都需要人工介入
分步實操:5個AI結構化輸出核心模式
模式1:Pydantic Schema定義 + 基礎結構化輸出
pip install pydantic==2.11 openai==1.82
from pydantic import BaseModel, Field
from openai import OpenAI
import json
class ProductInfo(BaseModel):
name: str = Field(description="產品名稱")
price: float = Field(description="產品價格,單位:元")
category: str = Field(description="產品分類")
in_stock: bool = Field(description="是否有庫存")
tags: list[str] = Field(description="產品標籤列表")
client = OpenAI()
# 方式1:使用response_format參數(OpenAI原生結構化輸出)
response = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[
{"role": "system", "content": "你是產品資訊提取助手,從使用者描述中提取結構化產品資訊。"},
{"role": "user", "content": "這款MacBook Pro 14吋售價14999元,屬於筆記型電腦分類,目前有庫存,標籤是蘋果、高階、生產力工具"},
],
response_format=ProductInfo,
)
product = response.choices[0].message.parsed
print(f"產品: {product.name}")
print(f"價格: {product.price}")
print(f"分類: {product.category}")
print(f"庫存: {product.in_stock}")
print(f"標籤: {product.tags}")
print(f"型別驗證: price是{type(product.price).__name__}")
# 方式2:手動JSON Schema + 解析驗證
class OrderItem(BaseModel):
item_name: str = Field(description="商品名稱")
quantity: int = Field(gt=0, description="購買數量")
unit_price: float = Field(ge=0, description="單價")
class CustomerOrder(BaseModel):
order_id: str = Field(description="訂單編號")
customer_name: str = Field(description="客戶姓名")
items: list[OrderItem] = Field(description="訂單商品列表")
total_amount: float = Field(ge=0, description="訂單總金額")
response2 = client.chat.completions.create(
model="gpt-4o-2024-08-06",
messages=[
{"role": "system", "content": "提取訂單資訊,嚴格回傳JSON。"},
{"role": "user", "content": "訂單ORD-20260621,客戶張三,買了2個鍵盤每個299元,1個顯示器每個2999元,總計3597元"},
],
response_format={"type": "json_object"},
)
raw_json = json.loads(response2.choices[0].message.content)
order = CustomerOrder.model_validate(raw_json)
print(f"\n訂單: {order.order_id}, 客戶: {order.customer_name}")
for item in order.items:
print(f" {item.item_name} x{item.quantity} @ {item.unit_price}")
print(f"總計: {order.total_amount}")
模式2:OpenAI Function Calling結構化輸出
from pydantic import BaseModel, Field
from openai import OpenAI
import json
class WeatherQuery(BaseModel):
city: str = Field(description="城市名稱")
temperature: float = Field(description="溫度,攝氏度")
condition: str = Field(description="天氣狀況:晴/多雲/雨/雪")
humidity: int = Field(ge=0, le=100, description="濕度百分比")
wind_speed: float = Field(ge=0, description="風速,km/h")
class TravelPlan(BaseModel):
destination: str = Field(description="目的地")
days: int = Field(gt=0, description="旅行天數")
budget: float = Field(ge=0, description="預算,元")
activities: list[str] = Field(description="推薦活動列表")
weather_advice: str = Field(description="天氣相關建議")
client = OpenAI()
# 使用Function Calling強制結構化輸出
tools = [
{
"type": "function",
"function": {
"name": "get_travel_plan",
"description": "產生旅行計畫",
"parameters": TravelPlan.model_json_schema(),
},
},
{
"type": "function",
"function": {
"name": "get_weather",
"description": "取得天氣資訊",
"parameters": WeatherQuery.model_json_schema(),
},
},
]
response = client.chat.completions.create(
model="gpt-4o-2024-08-06",
messages=[
{"role": "system", "content": "你是旅行規劃助手,根據使用者需求產生旅行計畫和天氣資訊。"},
{"role": "user", "content": "我計畫去成都玩3天,預算5000元"},
],
tools=tools,
tool_choice="auto",
)
for tool_call in response.choices[0].message.tool_calls:
func_name = tool_call.function.name
func_args = json.loads(tool_call.function.arguments)
if func_name == "get_travel_plan":
plan = TravelPlan.model_validate(func_args)
print(f"目的地: {plan.destination}")
print(f"天數: {plan.days}")
print(f"預算: {plan.budget}")
print(f"活動: {', '.join(plan.activities)}")
print(f"天氣建議: {plan.weather_advice}")
elif func_name == "get_weather":
weather = WeatherQuery.model_validate(func_args)
print(f"\n{weather.city}: {weather.temperature}°C, {weather.condition}, 濕度{weather.humidity}%, 風速{weather.wind_speed}km/h")
模式3:Instructor函式庫實作自動重試結構化輸出
pip install instructor==1.7 pydantic==2.11
import instructor
from pydantic import BaseModel, Field
from openai import OpenAI
class SentimentResult(BaseModel):
text: str = Field(description="分析的文字")
sentiment: str = Field(description="情感傾向:positive/negative/neutral")
confidence: float = Field(ge=0, le=1, description="置信度0-1")
keywords: list[str] = Field(description="關鍵詞列表")
class ArticleSummary(BaseModel):
title: str = Field(description="文章標題")
summary: str = Field(min_length=50, max_length=200, description="50-200字摘要")
key_points: list[str] = Field(min_length=2, max_length=5, description="2-5個要點")
reading_time_minutes: int = Field(gt=0, description="預計閱讀時間(分鐘)")
difficulty: str = Field(description="難度:beginner/intermediate/advanced")
# 使用instructor包裝OpenAI客戶端
client = instructor.from_openai(OpenAI())
# 自動重試:如果輸出不符合Schema,instructor會自動重試
sentiment = client.chat.completions.create(
model="gpt-4o-2024-08-06",
messages=[
{"role": "system", "content": "你是情感分析專家。"},
{"role": "user", "content": "這個產品真的太好用了,介面簡潔,功能強大,強烈推薦!"},
],
response_model=SentimentResult,
max_retries=3,
)
print(f"情感: {sentiment.sentiment}")
print(f"置信度: {sentiment.confidence}")
print(f"關鍵詞: {', '.join(sentiment.keywords)}")
# 巢狀模型 + 嚴格驗證
summary = client.chat.completions.create(
model="gpt-4o-2024-08-06",
messages=[
{"role": "system", "content": "你是文章摘要產生器。"},
{"role": "user", "content": "Python 3.13在2024年10月正式發布,帶來了自由執行緒模式(PEP 703)、改進的互動式直譯器、更佳的錯誤提示等重大更新。自由執行緒模式允許Python在不使用GIL的情況下執行,這對多執行緒效能提升顯著。新的REPL支援多行編輯和語法高亮,大幅提升了開發體驗。"},
],
response_model=ArticleSummary,
max_retries=3,
)
print(f"\n標題: {summary.title}")
print(f"摘要: {summary.summary}")
for i, point in enumerate(summary.key_points, 1):
print(f" 要點{i}: {point}")
print(f"閱讀時間: {summary.reading_time_minutes}分鐘")
print(f"難度: {summary.difficulty}")
模式4:Outlines/結構化生成(約束解碼)
pip install outlines==0.1 pydantic==2.11 transformers
import outlines
from pydantic import BaseModel, Field
import json
class EntityExtraction(BaseModel):
person_names: list[str] = Field(description="人名列表")
organizations: list[str] = Field(description="組織機構列表")
locations: list[str] = Field(description="地點列表")
dates: list[str] = Field(description="日期列表")
class CodeAnalysis(BaseModel):
language: str = Field(description="程式語言")
complexity: str = Field(description="複雜度:low/medium/high")
functions: list[str] = Field(description="函式名稱列表")
imports: list[str] = Field(description="匯入模組列表")
issues: list[str] = Field(description="潛在問題列表")
# 使用Outlines進行約束解碼(本機模型)
# 注意:首次執行會下載模型,約1.5GB
model = outlines.models.transformers("microsoft/Phi-3-mini-4k-instruct")
# 方式1:基於JSON Schema的約束生成
generator = outlines.generate.json(model, EntityExtraction.model_json_schema())
text = "2026年3月15日,張三在北京參加了華為開發者大會,與李四討論了AI技術在深圳的應用。"
result = generator(text)
print(f"人名: {result['person_names']}")
print(f"組織: {result['organizations']}")
print(f"地點: {result['locations']}")
print(f"日期: {result['dates']}")
# 方式2:基於正規表示式的約束生成
# 確保輸出格式100%合規
phone_generator = outlines.generate.regex(model, r"\d{3}-\d{4}-\d{4}")
phone = phone_generator("我的電話號碼是")
print(f"\n電話號碼: {phone}")
# 方式3:基於Choice的約束生成(分類任務)
choice_generator = outlines.generate.choice(model, ["positive", "negative", "neutral"])
sentiment = choice_generator("這個產品非常好用,我非常滿意!")
print(f"情感分類: {sentiment}")
# 方式4:基於Pydantic Model的約束生成
code_generator = outlines.generate.json(model, CodeAnalysis.model_json_schema())
code_text = """
import os
import sys
from typing import List
def process_data(items: List[str]) -> dict:
result = {}
for item in items:
result[item] = len(item)
return result
def main():
data = process_data(["hello", "world"])
print(data)
"""
code_result = code_generator(code_text)
print(f"\n語言: {code_result['language']}")
print(f"複雜度: {code_result['complexity']}")
print(f"函式: {code_result['functions']}")
模式5:多模型結構化輸出適配器
from pydantic import BaseModel, Field
from openai import OpenAI
import anthropic
import google.generativeai as genai
import json
from abc import ABC, abstractmethod
from typing import TypeVar, Type
T = TypeVar("T", bound=BaseModel)
class BookRecommendation(BaseModel):
title: str = Field(description="書名")
author: str = Field(description="作者")
genre: str = Field(description="類型")
rating: float = Field(ge=0, le=5, description="評分0-5")
reason: str = Field(description="推薦理由")
class StructuredOutputAdapter(ABC):
"""多模型結構化輸出適配器基底類別"""
@abstractmethod
def generate(self, prompt: str, response_model: Type[T]) -> T:
pass
class OpenAIAdapter(StructuredOutputAdapter):
def __init__(self, api_key: str, model: str = "gpt-4o-2024-08-06"):
self.client = OpenAI(api_key=api_key)
self.model = model
def generate(self, prompt: str, response_model: Type[T]) -> T:
response = self.client.beta.chat.completions.parse(
model=self.model,
messages=[
{"role": "system", "content": f"嚴格按照Schema回傳資料。"},
{"role": "user", "content": prompt},
],
response_format=response_model,
)
return response.choices[0].message.parsed
class AnthropicAdapter(StructuredOutputAdapter):
def __init__(self, api_key: str, model: str = "claude-sonnet-4-20250514"):
self.client = anthropic.Anthropic(api_key=api_key)
self.model = model
def generate(self, prompt: str, response_model: Type[T]) -> T:
schema = response_model.model_json_schema()
response = self.client.messages.create(
model=self.model,
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
tools=[{
"name": "structured_output",
"description": "回傳結構化資料",
"input_schema": schema,
}],
tool_choice={"type": "tool", "name": "structured_output"},
)
for block in response.content:
if block.type == "tool_use":
return response_model.model_validate(block.input)
raise ValueError("No tool use in response")
class GeminiAdapter(StructuredOutputAdapter):
def __init__(self, api_key: str, model: str = "gemini-2.0-flash"):
genai.configure(api_key=api_key)
self.model_name = model
def generate(self, prompt: str, response_model: Type[T]) -> T:
model = genai.GenerativeModel(
self.model_name,
generation_config={"response_mime_type": "application/json"},
)
schema_instructions = f"回傳嚴格符合以下JSON Schema的資料:\n{json.dumps(response_model.model_json_schema(), ensure_ascii=False)}"
response = model.generate_content(f"{schema_instructions}\n\n{prompt}")
raw = json.loads(response.text)
return response_model.model_validate(raw)
# 統一呼叫介面
def get_recommendation(adapter: StructuredOutputAdapter, query: str) -> BookRecommendation:
return adapter.generate(query, BookRecommendation)
# 使用範例(需要設定對應API Key)
# openai_adapter = OpenAIAdapter(api_key="sk-xxx")
# result = get_recommendation(openai_adapter, "推薦一本關於AI的入門書籍")
# print(f"推薦: 《{result.title}》 by {result.author}, 評分{result.rating}")
# 通用驗證層:無論使用哪個模型,最終都經過Pydantic驗證
def safe_parse(raw_json: dict, model: Type[T]) -> T:
"""通用安全解析:驗證 + 預設值填充"""
try:
return model.model_validate(raw_json)
except Exception as e:
print(f"驗證失敗: {e}")
defaults = {}
for field_name, field_info in model.model_fields.items():
if field_info.is_required():
raise ValueError(f"必填欄位 {field_name} 缺失")
defaults[field_name] = field_info.default
return model.model_validate(defaults)
避坑指南
坑1:Pydantic欄位缺少description
# ❌ 錯誤:沒有description,LLM不知道該填什麼
class BadModel(BaseModel):
x1: str
x2: float
x3: list[str]
# ✅ 正確:每個欄位都加description
class GoodModel(BaseModel):
product_name: str = Field(description="產品全稱")
price: float = Field(description="價格,單位:人民幣元")
tags: list[str] = Field(description="產品標籤,如'電子產品''促銷'")
坑2:依賴LLM自動輸出JSON而不設response_format
# ❌ 錯誤:只靠Prompt要求JSON,格式不穩定
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "請回傳JSON格式的產品資訊"}],
)
# ✅ 正確:顯式設定response_format
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "請回傳JSON格式的產品資訊"}],
response_format={"type": "json_object"},
)
坑3:忽略Pydantic驗證錯誤
# ❌ 錯誤:直接json.loads不做驗證
data = json.loads(response.choices[0].message.content)
price = data["price"] # 可能是字串"42.0"而非float
# ✅ 正確:使用Pydantic驗證
from pydantic import ValidationError
try:
product = ProductInfo.model_validate_json(response.choices[0].message.content)
price = product.price # 保證是float型別
except ValidationError as e:
print(f"驗證失敗: {e}")
坑4:巢狀模型過深導致LLM輸出混亂
# ❌ 錯誤:3層以上巢狀,LLM極易出錯
class DeepNested(BaseModel):
level1: "Level1Model" # -> level2: Level2Model -> level3: Level3Model
# ✅ 正確:扁平化設計,最多2層巢狀
class OrderItem(BaseModel):
name: str = Field(description="商品名")
qty: int = Field(description="數量")
class Order(BaseModel):
items: list[OrderItem] = Field(description="商品列表") # 只巢狀1層
total: float = Field(description="總價")
坑5:Function Calling中tool_choice設定不當
# ❌ 錯誤:tool_choice="auto"時LLM可能不呼叫函式
response = client.chat.completions.create(
model="gpt-4o",
messages=[...],
tools=tools,
tool_choice="auto", # LLM可能直接回覆文字
)
# ✅ 正確:強制呼叫特定函式
response = client.chat.completions.create(
model="gpt-4o",
messages=[...],
tools=tools,
tool_choice={"type": "function", "function": {"name": "get_travel_plan"}},
)
報錯排查
| 序號 | 報錯資訊 | 原因 | 解決方法 |
|---|---|---|---|
| 1 | ValidationError: missing field |
LLM輸出缺少必填欄位 | 使用Field(default=...)或增加max_retries |
| 2 | json.decoder.JSONDecodeError |
LLM輸出不是合法JSON | 設定response_format={"type": "json_object"} |
| 3 | ValidationError: value is not a valid float |
LLM回傳字串而非數字 | Pydantic自動轉換或使用Field(coerce_numbers=True) |
| 4 | InstructorRetryError: max retries exceeded |
重試N次仍不符合Schema | 簡化Schema或最佳化Prompt描述 |
| 5 | TypeError: 'NoneType' object is not subscriptable |
LLM回傳空內容 | 檢查API Key額度和模型可用性 |
| 6 | ValidationError: list should have at least N items |
列表欄位元素不足 | 使用Field(min_length=N)並最佳化Prompt |
| 7 | StructuredOutputError: schema too complex |
Schema過於複雜 | 扁平化模型,減少巢狀層級 |
| 8 | RateLimitError |
API呼叫頻率超限 | 新增退避重試或降低並發 |
| 9 | ValidationError: string too long/short |
字串長度不符合約束 | 調整max_length/min_length或最佳化Prompt |
| 10 | ToolCallNotFoundError |
LLM未呼叫tool | 設定tool_choice強制呼叫 |
進階最佳化
- 串流結構化輸出:使用
instructor的create_partial方法實作串流解析,邊產生邊驗證 - Schema快取:對相同Schema的請求快取JSON Schema,減少重複序列化開銷
- 多Schema路由:根據使用者意圖動態選擇不同的Pydantic Model,實作靈活的結構化輸出
- 輸出後處理Pipeline:Pydantic驗證→業務規則校驗→預設值填充→日誌記錄,形成完整的資料品質保障鏈
- A/B測試Schema:對同一任務使用不同粒度的Schema,對比LLM輸出品質和Token消耗
對比分析
| 維度 | OpenAI Structured Output | Instructor | Outlines | LMQL | Guidance |
|---|---|---|---|---|---|
| 格式保證 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 自動重試 | ❌ | ⭐⭐⭐⭐⭐ | ❌ | ⭐⭐⭐ | ⭐⭐⭐ |
| 本機模型支援 | ❌ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 多模型適配 | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
| 學習曲線 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐ |
| 型別安全 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
總結:AI結構化輸出是從「聊天式AI」到「生產級AI」的必經之路。Pydantic Schema→Function Calling→Instructor重試→約束解碼→多模型適配五位一體,是2026年Python AI結構化輸出的最佳實踐。核心原則:Schema即合約、驗證即保障、重試即容錯。
線上工具推薦
- JSON格式化:/zh-TW/json/format
- Hash計算:/zh-TW/encode/hash
- cURL轉程式碼:/zh-TW/dev/curl-to-code
本站提供瀏覽器本地工具,免註冊即可試用 →
#结构化输出#AI JSON#Pydantic#Python AI#2026#AI与大数据