回到頂部

📐 Structured Output 結構化輸出

讓 AI 穩定輸出 JSON——Function Calling 和 Schema 設計完全指南。

📐 為什麼需要 Structured Output?

AI 預設回答是「自由文字」——但當你要把 AI 嵌入程式碼,你需要的是 JSON、不是散文。

💡 一句話理解 Structured Output = 讓 AI 的輸出像資料庫一樣穩定、可預測、可解析。

真實問題

❌ 直接問 AI:「分析這段客戶回饋的情感」

AI 回答:「這段文字整體偏正面,客戶對產品品質很滿意,
         但對出貨速度有些不滿...(balabala 500 字)」

→ 你的程式碼要怎麼 parse 這坨文字?正則表達式?不可能穩定。
✅ 用 Structured Output:

AI 回答:{
  "sentiment": "mixed",
  "score": 0.65,
  "positive": ["產品品質", "客服態度"],
  "negative": ["出貨速度"],
  "summary": "整體正面但物流需改善"
}

→ JSON.parse() 就搞定。穩定、可預測、可自動化。

🔧 三種實現方式

方式一覽

方式穩定度複雜度適合場景
Prompt 指定格式⭐⭐快速原型
JSON Mode⭐⭐⭐⭐簡單結構
Function Calling⭐⭐⭐⭐⭐生產環境

📝 方式 1:Prompt 指定格式

最簡單但最不穩定的方式——在 Prompt 裡告訴 AI 要輸出 JSON。

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{
        "role": "user",
        "content": """分析以下客戶回饋的情感。

回饋:「產品很棒但是等了兩週才收到」

請用以下 JSON 格式回答,不要加其他文字:
{
  "sentiment": "positive" | "negative" | "mixed",
  "score": 0.0 到 1.0,
  "positive_aspects": ["..."],
  "negative_aspects": ["..."]
}"""
    }]
)

import json
result = json.loads(response.choices[0].message.content)

⚠️ 風險:AI 可能回傳 "```json\n{...}\n```" 或加上解釋文字,導致 JSON.parse 失敗。只適合原型驗證。


🔒 方式 2:JSON Mode

各家 API 都有「保證輸出合法 JSON」的模式。

OpenAI JSON Mode

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[
        {"role": "system", "content": "你是情感分析 API。永遠用 JSON 格式回答。"},
        {"role": "user", "content": "分析:「產品很棒但等了兩週才收到」"}
    ],
    response_format={"type": "json_object"}  # 保證輸出合法 JSON
)

result = json.loads(response.choices[0].message.content)
# ✅ 保證是合法 JSON
# ⚠️ 不保證欄位名稱和型別——AI 可能回傳任意結構

OpenAI Structured Outputs(最推薦)

from pydantic import BaseModel
from typing import List

# 用 Pydantic 定義你想要的 Schema
class SentimentResult(BaseModel):
    sentiment: str        # "positive", "negative", "mixed"
    score: float          # 0.0 ~ 1.0
    positive_aspects: List[str]
    negative_aspects: List[str]
    summary: str

response = client.beta.chat.completions.parse(
    model="gpt-4o",
    messages=[
        {"role": "system", "content": "分析客戶回饋的情感。"},
        {"role": "user", "content": "產品很棒但等了兩週才收到"}
    ],
    response_format=SentimentResult  # 保證符合 Schema!
)

result = response.choices[0].message.parsed
print(result.sentiment)  # "mixed"
print(result.score)      # 0.65
# ✅ 型別安全、欄位保證存在、IDE 自動補全

Claude JSON 輸出

response = client.messages.create(
    model="claude-sonnet-4-20260514",
    max_tokens=500,
    system="你是情感分析 API。只回傳 JSON,不要其他文字。",
    messages=[{
        "role": "user",
        "content": """分析以下回饋的情感,用這個 JSON schema 回答:
{"sentiment": string, "score": number, "aspects": string[]}

回饋:「產品很棒但等了兩週」"""
    }]
)

result = json.loads(response.content[0].text)

🔧 方式 3:Function Calling(最強大)

Function Calling 讓 AI 不只是「輸出 JSON」,而是主動呼叫你定義的函式。這是建構 AI Agent 的基礎。

基本概念

你定義一組工具(functions)→ AI 判斷該用哪個工具
→ AI 輸出符合 schema 的參數 → 你的程式碼執行對應邏輯

OpenAI Function Calling

