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類痛點

  1. 格式不穩定:同一個Prompt,LLM有時回傳JSON,有時回傳Markdown包裹的JSON
  2. 型別不可靠:要求回傳integer,LLM可能回傳字串"42"或浮點數42.0
  3. 欄位缺失:Schema定義了10個欄位,LLM隨機省略2-3個
  4. 幻覺內容:LLM在JSON值中夾帶解釋性文字,如"price": "約100元左右"
  5. 解析崩潰:生產環境中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強制呼叫

進階最佳化

  1. 串流結構化輸出:使用instructorcreate_partial方法實作串流解析,邊產生邊驗證
  2. Schema快取:對相同Schema的請求快取JSON Schema,減少重複序列化開銷
  3. 多Schema路由:根據使用者意圖動態選擇不同的Pydantic Model,實作靈活的結構化輸出
  4. 輸出後處理Pipeline:Pydantic驗證→業務規則校驗→預設值填充→日誌記錄,形成完整的資料品質保障鏈
  5. 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即合約、驗證即保障、重試即容錯


線上工具推薦

本站提供瀏覽器本地工具,免註冊即可試用 →

#结构化输出#AI JSON#Pydantic#Python AI#2026#AI与大数据