# 1. 定義工具
tools = [
    {
        "type": "function",
        "function": {
            "name": "search_products",
            "description": "依據條件搜尋商品",
            "parameters": {
                "type": "object",
                "properties": {
                    "keyword": {
                        "type": "string",
                        "description": "搜尋關鍵字"
                    },
                    "category": {
                        "type": "string",
                        "enum": ["electronics", "clothing", "food"],
                        "description": "商品類別"
                    },
                    "max_price": {
                        "type": "number",
                        "description": "最高價格(台幣)"
                    }
                },
                "required": ["keyword"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "get_order_status",
            "description": "查詢訂單狀態",
            "parameters": {
                "type": "object",
                "properties": {
                    "order_id": {
                        "type": "string",
                        "description": "訂單編號"
                    }
                },
                "required": ["order_id"]
            }
        }
    }
]

# 2. 呼叫 API,AI 會自動選擇要用的工具
response = client.chat.completions.create(
    model="gpt-4o",
    messages=[
        {"role": "user", "content": "我想找 5000 元以下的無線耳機"}
    ],
    tools=tools,
    tool_choice="auto"  # AI 自動決定是否使用工具
)

# 3. 處理 AI 的回應
message = response.choices[0].message

if message.tool_calls:
    for call in message.tool_calls:
        fn_name = call.function.name        # "search_products"
        fn_args = json.loads(call.function.arguments)
        # {"keyword": "無線耳機", "category": "electronics", "max_price": 5000}

        # 4. 執行你的實際函式
        if fn_name == "search_products":
            results = search_products(**fn_args)  # 你的搜尋邏輯

        # 5. 把結果回傳給 AI,讓它生成自然語言回覆
        follow_up = client.chat.completions.create(
            model="gpt-4o",
            messages=[
                {"role": "user", "content": "我想找 5000 元以下的無線耳機"},
                message,  # AI 的 tool_call
                {
                    "role": "tool",
                    "tool_call_id": call.id,
                    "content": json.dumps(results, ensure_ascii=False)
                }
            ]
        )
        print(follow_up.choices[0].message.content)

Claude Tool Use

response = client.messages.create(
    model="claude-sonnet-4-20260514",
    max_tokens=1000,
    tools=[{
        "name": "search_products",
        "description": "依據條件搜尋商品",
        "input_schema": {
            "type": "object",
            "properties": {
                "keyword": {"type": "string", "description": "搜尋關鍵字"},
                "max_price": {"type": "number", "description": "最高價格"}
            },
            "required": ["keyword"]
        }
    }],
    messages=[
        {"role": "user", "content": "我想找 5000 元以下的無線耳機"}
    ]
)

# Claude 的 tool use 回傳在 content block 中
for block in response.content:
    if block.type == "tool_use":
        print(f"工具: {block.name}")
        print(f"參數: {block.input}")

🏗️ 實戰:Schema 設計原則

好的 Schema 設計

# ✅ 好:欄位語意明確、有 enum 約束、有 description
{
    "type": "object",
    "properties": {
        "urgency": {
            "type": "string",
            "enum": ["low", "medium", "high", "critical"],
            "description": "問題的緊急程度"
        },
        "category": {
            "type": "string",
            "enum": ["billing", "technical", "shipping", "other"],
            "description": "問題分類"
        },
        "requires_human": {
            "type": "boolean",
            "description": "是否需要轉接真人客服"
        }
    },
    "required": ["urgency", "category", "requires_human"]
}
# ❌ 壞:欄位模糊、沒有約束、沒有描述
{
    "type": "object",
    "properties": {
        "level": {"type": "string"},
        "type": {"type": "string"},
        "flag": {"type": "boolean"}
    }
}

Schema 設計清單

  • ✅ 每個欄位都有 description
  • ✅ 可列舉的值用 enum 約束
  • ✅ 標記 required 欄位
  • ✅ 數值欄位設定合理的 minimum / maximum
  • ✅ 陣列欄位設定 maxItems 避免輸出過長
  • ✅ 巢狀結構不要超過 3 層

⚠️ 錯誤處理

即使用了 Structured Output,還是可能出問題:

def safe_parse(response, fallback=None):
    """安全的 JSON 解析,含降級策略"""
    content = response.choices[0].message.content
    try:
        return json.loads(content)
    except json.JSONDecodeError:
        # 嘗試清理(去掉 markdown code block)
        cleaned = content.strip().removeprefix("```json").removesuffix("```").strip()
        try:
            return json.loads(cleaned)
        except json.JSONDecodeError:
            print(f"⚠️ JSON 解析失敗,原始回應: {content[:200]}")
            return fallback

→ 學會 API 呼叫和結構化輸出後,下一步是理解 Embedding 向量嵌入 技術。


❓ FAQ

Function Calling 和 Structured Output 有什麼差別?

Structured Output 是「讓 AI 產出符合 schema 的 JSON」。Function Calling 是「讓 AI 決定要呼叫哪個函式、傳什麼參數」——是更上層的概念。Function Calling 底層依賴 Structured Output,但多了「工具選擇」的智慧。

JSON Mode 和 Structured Outputs 怎麼選?

JSON Mode 只保證輸出合法 JSON,不保證符合你的 schema。Structured Outputs(Pydantic model)保證每個欄位的名稱和型別都正確。生產環境永遠用 Structured Outputs。

Function Calling 和 MCP 什麼關係?

Function Calling 是各 API 各自的工具呼叫方式(OpenAI 一套、Claude 一套)。MCP 是統一標準——寫一次工具,所有支援 MCP 的 AI 都能用。MCP 是 Function Calling 的跨平台進化版。

AI 不遵守 Schema 怎麼辦?

用 OpenAI 的 Structured Outputs(Pydantic)幾乎不會發生。用 JSON Mode 或 Prompt 方式可能遇到。解法:1) 升級到 Structured Outputs 2) 加 JSON 解析 fallback 3) 重試機制。

📚 延伸閱讀