打造專屬 ChatGPT(二):結構化輸出、Tool Calling 與 Streaming (20260601更新)

G. 控制輸出格式的參數

前面介紹的生成控制參數,例如 temperature、top_p、max_completion_tokens,主要是在控制模型「怎麼生成」。

接下來要看的 response_format,則是控制模型「用什麼格式輸出」。

在實務應用中,這個參數非常重要。因為很多時候,我們不是只想讓模型回覆一段自然語言,而是希望它回傳可以被程式穩定解析的資料。

例如:

  • 從使用者訊息中抽取姓名、日期、金額。
  • 把文章整理成標題、摘要、關鍵字。
  • 將客服對話分類成固定類別。
  • 從收據文字中抽取商店名稱、消費日期、品項與總金額。
  • 讓模型輸出符合前端 UI 可以直接渲染的資料結構。

如果只靠 prompt 要求模型「請用 JSON 格式回答」,通常不夠穩定。模型可能會在 JSON 前後加說明文字,也可能輸出格式不一致,導致後端解析失敗。

response_format 的目的,就是讓開發者可以更明確地要求模型用指定格式回覆。

G.1 response_format 是什麼?

response_format 是 Chat Completions API 裡用來指定模型輸出格式的參數。

常見型態可以分成三種:

type說明
text預設文字輸出
json_object舊版 JSON mode,要求模型輸出 valid JSON object
json_schemaStructured Outputs,要求模型依照指定 JSON Schema 輸出

如果你沒有特別設定 response_format,通常就是一般文字輸出,也就是模型自由產生自然語言回答。

例如:

completion = client.chat.completions.create(
    model="gpt-5.5",
    messages=[
        {
            "role": "user",
            "content": "請用一句話介紹 TallyTrip。"
        }
    ],
)

print(completion.choices[0].message.content)

回傳可能是:

TallyTrip 是一個協助旅伴共同整理旅程、收據與分帳的旅行協作工具。

這種輸出很適合給使用者直接閱讀,但不一定適合程式解析。

如果你要把模型輸出接到後端流程、資料庫、API response 或前端元件,就應該考慮使用 response_format。

G.2 預設文字輸出:適合自然語言回答

不設定 response_format 時,模型通常會輸出一般文字。

這是最常見、也最直覺的使用方式。

適合情境包括:

  • 聊天機器人
  • 客服回答
  • 技術解釋
  • 文章草稿
  • 摘要說明
  • 翻譯
  • 改寫

例如:

completion = client.chat.completions.create(
    model="gpt-5.5",
    messages=[
        {
            "role": "developer",
            "content": "請使用繁體中文回答,並保持簡潔。"
        },
        {
            "role": "user",
            "content": "請解釋什麼是 Chat Completions API。"
        }
    ],
)

這種模式的優點是彈性高,模型可以自然組織語言。

缺點是輸出格式不固定。如果你後端需要穩定解析欄位,例如 title、summary、tags,就不應該只依賴自然語言輸出。

例如你要求模型:

請幫我整理成 JSON,包含 title、summary、tags。

模型可能回傳:

以下是整理後的 JSON:

{
  "title": "TallyTrip 產品介紹",
  "summary": "TallyTrip 是一個旅行協作與分帳工具。",
  "tags": ["旅遊", "分帳", "OCR"]
}

這對人類來說看得懂,但對程式來說,前面的「以下是整理後的 JSON:」可能會讓 JSON parser 失敗。

這也是為什麼在需要程式解析時,應該改用 JSON mode 或 Structured Outputs。

G.3 json_object:舊版 JSON mode

json_object 是較早期的 JSON mode。

設定方式如下:

completion = client.chat.completions.create(
    model="gpt-5.5",
    messages=[
        {
            "role": "developer",
            "content": "你必須只輸出 JSON,不要輸出任何額外說明文字。"
        },
        {
            "role": "user",
            "content": "請把 TallyTrip 整理成 JSON,包含 name、description、features。"
        }
    ],
    response_format={
        "type": "json_object"
    },
)

json_object 的重點是:它會要求模型輸出 valid JSON object。

回傳可能會像這樣:

{
  "name": "TallyTrip",
  "description": "一個協助旅伴整理旅程資料、收據與多人分帳的工具。",
  "features": [
    "旅程協作",
    "收據 OCR",
    "多人分帳",
    "多幣別結算"
  ]
}

這比單純在 prompt 裡寫「請用 JSON 回答」穩定很多。

不過,json_object 有一個重要限制:

它只保證輸出是 valid JSON,不保證 JSON 一定符合你想要的欄位結構。

也就是說,即使你要求模型輸出 name、description、features,模型仍可能輸出不同欄位名稱,例如:

{
  "product_name": "TallyTrip",
  "summary": "一個旅行協作與分帳工具。",
  "main_features": [
    "OCR",
    "Trip collaboration",
    "Expense splitting"
  ]
}

這仍然是 valid JSON,但不一定符合你的程式預期。

因此,如果你的後端只是需要「可以 parse 的 JSON」,json_object 可以使用。
但如果你需要「固定欄位、固定型別、固定結構」,就應該優先使用 json_schema。

另外,使用 json_object 時,仍建議在 developer 或 user message 裡明確告訴模型要輸出 JSON。不要只設定 response_format,卻完全不在指令中說明輸出需求。

G.4 json_schema:使用 Structured Outputs 指定結構

json_schema 是更適合正式產品使用的結構化輸出方式。

它可以讓你提供一份 JSON Schema,要求模型依照指定結構輸出資料。

例如,我們希望模型從一段產品描述中抽取:

  • name:產品名稱
  • summary:一句話摘要
  • features:功能列表
  • target_users:目標使用者

可以這樣寫:

completion = client.chat.completions.create(
    model="gpt-5.5",
    messages=[
        {
            "role": "developer",
            "content": "請根據使用者輸入抽取產品資訊,並依照指定 schema 輸出。"
        },
        {
            "role": "user",
            "content": "TallyTrip 是一個給自由行小團體使用的旅程協作工具,支援收據 OCR、多人分帳與多幣別結算。"
        }
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "product_summary",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "name": {
                        "type": "string"
                    },
                    "summary": {
                        "type": "string"
                    },
                    "features": {
                        "type": "array",
                        "items": {
                            "type": "string"
                        }
                    },
                    "target_users": {
                        "type": "string"
                    }
                },
                "required": [
                    "name",
                    "summary",
                    "features",
                    "target_users"
                ],
                "additionalProperties": False
            }
        }
    },
)

回傳內容可能會是:

{
  "name": "TallyTrip",
  "summary": "TallyTrip 是一個給自由行小團體使用的旅程協作工具。",
  "features": [
    "收據 OCR",
    "多人分帳",
    "多幣別結算",
    "旅程協作"
  ],
  "target_users": "自由行小團體與旅伴"
}

這種方式比 json_object 更適合正式後端流程,因為你不只是要求模型輸出 JSON,而是明確要求它遵守某個資料結構。

G.5 strict:要求模型嚴格遵守 schema

在 json_schema 裡,可以設定:

"strict": true

strict 的用途是要求模型更嚴格地遵守你提供的 JSON Schema。

例如:

response_format={
    "type": "json_schema",
    "json_schema": {
        "name": "expense_item",
        "strict": True,
        "schema": {
            "type": "object",
            "properties": {
                "merchant": {
                    "type": "string"
                },
                "date": {
                    "type": "string"
                },
                "currency": {
                    "type": "string"
                },
                "total": {
                    "type": "number"
                }
            },
            "required": [
                "merchant",
                "date",
                "currency",
                "total"
            ],
            "additionalProperties": False
        }
    }
}

這種設定很適合做資料抽取,例如從收據 OCR 文字中抽取消費資訊。

範例:

completion = client.chat.completions.create(
    model="gpt-5.5",
    messages=[
        {
            "role": "developer",
            "content": "你是收據資料抽取器。請根據 OCR 文字抽取消費資料。"
        },
        {
            "role": "user",
            "content": """
店名:Tokyo Coffee
日期:2026-05-20
拿鐵 600 JPY
蛋糕 800 JPY
總計 1400 JPY
"""
        }
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "receipt_expense",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "merchant": {
                        "type": "string"
                    },
                    "date": {
                        "type": "string"
                    },
                    "currency": {
                        "type": "string",
                        "enum": ["JPY", "TWD", "USD", "KRW", "EUR"]
                    },
                    "total": {
                        "type": "number"
                    },
                    "items": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "name": {
                                    "type": "string"
                                },
                                "amount": {
                                    "type": "number"
                                }
                            },
                            "required": ["name", "amount"],
                            "additionalProperties": False
                        }
                    }
                },
                "required": [
                    "merchant",
                    "date",
                    "currency",
                    "total",
                    "items"
                ],
                "additionalProperties": False
            }
        }
    },
)

print(completion.choices[0].message.content)

可能輸出:

{
  "merchant": "Tokyo Coffee",
  "date": "2026-05-20",
  "currency": "JPY",
  "total": 1400,
  "items": [
    {
      "name": "拿鐵",
      "amount": 600
    },
    {
      "name": "蛋糕",
      "amount": 800
    }
  ]
}

對產品開發來說,這種輸出就比自然語言更好處理。你可以直接把 JSON parse 後存進資料庫,或交給前端渲染。

G.6 json_object 與 json_schema 的差異

json_object 和 json_schema 都和 JSON 有關,但它們解決的問題不同。

參數設定解決問題限制
{“type”: “json_object”}確保輸出是 valid JSON不保證欄位結構符合預期
{“type”: “json_schema”, …}讓輸出符合指定 JSON Schema需要先設計 schema,且依模型支援情況使用

可以這樣判斷:

如果你只是想要模型輸出可以被 JSON parser 解析的結果,可以使用 json_object。

如果你要把模型輸出接到正式流程,例如寫入資料庫、呼叫其他 API、渲染 UI、產生報表,建議使用 json_schema。

實務上,我會這樣選:

使用情境建議
一般聊天不設定 response_format
文章摘要給人看不設定或使用文字輸出
簡單 JSON 回傳json_object
資料抽取json_schema
表單自動填寫json_schema
收據 OCR 結果整理json_schema
前端 UI 結構資料json_schema
後端工作流程輸入json_schema

G.7 使用 response_format 時的注意事項

使用 response_format 時,有幾個常見注意事項。

第一,response_format 不是萬能驗證器。

即使使用 json_schema,正式產品中仍建議在後端做 schema validation。模型輸出應該被視為外部輸入,不要在沒有驗證的情況下直接寫入資料庫或觸發高風險操作。

第二,schema 不要設計得過度複雜。

如果 schema 太深、欄位太多、條件太複雜,模型輸出失敗或品質下降的機率會提高。建議先從簡單、明確、必要的欄位開始。

第三,欄位命名要穩定。

例如不要在同一個專案裡有時候用 total,有時候用 amount_total,有時候又用 final_price。欄位名稱應該和你的資料庫、API schema 或前端資料結構盡量一致。

第四,對日期、幣別、分類這類欄位,最好明確定義格式。

例如:

{
  "date": {
    "type": "string",
    "description": "日期,格式為 YYYY-MM-DD"
  },
  "currency": {
    "type": "string",
    "enum": ["TWD", "JPY", "USD", "KRW", "EUR"]
  }
}

這樣可以降低模型輸出不一致的機率。

第五,結構化輸出不代表內容一定正確。

json_schema 可以幫你控制格式,但不能保證模型抽取的金額、日期、分類一定百分之百正確。像收據 OCR、財務資料、醫療資料、法律資料這類高風險或高精確度需求,仍然需要人工確認或額外驗證流程。

G.8 response_format 實務範例:文章摘要

假設我們想把一篇文章整理成固定格式:

  • title
  • summary
  • keywords

可以使用:

completion = client.chat.completions.create(
    model="gpt-5.5",
    messages=[
        {
            "role": "developer",
            "content": "你是文章摘要工具,請根據使用者提供的文章內容輸出結構化摘要。"
        },
        {
            "role": "user",
            "content": """
Chat Completions API 是 OpenAI 提供的對話生成介面。
開發者可以傳入 messages,並透過 temperature、tools、response_format 等參數控制模型行為。
"""
        }
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "article_summary",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "title": {
                        "type": "string"
                    },
                    "summary": {
                        "type": "string"
                    },
                    "keywords": {
                        "type": "array",
                        "items": {
                            "type": "string"
                        }
                    }
                },
                "required": [
                    "title",
                    "summary",
                    "keywords"
                ],
                "additionalProperties": False
            }
        }
    },
)

輸出可能是:

{
  "title": "Chat Completions API 參數與使用方式",
  "summary": "Chat Completions API 讓開發者可以傳入對話訊息,並使用多種參數控制模型輸出與行為。",
  "keywords": [
    "Chat Completions API",
    "messages",
    "temperature",
    "tools",
    "response_format"
  ]
}

這樣後端就可以直接將 title、summary、keywords 存入資料庫,或顯示在管理後台。

G.9 response_format 實務範例:分類任務

另一個常見用途是分類。

假設客服系統收到一則訊息,要判斷它屬於哪種問題:

completion = client.chat.completions.create(
    model="gpt-5.5",
    messages=[
        {
            "role": "developer",
            "content": "你是客服分類器,請根據使用者訊息判斷問題類型。"
        },
        {
            "role": "user",
            "content": "我昨天付款了,但是帳號還是沒有升級。"
        }
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "support_ticket_category",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "category": {
                        "type": "string",
                        "enum": [
                            "billing",
                            "account",
                            "technical",
                            "general"
                        ]
                    },
                    "priority": {
                        "type": "string",
                        "enum": [
                            "low",
                            "medium",
                            "high"
                        ]
                    },
                    "reason": {
                        "type": "string"
                    }
                },
                "required": [
                    "category",
                    "priority",
                    "reason"
                ],
                "additionalProperties": False
            }
        }
    },
)

可能輸出:

{
  "category": "billing",
  "priority": "medium",
  "reason": "使用者已付款但帳號尚未升級,屬於付款或訂閱狀態問題。"
}

這種結果可以直接接到客服系統的 ticket routing 流程,例如把 billing 類問題交給付款處理團隊。

G.10 response_format 小結

整理一下,response_format 是用來控制模型輸出格式的參數。

模式用途適合情境
預設文字自然語言回答聊天、客服、文章、摘要
json_objectvalid JSON object簡單 JSON 輸出、較舊 JSON mode
json_schema符合指定 JSON Schema資料抽取、分類、表單填寫、後端流程

實務上可以這樣選:

  • 給人看的回答:使用預設文字輸出。
  • 給程式 parse 的簡單 JSON:可以使用 json_object。
  • 給後端流程或資料庫使用的結構化資料:優先使用 json_schema。
  • 要穩定欄位與型別:使用 json_schema 並搭配 strict: true。
  • 高風險或高精確度資料:即使使用 structured output,仍然要做驗證與人工確認。

response_format 是把 LLM 從「聊天回答」推進到「可整合進產品流程」的重要參數。
如果你的應用只是讓使用者閱讀回答,可以先不設定它。
但如果你的應用需要把模型輸出交給程式處理,這個參數就非常值得掌握。

下一章會介紹工具呼叫相關參數:tools、tool_choice 與 parallel_tool_calls。這些參數會讓模型不只是產生文字,而是可以決定何時呼叫你的後端函式或外部系統。

H. 工具呼叫相關參數

前面介紹的 response_format,主要是控制模型輸出格式。

接下來要看的工具呼叫相關參數,則是讓模型不只輸出文字,而是可以在需要時「要求你的程式呼叫某個工具」。

這類能力通常被稱為 Tool Calling 或 Function Calling。

簡單來說,工具呼叫的流程是:

  1. 你先告訴模型有哪些工具可以使用。
  2. 使用者提出問題。
  3. 模型判斷是否需要呼叫工具。
  4. 如果需要,模型回傳工具名稱與參數。
  5. 你的程式實際執行工具。
  6. 你的程式把工具結果回填給模型。
  7. 模型根據工具結果產生最後回答。

工具呼叫常見用途包括:

  • 查詢資料庫。
  • 查詢訂單狀態。
  • 查詢天氣。
  • 取得使用者帳號資訊。
  • 建立行事曆事件。
  • 查詢旅程、花費或收據資料。
  • 呼叫內部 API。
  • 執行計算或格式轉換。

要注意的是:模型本身不會真的幫你查資料庫、呼叫 API 或修改資料。模型只會產生「它想呼叫哪個工具」以及「要帶哪些參數」。真正執行工具的是你的後端程式。

這也是工具呼叫最重要的安全邊界:

模型負責決定要不要呼叫工具與產生參數;你的程式負責驗證、執行、授權與錯誤處理。

H.1 tools:定義模型可以使用哪些工具

tools 用來定義這次請求中,模型可以使用哪些工具。

在 Chat Completions API 裡,最常見的工具類型是 function。

例如,假設我們有一個查詢天氣的工具 get_weather,可以這樣定義:

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "取得指定城市的天氣資訊。",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "城市名稱,例如 Taipei、Tokyo、Seoul。"
                    }
                },
                "required": ["city"],
                "additionalProperties": False
            }
        }
    }
]

這個工具定義包含幾個重點:

欄位說明
type工具類型,function calling 時通常是 function
function.name工具名稱
function.description工具用途說明,幫助模型判斷什麼時候該用
function.parameters工具參數的 JSON Schema

其中 description 很重要,因為模型會根據工具名稱、工具描述與參數 schema 判斷是否需要呼叫這個工具。

如果工具描述太模糊,模型就比較容易誤用工具。
如果工具描述太精準,模型就比較能在適合的情境使用它。

例如,這樣的描述太模糊:

"description": "取得資料。"

比較好的寫法是:

"description": "根據城市名稱取得目前天氣資訊,適合在使用者詢問某個城市天氣、溫度或降雨機率時使用。"

工具參數也應該盡量設計清楚。例如:

"parameters": {
  "type": "object",
  "properties": {
    "city": {
      "type": "string",
      "description": "城市名稱,例如 Taipei、Tokyo、Seoul。"
    },
    "unit": {
      "type": "string",
      "enum": ["celsius", "fahrenheit"],
      "description": "溫度單位。"
    }
  },
  "required": ["city"],
  "additionalProperties": false
}

這樣模型就知道 city 是必要參數,而 unit 只能是 celsius 或 fahrenheit。

H.2 使用 tools 的基本範例

定義好 tools 後,就可以把它放進 Chat Completions API 請求中。

例如:

from openai import OpenAI

client = OpenAI()

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "取得指定城市的天氣資訊。",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "城市名稱,例如 Taipei、Tokyo、Seoul。"
                    }
                },
                "required": ["city"],
                "additionalProperties": False
            }
        }
    }
]

completion = client.chat.completions.create(
    model="gpt-5.5",
    messages=[
        {
            "role": "developer",
            "content": "你是天氣助理。若需要即時天氣資訊,請使用工具。"
        },
        {
            "role": "user",
            "content": "台北今天會下雨嗎?"
        }
    ],
    tools=tools,
)

message = completion.choices[0].message

print(message)

如果模型判斷需要查天氣,它可能不會直接回答,而是回傳一個包含 tool_calls 的 assistant message。

簡化後可能像這樣:

{
  "role": "assistant",
  "content": null,
  "tool_calls": [
    {
      "id": "call_abc123",
      "type": "function",
      "function": {
        "name": "get_weather",
        "arguments": "{\"city\":\"Taipei\"}"
      }
    }
  ]
}

這裡的意思是:模型希望你的程式呼叫 get_weather 這個工具,並帶入參數:

{
  "city": "Taipei"
}

注意:這時候工具還沒有真的被執行。模型只是提出工具呼叫要求。

你的程式需要自己解析 tool_calls,執行對應函式,然後再把結果回傳給模型。

H.3 執行工具並回填 tool message

假設你的後端有一個實際的 Python 函式:

def get_weather(city: str) -> dict:
    # 實務上這裡可能會呼叫天氣 API
    return {
        "city": city,
        "forecast": "今天下午有短暫陣雨機率。",
        "temperature": 28
    }

當模型回傳 tool_calls 後,你可以解析工具名稱與參數:

import json

message = completion.choices[0].message

tool_call = message.tool_calls[0]
function_name = tool_call.function.name
arguments = json.loads(tool_call.function.arguments)

if function_name == "get_weather":
    tool_result = get_weather(**arguments)

接著要把工具結果用 tool message 回填給模型。

這一步很重要。tool message 必須帶上前面那次 tool call 的 id,也就是 tool_call_id。

messages = [
    {
        "role": "developer",
        "content": "你是天氣助理。若需要即時天氣資訊,請使用工具。"
    },
    {
        "role": "user",
        "content": "台北今天會下雨嗎?"
    },
    message,
    {
        "role": "tool",
        "tool_call_id": tool_call.id,
        "content": json.dumps(tool_result, ensure_ascii=False)
    }
]

然後再呼叫一次 Chat Completions API:

final_completion = client.chat.completions.create(
    model="gpt-5.5",
    messages=messages,
    tools=tools,
)

print(final_completion.choices[0].message.content)

模型看到工具結果後,才會產生給使用者看的自然語言回答,例如:

台北今天下午有短暫陣雨機率,氣溫約 28 度,建議出門攜帶雨具。

整個流程可以理解成:

使用者提問
   ↓
模型決定呼叫工具
   ↓
你的程式執行工具
   ↓
你的程式把結果用 tool message 回填
   ↓
模型整理成最後回答

H.4 tool_choice:控制模型是否可以呼叫工具

tool_choice 用來控制模型是否要使用工具,以及要怎麼選工具。

常見設定包括:

tool_choice說明
none不使用工具,只產生一般 assistant message
auto由模型自行判斷要不要使用工具
required模型必須呼叫一個或多個工具
指定特定工具強制模型呼叫某個指定工具
allowed_tools限制模型只能在指定工具集合中選擇

如果你有提供 tools,但沒有設定 tool_choice,通常預設會是 auto。也就是模型可以自己判斷要直接回答,還是呼叫工具。

例如:

completion = client.chat.completions.create(
    model="gpt-5.5",
    messages=messages,
    tools=tools,
    tool_choice="auto",
)

這表示模型可以自己判斷是否需要呼叫工具。

如果你希望模型完全不要使用工具,可以設定:

completion = client.chat.completions.create(
    model="gpt-5.5",
    messages=messages,
    tools=tools,
    tool_choice="none",
)

這時候即使你有提供 tools,模型也會直接產生一般文字回答,而不會呼叫工具。

如果你希望模型一定要呼叫工具,可以設定:

completion = client.chat.completions.create(
    model="gpt-5.5",
    messages=messages,
    tools=tools,
    tool_choice="required",
)

這通常適合用在資料抽取、分類、查詢類任務。
例如你希望模型一定要透過工具查訂單狀態,而不是憑空回答,就可以使用 required。

H.5 強制模型呼叫特定工具

有時候你不只希望模型使用工具,而是希望它一定要呼叫某個指定工具。

例如你有一個工具叫 get_weather,可以這樣指定:

completion = client.chat.completions.create(
    model="gpt-5.5",
    messages=messages,
    tools=tools,
    tool_choice={
        "type": "function",
        "function": {
            "name": "get_weather"
        }
    },
)

這表示模型必須呼叫 get_weather。

這種方式適合用在你已經知道下一步一定要執行某個工具的場景。

例如:

  • 使用者點了「查詢天氣」按鈕。
  • 使用者送出訂單查詢表單。
  • 後端流程已經判斷下一步必須呼叫某個工具。
  • 你希望模型只負責產生該工具的參數。

不過,強制呼叫工具要小心使用。
如果使用者的輸入其實不適合該工具,模型仍可能被迫產生參數,導致結果不自然或參數品質不佳。

實務建議:

  • 一般聊天助理:使用 auto。
  • 表單化流程:可以指定特定工具。
  • 必須查資料才能回答:可以使用 required。
  • 不希望模型碰工具:使用 none。

H.6 allowed_tools:限制模型只能使用部分工具

如果你的 tools 很多,但這次請求只想讓模型使用其中一部分,可以使用 allowed_tools 限制工具範圍。

概念上,它可以讓你定義:

  • 這次允許哪些工具。
  • 模型可以在允許工具中自動選擇,或必須選擇其中之一。

例如,假設你的系統有很多工具:

  • get_weather
  • get_time
  • search_trip
  • create_expense
  • delete_expense

但在某個查詢型情境中,你只希望模型能查詢旅程資料,不希望它建立或刪除資料,就可以限制可用工具集合。

概念上會像這樣:

completion = client.chat.completions.create(
    model="gpt-5.5",
    messages=messages,
    tools=tools,
    tool_choice={
        "type": "allowed_tools",
        "allowed_tools": {
            "mode": "auto",
            "tools": [
                {
                    "type": "function",
                    "function": {
                        "name": "search_trip"
                    }
                }
            ]
        }
    },
)

這樣可以避免模型在不該修改資料的場景中選到寫入型工具。

實務上,我會建議把工具分成幾類:

類型範例風險
查詢型工具查天氣、查訂單、查旅程低到中
建立型工具建立行程、建立花費
修改型工具更新訂單、修改旅程中到高
刪除型工具刪除資料、取消訂單
外部動作工具發信、付款、下單

對於高風險工具,不建議在所有請求中都開放給模型使用。
比較好的做法是根據使用者所在頁面、操作流程、權限與明確意圖,動態限制可用工具。

H.7 parallel_tool_calls:是否允許平行工具呼叫

parallel_tool_calls 用來控制模型是否可以在同一輪回覆中呼叫多個工具。

例如使用者問:

請幫我查台北和東京今天的天氣。

如果允許平行工具呼叫,模型可能會在同一則 assistant message 中產生兩個 tool calls:

{
  "role": "assistant",
  "content": null,
  "tool_calls": [
    {
      "id": "call_taipei",
      "type": "function",
      "function": {
        "name": "get_weather",
        "arguments": "{\"city\":\"Taipei\"}"
      }
    },
    {
      "id": "call_tokyo",
      "type": "function",
      "function": {
        "name": "get_weather",
        "arguments": "{\"city\":\"Tokyo\"}"
      }
    }
  ]
}

你的程式就可以分別執行這兩個工具,然後把結果都回填給模型。

概念上會像這樣:

tool_messages = []

for tool_call in message.tool_calls:
    function_name = tool_call.function.name
    arguments = json.loads(tool_call.function.arguments)

    if function_name == "get_weather":
        result = get_weather(**arguments)

        tool_messages.append({
            "role": "tool",
            "tool_call_id": tool_call.id,
            "content": json.dumps(result, ensure_ascii=False)
        })

然後把所有 tool messages 加回 messages:

messages = [
    {
        "role": "developer",
        "content": "你是天氣助理。"
    },
    {
        "role": "user",
        "content": "請幫我查台北和東京今天的天氣。"
    },
    message,
    *tool_messages
]

如果你希望模型一次最多只呼叫一個工具,可以設定:

completion = client.chat.completions.create(
    model="gpt-5.5",
    messages=messages,
    tools=tools,
    parallel_tool_calls=False,
)

這樣可以讓工具流程更容易控制,尤其是當工具之間有依賴關係時。

例如:

  1. 先建立旅程。
  2. 再把花費加到該旅程。
  3. 再產生結算結果。

這種流程不適合讓模型一次平行呼叫三個工具,因為後面的工具可能依賴前面工具產生的 ID。

實務建議:

使用情境parallel_tool_calls
查多個獨立資料可以允許
查多個城市天氣可以允許
多個工具互不依賴可以允許
建立、更新、刪除資料建議關閉或嚴格控制
工具之間有順序依賴建議關閉
高風險外部動作建議關閉

H.8 工具參數只是模型產生的輸出,仍然要驗證

工具呼叫最容易讓人誤解的地方是:看到模型產生了符合 schema 的 arguments,就以為可以直接執行。

正式產品中不應該這樣做。

模型產生的工具參數仍然應該被視為外部輸入。你需要在後端做驗證,例如:

  • 使用者是否有權限執行這個工具。
  • 工具參數是否符合 schema。
  • 參數中的 ID 是否屬於目前使用者。
  • 金額、日期、幣別是否合理。
  • 是否需要使用者二次確認。
  • 是否涉及刪除、付款、發信、下單等高風險操作。

例如模型產生:

{
  "trip_id": "trip_123",
  "amount": 5000,
  "currency": "JPY"
}

你的後端仍然要檢查:

  • trip_123 是否存在。
  • 目前使用者是否有權限存取這個 trip。
  • amount 是否為合理數值。
  • currency 是否是系統支援的幣別。
  • 這次操作是否需要使用者確認。

工具呼叫不是讓模型繞過後端邏輯。
相反地,工具呼叫應該接在你原本就設計好的後端 API、權限系統與 validation 流程上。

H.9 Tool Calling 實務範例:查詢旅程花費

假設我們在 TallyTrip 裡做一個 AI 助理,使用者可以問:

這趟東京旅行目前總共花了多少日圓?

我們可以定義一個查詢旅程花費的工具:

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_trip_expense_summary",
            "description": "查詢指定旅程的花費總結,包含總金額、幣別與每位成員的分攤資訊。",
            "parameters": {
                "type": "object",
                "properties": {
                    "trip_id": {
                        "type": "string",
                        "description": "旅程 ID。"
                    },
                    "currency": {
                        "type": "string",
                        "description": "希望統計的幣別。",
                        "enum": ["TWD", "JPY", "USD", "KRW", "EUR"]
                    }
                },
                "required": ["trip_id", "currency"],
                "additionalProperties": False
            }
        }
    }
]

然後呼叫 Chat Completions API:

completion = client.chat.completions.create(
    model="gpt-5.5",
    messages=[
        {
            "role": "developer",
            "content": "你是 TallyTrip 的旅程助理。需要查詢旅程花費時,請使用工具。"
        },
        {
            "role": "user",
            "content": "這趟東京旅行目前總共花了多少日圓?trip_id=tokyo_2026"
        }
    ],
    tools=tools,
    tool_choice="auto",
)

模型可能會產生:

{
  "role": "assistant",
  "content": null,
  "tool_calls": [
    {
      "id": "call_expense_summary",
      "type": "function",
      "function": {
        "name": "get_trip_expense_summary",
        "arguments": "{\"trip_id\":\"tokyo_2026\",\"currency\":\"JPY\"}"
      }
    }
  ]
}

你的後端執行工具後,取得結果:

tool_result = {
    "trip_id": "tokyo_2026",
    "currency": "JPY",
    "total": 128000,
    "members": [
        {
            "name": "Alfred",
            "paid": 60000,
            "share": 42667
        },
        {
            "name": "Ben",
            "paid": 38000,
            "share": 42667
        },
        {
            "name": "Cindy",
            "paid": 30000,
            "share": 42666
        }
    ]
}

再把結果回填給模型:

messages = [
    {
        "role": "developer",
        "content": "你是 TallyTrip 的旅程助理。請根據工具結果回答。"
    },
    {
        "role": "user",
        "content": "這趟東京旅行目前總共花了多少日圓?trip_id=tokyo_2026"
    },
    completion.choices[0].message,
    {
        "role": "tool",
        "tool_call_id": "call_expense_summary",
        "content": json.dumps(tool_result, ensure_ascii=False)
    }
]

final_completion = client.chat.completions.create(
    model="gpt-5.5",
    messages=messages,
    tools=tools,
)

print(final_completion.choices[0].message.content)

最後模型可能回答:

這趟東京旅行目前總花費是 128,000 JPY。平均分攤後,Alfred 已多付 17,333 JPY,Ben 目前少付 4,667 JPY,Cindy 目前少付 12,666 JPY。

這個例子可以看出 Tool Calling 的價值:模型不是自己猜答案,而是透過你的後端工具取得真實資料,再把結果整理成使用者容易理解的回答。

H.10 functions 與 function_call:舊版相容性參數

在較早期的 Chat Completions API 中,工具呼叫主要使用:

  • functions
  • function_call
  • function role

後來這套用法被新的 tools 架構取代。新專案應該優先使用:

  • tools
  • tool_choice
  • tool role

可以這樣理解:

舊用法新用法
functionstools
function_calltool_choice
function roletool role
function resulttool message with tool_call_id

舊寫法可能長這樣:

completion = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=messages,
    functions=[
        {
            "name": "get_weather",
            "description": "取得指定城市的天氣資訊。",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string"
                    }
                },
                "required": ["city"]
            }
        }
    ],
    function_call="auto",
)

如果你是在維護舊專案,可能仍然會看到這種寫法。
但如果你正在寫新文章或新功能,建議以 tools 和 tool_choice 為主,把 functions 和 function_call 放在 legacy 或相容性段落即可。

H.11 Tool Calling 的常見錯誤

工具呼叫很強大,但也很容易踩雷。

錯誤一:以為模型會真的執行工具

模型不會真的執行工具。它只會產生工具呼叫請求。
真正執行的是你的程式。

錯誤二:沒有把 tool result 回填給模型

如果模型要求呼叫工具,你的程式執行完工具後,必須用 tool message 把結果回填。
否則模型不會知道工具執行結果。

錯誤三:沒有對 arguments 做驗證

工具參數是模型產生的輸出,不應該直接信任。
正式產品一定要做 schema validation、權限檢查與商業邏輯驗證。

錯誤四:工具 description 寫得太模糊

工具描述太模糊,模型就容易在不適合的情境呼叫工具,或該呼叫時沒有呼叫。

錯誤五:一次開放太多高風險工具

刪除資料、付款、發信、下單這類工具,不應該在沒有確認流程的情況下直接開放給模型任意呼叫。

錯誤六:沒有處理工具錯誤

工具可能失敗,例如 API timeout、資料不存在、權限不足、參數錯誤。
你應該把錯誤結果回傳給模型,或由後端直接處理錯誤訊息。

例如:

{
  "error": true,
  "message": "找不到指定旅程,或使用者沒有權限存取。"
}

再讓模型根據這個錯誤結果整理成使用者能理解的回答。

H.12 工具呼叫參數小結

整理一下,工具呼叫相關參數可以這樣理解:

參數用途
tools定義模型可以使用哪些工具
tool_choice控制模型是否使用工具、是否必須使用工具、或指定使用哪個工具
parallel_tool_calls控制模型是否可以在同一輪中呼叫多個工具
functions舊版工具定義方式,新專案不建議優先使用
function_call舊版工具選擇方式,新專案不建議優先使用

實務上可以這樣選:

  • 一般 AI 助理:tools + tool_choice=”auto”
  • 必須查資料才能回答:tool_choice=”required”
  • 已知下一步要呼叫某工具:指定特定 tool
  • 高風險操作:限制 allowed tools,並加入使用者確認流程
  • 工具之間有順序依賴:考慮關閉 parallel_tool_calls
  • 新專案:使用 tools / tool_choice / tool message
  • 舊專案維護:理解 functions / function_call / function role

Tool Calling 是把 Chat Completions API 從「文字生成」推進到「可操作系統」的關鍵能力。
但它不是讓模型直接控制你的後端,而是讓模型提出工具呼叫意圖,再由你的程式負責執行、驗證與控管。

下一章會介紹串流回應相關參數:stream、stream_options 與 chat.completion.chunk。這些參數會影響模型回覆如何被分段傳回,以及聊天 UI 如何即時顯示文字。

I. 串流回應相關參數

前面介紹的範例,大多是等模型產生完整回答後,再一次取得結果。

例如:

completion = client.chat.completions.create(
    model="gpt-5.5",
    messages=[
        {
            "role": "user",
            "content": "請用三句話介紹 Chat Completions API。"
        }
    ],
)

print(completion.choices[0].message.content)

這種方式很適合後端任務、資料抽取、摘要、分類等情境,因為程式可以一次拿到完整的 Chat Completion object。

但如果你正在做聊天 UI,使用者通常會期待文字像 ChatGPT 一樣逐步出現,而不是等整段回答完成後才顯示。

這時候就可以使用串流回應,也就是 streaming。

串流回應相關參數主要包括:

  • stream
  • stream_options
  • stream_options.include_usage

而串流模式下回傳的資料物件,則是:

  • chat.completion.chunk

I.1 stream:啟用串流回應

stream 用來控制是否啟用串流回應。

型別:

boolean or null

預設情況下,如果沒有設定 stream,API 會在模型完成整段回答後,一次回傳完整結果。

如果設定:

stream=True

API 就會把模型產生的內容分成多個 chunk,逐步傳回來。

範例:

from openai import OpenAI

client = OpenAI()

stream = client.chat.completions.create(
    model="gpt-5.5",
    messages=[
        {
            "role": "user",
            "content": "請用三句話介紹 Chat Completions API。"
        }
    ],
    stream=True,
)

for chunk in stream:
    if chunk.choices and chunk.choices[0].delta.content:
        print(chunk.choices[0].delta.content, end="")

這段程式和非串流版本最大的差異是:

stream=True

以及回傳結果的處理方式。

非串流模式下,你會拿到完整的 completion:

completion.choices[0].message.content

串流模式下,你會一段一段收到 chunk:

chunk.choices[0].delta.content

因此,串流模式不是讀 message.content,而是讀每個 chunk 裡的 delta.content。

I.2 非串流與串流的差異

可以先用一張表理解:

模式設定回傳物件常見讀取欄位適合情境
非串流stream=False 或不設定chat.completioncompletion.choices[0].message.content後端任務、摘要、分類、資料抽取
串流stream=Truechat.completion.chunkchunk.choices[0].delta.content聊天 UI、長回答、即時文字顯示

非串流模式比較容易處理,因為一次就拿到完整內容。
串流模式則比較適合使用者體驗,因為可以提早顯示部分文字,降低等待感。

例如使用者問:

請寫一篇 800 字文章。

如果使用非串流模式,前端可能要等模型產生完整文章後才看到內容。
如果使用串流模式,前端可以邊收到 chunk 邊渲染文字,使用者會感覺回應更即時。

I.3 chat.completion.chunk:串流回傳的資料格式

啟用 stream=True 後,每次收到的不是完整的 Chat Completion object,而是一個 Chat Completion Chunk object。

簡化後的 chunk 可能像這樣:

{
  "id": "chatcmpl_xxx",
  "object": "chat.completion.chunk",
  "created": 1710000000,
  "model": "gpt-5.5",
  "choices": [
    {
      "index": 0,
      "delta": {
        "role": "assistant",
        "content": ""
      },
      "finish_reason": null
    }
  ],
  "usage": null
}

後續 chunk 可能只包含新增的文字片段:

{
  "id": "chatcmpl_xxx",
  "object": "chat.completion.chunk",
  "created": 1710000000,
  "model": "gpt-5.5",
  "choices": [
    {
      "index": 0,
      "delta": {
        "content": "Chat"
      },
      "finish_reason": null
    }
  ],
  "usage": null
}

下一個 chunk 可能是:

{
  "id": "chatcmpl_xxx",
  "object": "chat.completion.chunk",
  "created": 1710000000,
  "model": "gpt-5.5",
  "choices": [
    {
      "index": 0,
      "delta": {
        "content": " Completions"
      },
      "finish_reason": null
    }
  ],
  "usage": null
}

直到最後,可能會收到 finish_reason:

{
  "id": "chatcmpl_xxx",
  "object": "chat.completion.chunk",
  "created": 1710000000,
  "model": "gpt-5.5",
  "choices": [
    {
      "index": 0,
      "delta": {},
      "finish_reason": "stop"
    }
  ],
  "usage": null
}

這裡要注意:

  • delta 表示這個 chunk 新增的內容。
  • delta.content 不一定每個 chunk 都有。
  • finish_reason 通常在最後才會出現。
  • usage 預設可能是 null 或不出現在一般內容 chunk 中,取決於是否設定 stream_options.include_usage。

因此處理 streaming 時,不應該假設每個 chunk 都一定有文字。

比較安全的寫法是:

for chunk in stream:
    if not chunk.choices:
        continue

    delta = chunk.choices[0].delta

    if delta.content:
        print(delta.content, end="")

這樣可以避免遇到沒有 choices、沒有 delta.content 或最後 usage chunk 時出錯。

I.4 把 streaming chunk 組成完整文字

串流模式下,模型回覆會被拆成多個 delta.content。

如果你想在後端取得完整文字,可以自己累積:

from openai import OpenAI

client = OpenAI()

stream = client.chat.completions.create(
    model="gpt-5.5",
    messages=[
        {
            "role": "user",
            "content": "請用三句話介紹 Chat Completions API。"
        }
    ],
    stream=True,
)

parts = []

for chunk in stream:
    if not chunk.choices:
        continue

    delta = chunk.choices[0].delta

    if delta.content:
        parts.append(delta.content)
        print(delta.content, end="")

full_text = "".join(parts)

print("\n\n完整回覆:")
print(full_text)

這個範例同時做兩件事:

  • 即時把文字印出來。
  • 把所有片段累積成 full_text。

在前端聊天 UI 中,也會做類似的事:每收到一個新的 chunk,就把文字追加到目前訊息中。

概念上可以想成:

目前畫面文字 = 目前畫面文字 + 新收到的 delta.content

I.5 stream_options.include_usage:串流時取得 token 使用量

非串流模式下,完整回傳結果通常會包含 usage,可以用來查看 token 使用量。

例如:

completion.usage

但串流模式下,因為內容是分段傳回的,所以 token usage 不會像非串流模式那樣直接在一開始就拿到完整值。

如果希望在 streaming 結束時取得 usage,可以設定:

stream_options={
    "include_usage": True
}

完整範例:

from openai import OpenAI

client = OpenAI()

stream = client.chat.completions.create(
    model="gpt-5.5",
    messages=[
        {
            "role": "user",
            "content": "請用三句話介紹 Chat Completions API。"
        }
    ],
    stream=True,
    stream_options={
        "include_usage": True
    },
)

parts = []
usage = None

for chunk in stream:
    if chunk.usage:
        usage = chunk.usage
        continue

    if not chunk.choices:
        continue

    delta = chunk.choices[0].delta

    if delta.content:
        parts.append(delta.content)
        print(delta.content, end="")

full_text = "".join(parts)

print("\n\nToken usage:")
print(usage)

設定 include_usage=True 後,串流過程中的每個內容 chunk 通常仍然不會有完整 usage。
完整 usage 會在額外的 final chunk 中回傳。

這個 final usage chunk 有幾個特徵:

  • usage 會包含整次請求的 token 使用量。
  • choices 通常會是空陣列。
  • 如果串流被中斷,可能收不到這個 final usage chunk。

因此處理程式要能接受:

if chunk.usage:
    usage = chunk.usage

也要能處理:

if not chunk.choices:
    continue

不要假設每個 chunk 都一定有 choices[0]。

I.6 使用 stream_options.include_usage 的注意事項

stream_options.include_usage 很適合用來做成本追蹤與紀錄。

例如你可以在請求結束後記錄:

  • prompt tokens
  • completion tokens
  • total tokens
  • 使用模型
  • 使用者 ID
  • 功能名稱
  • 請求時間

這對產品營運很有用,因為你可以知道:

  • 哪些功能最耗 token。
  • 哪些使用者用量最高。
  • 哪些 prompt 成本過高。
  • 哪些模型在特定任務上成本效益較好。

不過要注意幾點。

第一,如果 streaming 連線中斷,可能收不到最後 usage chunk。

例如使用者關閉頁面、網路斷線、伺服器中止連線,都可能導致你拿不到最後的 usage。因此正式產品中,如果 usage 很重要,可以搭配其他紀錄方式,例如後端日誌、請求估算或平台用量資料。

第二,不同 OpenAI 相容 API、代理服務或雲端供應商,不一定完整支援 stream_options.include_usage。

如果你不是直接使用 OpenAI 官方 API,而是透過 Azure OpenAI、第三方 gateway、proxy 或 self-hosted OpenAI-compatible API,應該先確認該服務是否支援這個參數。

第三,不要把 usage chunk 當成一般內容 chunk 處理。

因為 final usage chunk 的 choices 可能是空陣列,如果程式直接寫:

chunk.choices[0].delta.content

就可能在最後一個 chunk 出錯。

比較安全的寫法是:

for chunk in stream:
    if chunk.usage:
        usage = chunk.usage
        continue

    if not chunk.choices:
        continue

    content = chunk.choices[0].delta.content
    if content:
        print(content, end="")

I.7 streaming 與 finish_reason

在非串流模式下,finish_reason 會出現在完整 response object 的 choices 裡。

例如:

{
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "..."
      },
      "finish_reason": "stop"
    }
  ]
}

在串流模式下,finish_reason 通常不會在一開始就出現,而是在最後幾個 chunk 中出現。

常見的 finish_reason 包括:

finish_reason說明
stop模型自然停止,或遇到 stop sequence
length達到 token 上限而停止
tool_calls模型產生工具呼叫
content_filter因內容過濾而停止
function_call舊版 function calling,相容性用法

處理 streaming 時,可以這樣記錄最後的 finish_reason:

finish_reason = None

for chunk in stream:
    if chunk.usage:
        usage = chunk.usage
        continue

    if not chunk.choices:
        continue

    choice = chunk.choices[0]

    if choice.delta.content:
        print(choice.delta.content, end="")

    if choice.finish_reason:
        finish_reason = choice.finish_reason

print("finish_reason:", finish_reason)

這在除錯時很有用。

例如:

  • 如果 finish_reason=”stop”,通常代表正常結束。
  • 如果 finish_reason=”length”,代表回答被 token 上限截斷,可能需要提高 max_completion_tokens。
  • 如果 finish_reason=”tool_calls”,代表模型這輪產生了工具呼叫,你需要接著執行工具流程。

I.8 streaming 與 tool_calls

當 stream=True 並且使用 Tool Calling 時,模型產生的 tool_calls 也可能被分段回傳。

也就是說,工具名稱、arguments JSON 字串,不一定會一次完整出現在同一個 chunk 裡。

概念上,模型可能分幾段輸出:

{"city"

下一段:

:"Tai"

下一段:

pei"}

因此,如果你要在 streaming 模式下處理 tool calls,不能只讀單一 chunk,而是需要把 tool call arguments 累積起來,等工具呼叫完整後再解析 JSON。

簡化概念如下:

tool_call_arguments = ""

for chunk in stream:
    if not chunk.choices:
        continue

    delta = chunk.choices[0].delta

    if delta.tool_calls:
        for tool_call_delta in delta.tool_calls:
            if tool_call_delta.function and tool_call_delta.function.arguments:
                tool_call_arguments += tool_call_delta.function.arguments

# 等完整後再 json.loads(tool_call_arguments)

實務上,streaming tool calls 的處理會比一般文字 streaming 複雜。你需要追蹤:

  • tool call index
  • tool call id
  • function name
  • function arguments 的片段
  • 何時 finish_reason 變成 tool_calls

如果你只是剛開始使用 Tool Calling,建議先用非串流模式把流程做穩,再考慮加入 streaming。
如果你的產品需要同時支援 streaming UI 和工具呼叫,就要特別設計好 tool call chunk 的累積邏輯。

I.9 前端聊天 UI 的基本處理方式

在實務產品中,串流通常不是直接印在後端 console,而是透過 HTTP streaming、Server-Sent Events、WebSocket 或框架提供的 streaming response 傳給前端。

前端收到片段後,會把文字追加到目前 assistant message。

概念上像這樣:

let assistantMessage = "";

for await (const chunk of stream) {
  const content = chunk.choices?.[0]?.delta?.content;

  if (content) {
    assistantMessage += content;
    renderAssistantMessage(assistantMessage);
  }
}

在 UI 上,你通常會需要處理:

  • loading 狀態
  • 使用者取消生成
  • 重新產生回答
  • 串流中斷
  • 錯誤訊息
  • partial message 是否保存
  • token usage 是否成功回傳

例如使用者按下「停止生成」時,前端可能會中止連線。
這時候後端不一定能拿到 final usage chunk,也不一定能取得完整回答。因此你需要決定:要不要保存 partial message?要不要標記這則訊息為 incomplete?要不要允許使用者接續生成?

這些都是聊天產品實作時需要考慮的細節。

I.10 streaming 的實務建議

串流回應很適合提升使用者體驗,但不一定每個功能都需要。

可以這樣判斷:

使用情境是否建議 streaming
聊天 UI建議
長文章生成建議
即時客服回答建議
後台批次摘要不一定需要
結構化資料抽取通常不需要
分類任務通常不需要
Tool Calling 初期開發建議先不用 streaming
需要完整 JSON 後再處理通常不需要

如果你的輸出是給人看的,而且可能比較長,streaming 很有價值。
如果你的輸出是給程式 parse 的,例如 JSON、分類結果、資料抽取結果,通常一次拿完整 response 會更簡單。

I.11 串流回應小結

整理一下:

參數或物件用途
stream是否啟用串流回應
stream=True讓模型輸出分段回傳
stream_options.include_usage在串流結束時回傳 token usage
chat.completion.chunk串流模式下每次收到的資料物件
delta.content本次 chunk 新增的文字
finish_reason模型停止原因,通常在最後幾個 chunk 出現
usagetoken 使用量,啟用 include_usage 後通常在 final chunk 出現

非串流模式下,最常讀:

completion.choices[0].message.content

串流模式下,最常讀:

chunk.choices[0].delta.content

如果要取得 streaming token usage,可以設定:

stream_options={
    "include_usage": True
}

並且在處理 chunk 時注意:

  • 不是每個 chunk 都有 delta.content。
  • final usage chunk 的 choices 可能是空陣列。
  • 串流中斷時,可能收不到 final usage。
  • Tool Calling 在 streaming 模式下需要累積 tool call delta。

串流回應的核心價值,是讓使用者不用等完整回答完成,就能開始看到模型輸出。
對聊天產品來說,這會明顯改善體驗;對後端資料處理來說,則要看是否真的需要即時顯示。

下一章會介紹多模態與進階輸入輸出參數,例如 modalities、audio、prediction、image input 與 file input。

J. 多模態與進階輸入輸出

前面幾章的範例大多使用純文字輸入與純文字輸出。

例如:

completion = client.chat.completions.create(
    model="gpt-5.5",
    messages=[
        {
            "role": "user",
            "content": "請用一句話介紹 Chat Completions API。"
        }
    ],
)

這種用法最直覺,也足以涵蓋大部分聊天、摘要、分類、改寫與資料抽取任務。

但 Chat Completions API 不一定只能處理純文字。依照模型支援能力不同,messages 裡的 content 也可以使用 content parts 來表示更複雜的輸入,例如:

  • 文字
  • 圖片
  • 音訊
  • 檔案

此外,部分模型也可以產生不只文字的輸出,例如音訊輸出。這時候就會用到:

  • modalities
  • audio

這一章會整理 Chat Completions API 裡和多模態輸入、音訊輸出,以及進階輸入輸出有關的用法。

要先提醒一點:多模態能力高度依賴模型支援。不是每個模型都支援圖片輸入、音訊輸入、檔案輸入或音訊輸出。因此實作前應該先確認你使用的模型是否支援對應功能。

J.1 content 可以是字串,也可以是 content parts

在最基本的文字聊天中,content 通常是一個字串:

{
  "role": "user",
  "content": "請用一句話解釋 Chat Completions API。"
}

這是最簡單的形式。

但在多模態情境中,content 可以改成一個陣列,陣列中的每個項目都是一個 content part。

例如,純文字也可以用 content part 表示:

{
  "role": "user",
  "content": [
    {
      "type": "text",
      "text": "請用一句話解釋 Chat Completions API。"
    }
  ]
}

這種寫法看起來比字串複雜,但它的好處是可以在同一則 message 中混合不同類型的內容。

例如:

{
  "role": "user",
  "content": [
    {
      "type": "text",
      "text": "請描述這張圖片中的收據內容。"
    },
    {
      "type": "image_url",
      "image_url": {
        "url": "https://example.com/receipt.jpg"
      }
    }
  ]
}

這裡同一則 user message 裡同時包含文字指令和圖片。

可以把 content parts 想成:

不是只傳一段文字,而是傳一組有類型的輸入片段。

常見 content part 包括:

type用途
text文字輸入
image_url圖片輸入
input_audio音訊輸入
file檔案輸入

不同模型支援的 content part 類型可能不同,所以不要假設所有模型都能吃所有格式。

J.2 文字 content part:用 type=text 表示文字

最基本的 content part 是 text。

格式如下:

{
  "type": "text",
  "text": "請分析以下內容。"
}

完整 message 範例:

completion = client.chat.completions.create(
    model="gpt-5.5",
    messages=[
        {
            "role": "user",
            "content": [
                {
                    "type": "text",
                    "text": "請用一句話解釋 Chat Completions API。"
                }
            ]
        }
    ],
)

如果你的輸入只有純文字,其實不一定要使用 content parts。直接使用字串更簡單:

messages=[
    {
        "role": "user",
        "content": "請用一句話解釋 Chat Completions API。"
    }
]

我會建議:

  • 純文字聊天:直接用字串。
  • 多模態輸入:使用 content parts。
  • 同一則 message 要混合文字、圖片、音訊或檔案:使用 content parts。

J.3 圖片輸入:image_url content part

如果模型支援圖片理解,可以在 user message 中加入 image_url content part。

基本格式如下:

{
  "type": "image_url",
  "image_url": {
    "url": "https://example.com/image.jpg"
  }
}

完整範例:

completion = client.chat.completions.create(
    model="gpt-5.5",
    messages=[
        {
            "role": "user",
            "content": [
                {
                    "type": "text",
                    "text": "請描述這張圖片。"
                },
                {
                    "type": "image_url",
                    "image_url": {
                        "url": "https://example.com/photo.jpg"
                    }
                }
            ]
        }
    ],
)

print(completion.choices[0].message.content)

這種方式適合:

  • 圖片描述
  • 截圖分析
  • UI 畫面理解
  • 商品圖片分析
  • 收據或票券圖片初步理解
  • 圖表、表格、文件截圖說明

例如在 TallyTrip 這類旅行工具裡,圖片輸入可能可以用在:

  • 分析收據照片
  • 辨識票券資訊
  • 描述旅程照片
  • 協助整理旅遊素材
  • 從截圖中理解訂位或交通資訊

圖片也可以使用 base64 encoded image data,而不一定只能使用公開 URL。概念上會像這樣:

{
  "type": "image_url",
  "image_url": {
    "url": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ..."
  }
}

實務上,如果圖片是使用者上傳到你的系統,通常會有兩種做法:

  1. 產生模型可讀取的圖片 URL。
  2. 將圖片轉成 base64 data URL 後傳入。

選哪一種方式,要看你的檔案儲存、權限控管、圖片大小與安全需求。

J.4 image_url.detail:控制圖片理解細節

image_url 可以包含 detail 欄位,用來指定圖片處理的細節程度。

常見值包括:

auto
low
high

範例:

completion = client.chat.completions.create(
    model="gpt-5.5",
    messages=[
        {
            "role": "user",
            "content": [
                {
                    "type": "text",
                    "text": "請讀取這張收據中的店名、日期與總金額。"
                },
                {
                    "type": "image_url",
                    "image_url": {
                        "url": "https://example.com/receipt.jpg",
                        "detail": "high"
                    }
                }
            ]
        }
    ],
)

可以簡單理解成:

detail說明
auto由模型或系統自動判斷
low較低細節,通常速度與成本較低
high較高細節,適合需要看清楚圖片內容的任務

實務上:

  • 一般圖片描述:可以使用 auto。
  • 大略判斷圖片內容:可以使用 low。
  • 收據、票券、截圖、表格、細小文字:可以考慮 high。

不過,圖片理解不是 OCR 專用引擎。若你的任務是高精度收據辨識、發票欄位抽取或正式財務資料處理,仍建議搭配專門 OCR 流程、後端驗證與人工確認。

J.5 音訊輸入:input_audio content part

如果模型支援音訊輸入,可以在 user message 中使用 input_audio content part。

基本格式如下:

{
  "type": "input_audio",
  "input_audio": {
    "data": "base64_encoded_audio_data",
    "format": "mp3"
  }
}

其中:

欄位說明
database64 encoded audio data
format音訊格式,例如 wav 或 mp3

完整範例概念如下:

completion = client.chat.completions.create(
    model="gpt-5.5",
    messages=[
        {
            "role": "user",
            "content": [
                {
                    "type": "text",
                    "text": "請將這段語音整理成三個重點。"
                },
                {
                    "type": "input_audio",
                    "input_audio": {
                        "data": "base64_encoded_audio_data",
                        "format": "mp3"
                    }
                }
            ]
        }
    ],
)

音訊輸入適合:

  • 語音摘要
  • 會議重點整理
  • 語音客服分析
  • 語音備忘錄整理
  • 口說內容轉成結構化資料

不過,如果你的目標只是「語音轉文字」,也可以考慮使用專門的 transcription API。
如果你的目標是「理解音訊內容並回答問題」,才比較適合直接使用支援 audio input 的聊天模型。

例如:

任務較適合方式
把音訊轉成逐字稿Speech-to-text / transcription
根據音訊回答問題支援 audio input 的聊天模型
將語音內容整理成摘要兩者都可能適合,視需求而定
即時語音互動Realtime API 可能更適合

J.6 檔案輸入:file content part

除了文字、圖片、音訊,Chat Completions API 的 content parts 也可以包含檔案。

基本格式如下:

{
  "type": "file",
  "file": {
    "file_id": "file_abc123"
  }
}

或使用 base64 encoded file data:

{
  "type": "file",
  "file": {
    "filename": "document.pdf",
    "file_data": "base64_encoded_file_data"
  }
}

常見欄位包括:

欄位說明
file_id已上傳檔案的 ID
file_database64 encoded file data
filename檔案名稱

完整範例概念:

completion = client.chat.completions.create(
    model="gpt-5.5",
    messages=[
        {
            "role": "user",
            "content": [
                {
                    "type": "text",
                    "text": "請整理這份文件的重點。"
                },
                {
                    "type": "file",
                    "file": {
                        "file_id": "file_abc123"
                    }
                }
            ]
        }
    ],
)

檔案輸入適合:

  • 文件摘要
  • 合約重點整理
  • 報告內容分析
  • 表格或文字檔理解
  • 將文件內容整理成結構化資料

不過要注意,檔案輸入不等於完整的檔案搜尋系統。

如果你的需求是「讓模型在大量文件中檢索資料」,通常應該考慮:

  • file search
  • retrieval
  • vector store
  • RAG 流程

如果你的需求是「這次請求附上一份文件,請模型讀它並回答」,才比較像 file content part 的使用情境。

J.7 modalities:指定模型輸出型態

modalities 用來指定模型這次要產生哪些輸出型態。

一般文字輸出不一定需要特別設定 modalities。
但如果你希望模型產生音訊輸出,就需要使用 modalities 搭配 audio 參數。

例如:

completion = client.chat.completions.create(
    model="gpt-5.5",
    messages=[
        {
            "role": "user",
            "content": "請用一句話介紹 Chat Completions API。"
        }
    ],
    modalities=["text", "audio"],
    audio={
        "format": "mp3",
        "voice": "alloy"
    },
)

這裡的意思是:模型除了產生文字之外,也要產生音訊輸出。

modalities 常見值可以理解成:

說明
text文字輸出
audio音訊輸出

如果你只需要一般文字回答,通常不需要設定:

modalities=["text"]

因為文字輸出就是最常見的預設情境。

但如果你需要音訊輸出,就要設定:

modalities=["audio"]

或:

modalities=["text", "audio"]

實務上,音訊輸出適合:

  • 語音助理
  • 朗讀回答
  • 無障礙輔助
  • 語音客服
  • 口語學習工具
  • 車載或行動情境

J.8 audio:設定音訊輸出格式與聲音

當你使用 modalities 要求音訊輸出時,需要提供 audio 參數。

audio 主要包含:

  • format
  • voice

例如:

completion = client.chat.completions.create(
    model="gpt-5.5",
    messages=[
        {
            "role": "user",
            "content": "請用自然口吻說明什麼是 Chat Completions API。"
        }
    ],
    modalities=["audio"],
    audio={
        "format": "mp3",
        "voice": "alloy"
    },
)

format 用來指定音訊格式,常見選項包括:

wav
mp3
flac
opus
pcm16

voice 用來指定聲音。

例如:

audio={
    "format": "mp3",
    "voice": "alloy"
}

不同聲音會有不同音色與風格,實際可用 voice 可能會依文件與模型更新而改變。

使用音訊輸出時,回傳的 assistant message 可能會包含 audio 相關資料,例如音訊 ID、base64 encoded audio bytes、過期時間與 transcript。

概念上,你可能會看到類似這樣的結構:

{
  "role": "assistant",
  "content": "Chat Completions API 是一個讓開發者傳入對話訊息並取得模型回覆的介面。",
  "audio": {
    "id": "audio_abc123",
    "data": "base64_encoded_audio_bytes",
    "format": "mp3",
    "transcript": "Chat Completions API 是一個讓開發者傳入對話訊息並取得模型回覆的介面。"
  }
}

實務上,你可以把 audio.data 解碼成音訊檔,再提供給前端播放。

J.9 assistant audio:多輪對話中的前一次音訊回覆

在多輪音訊對話中,assistant message 可能會帶有前一次 audio response 的資料。

概念上會像:

{
  "role": "assistant",
  "audio": {
    "id": "audio_abc123"
  },
  "content": "這是上一輪模型產生的音訊回答。"
}

這可以讓模型在多輪對話中知道前一次音訊回覆的脈絡。

不過,若你只是做一般文字聊天,通常不需要處理這個欄位。
它比較常出現在支援音訊輸入輸出的多模態應用中。

J.10 prediction:舊文件或其他 API 中可能看到的進階參數

你可能會在舊版文章、其他 API 範例或 latency optimization 相關文件裡看到 prediction 這類參數。

它的概念通常是:如果你已經大致知道模型可能要產生的內容,可以提供預測內容,讓模型在輸出大量已知文字時更快完成。這類能力常見於「修改既有文件」或「產生大部分內容已知、只有小部分不同」的情境。

例如:

  • 修改一份長文件中的少數段落。
  • 根據既有檔案產生小幅更新版本。
  • 讓模型輸出和已知模板高度相似的內容。
  • 降低大量重複文字生成的延遲。

不過,以目前 Chat Completions API reference 來看,我不建議在這篇文章把 prediction 當成主要 Chat Completions 參數介紹。比較穩妥的寫法是:

如果你在舊文件或其他 API 中看到 prediction,可以先把它理解成 latency optimization 相關的進階能力;但在撰寫 Chat Completions API 參數解析時,應以目前官方 Chat Completions reference 實際列出的參數為準。

因此,這篇文章可以在這裡簡單提到 prediction,但不建議給太多 Chat Completions 專用範例,避免讀者照抄後遇到模型或 API 不支援的問題。

如果後續要深入介紹 prediction 或 predicted outputs,可以另外寫在「延遲優化」或「Responses API」相關文章裡。

J.11 多模態輸入的實務範例:分析收據圖片

假設我們要做一個旅行分帳工具,使用者上傳一張收據圖片,希望模型先幫忙讀取店名、日期、幣別與總金額。

可以這樣寫:

completion = client.chat.completions.create(
    model="gpt-5.5",
    messages=[
        {
            "role": "developer",
            "content": "你是收據資料整理助理。請根據圖片內容整理店名、日期、幣別與總金額;如果看不清楚,請明確標示 unknown。"
        },
        {
            "role": "user",
            "content": [
                {
                    "type": "text",
                    "text": "請讀取這張收據,整理出 merchant、date、currency、total。"
                },
                {
                    "type": "image_url",
                    "image_url": {
                        "url": "https://example.com/receipt.jpg",
                        "detail": "high"
                    }
                }
            ]
        }
    ],
)

如果要讓結果更容易被程式處理,可以搭配 response_format:

completion = client.chat.completions.create(
    model="gpt-5.5",
    messages=[
        {
            "role": "developer",
            "content": "你是收據資料整理助理。請根據圖片內容抽取消費資訊。看不清楚的欄位請填 null。"
        },
        {
            "role": "user",
            "content": [
                {
                    "type": "text",
                    "text": "請讀取這張收據。"
                },
                {
                    "type": "image_url",
                    "image_url": {
                        "url": "https://example.com/receipt.jpg",
                        "detail": "high"
                    }
                }
            ]
        }
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "receipt_summary",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "merchant": {
                        "type": ["string", "null"]
                    },
                    "date": {
                        "type": ["string", "null"]
                    },
                    "currency": {
                        "type": ["string", "null"]
                    },
                    "total": {
                        "type": ["number", "null"]
                    }
                },
                "required": [
                    "merchant",
                    "date",
                    "currency",
                    "total"
                ],
                "additionalProperties": False
            }
        }
    },
)

這樣可以把多模態輸入和結構化輸出結合起來。

對 TallyTrip 這類產品來說,流程可能會是:

  1. 使用者上傳收據圖片。
  2. 後端把圖片傳給模型。
  3. 模型抽取可能的消費資訊。
  4. 後端驗證資料格式。
  5. 建立一筆「待確認」的花費草稿。
  6. 使用者確認後才正式寫入旅程花費。

這裡要特別注意:收據辨識結果不應該未經確認就直接進入正式帳務。模型可以協助降低輸入成本,但最後仍應讓使用者確認金額、幣別與日期。

J.12 多模態輸入的實務範例:分析票券或訂位截圖

另一個常見情境是旅行中的票券或訂位截圖。

例如使用者上傳一張交通票券截圖,希望模型幫忙整理:

  • 交通方式
  • 出發地
  • 目的地
  • 出發時間
  • 票價
  • 訂位編號

可以這樣寫:

completion = client.chat.completions.create(
    model="gpt-5.5",
    messages=[
        {
            "role": "developer",
            "content": "你是旅程資料整理助理。請根據票券或訂位截圖整理旅程資訊。看不清楚的欄位請填 null。"
        },
        {
            "role": "user",
            "content": [
                {
                    "type": "text",
                    "text": "請從這張票券截圖中整理出交通方式、出發地、目的地、出發時間與訂位編號。"
                },
                {
                    "type": "image_url",
                    "image_url": {
                        "url": "https://example.com/ticket.png",
                        "detail": "high"
                    }
                }
            ]
        }
    ],
)

如果再搭配 response_format,就可以直接把結果轉成旅程素材或行程草稿。

這種應用很適合旅行工具,因為旅行資料常常不是乾淨的表單,而是散落在:

  • 截圖
  • 電子票券
  • 訂位信
  • 收據照片
  • 地圖截圖
  • 聊天訊息

多模態輸入的價值,就是讓模型可以協助理解這些非結構化資料。

J.13 什麼時候該用多模態,什麼時候不要?

多模態很方便,但不代表所有任務都應該使用。

可以這樣判斷:

情境建議
使用者輸入本來就是文字使用純文字
要分析圖片內容使用 image_url
要分析截圖、票券、收據使用 image_url,必要時 detail=”high”
要分析語音內容使用 input_audio 或 transcription API
要整理單一文件可以使用 file content part
要搜尋大量文件考慮 file search、retrieval 或 RAG
要語音回答使用者使用 modalities + audio
要即時語音互動考慮 Realtime API

實務上,我會建議先問自己三個問題:

  1. 這個任務是否真的需要圖片、音訊或檔案?
  2. 模型是否支援該類輸入或輸出?
  3. 輸出結果是否需要後端驗證或人工確認?

如果答案不明確,先用純文字流程會比較簡單,也比較容易除錯。

J.14 多模態與進階輸入輸出小結

整理一下:

參數或格式用途
content string最簡單的純文字輸入
content array使用 content parts 表示多模態輸入
type: “text”文字 content part
type: “image_url”圖片輸入
image_url.detail控制圖片理解細節,例如 auto、low、high
type: “input_audio”音訊輸入
type: “file”檔案輸入
modalities指定輸出型態,例如文字或音訊
audio設定音訊輸出的格式與聲音
prediction在舊文件或其他 API 中可能看到的 latency optimization 相關能力,不建議在目前 Chat Completions 文章中當成主要參數

實務上:

  • 純文字聊天:使用字串 content。
  • 圖片、音訊、檔案輸入:使用 content parts。
  • 收據、票券、截圖分析:可以使用 image_url,必要時搭配 detail=”high”。
  • 語音輸入:使用 input_audio 或專門 transcription API。
  • 語音輸出:使用 modalities 搭配 audio。
  • 結構化資料:多模態輸入可以搭配 response_format 和 JSON Schema。
  • 高精度資料:仍要做後端驗證與人工確認。

多模態能力讓 Chat Completions API 不再只是文字聊天介面,而是可以處理更接近真實產品情境的輸入:圖片、截圖、音訊、文件與結構化資料流程。
但也因為多模態能力更依賴模型支援與資料品質,實作時更應該注意 fallback、驗證與錯誤處理。

下一章會介紹可觀測性與資料管理相關參數,例如 store、metadata、user 與 service_tier。這些參數不一定會改變模型回答內容,但會影響產品如何追蹤請求、管理資料與控制服務等級。

結語:從進階功能走向正式產品維護

到這裡,我們已經把 Chat Completions API 中幾個最重要的進階能力整理完了。

這一篇主要聚焦在三個方向:

  • 使用 response_format 讓模型輸出一般文字、JSON object,或符合 JSON Schema 的結構化資料。
  • 使用 tools、tool_choice、parallel_tool_calls 讓模型可以在需要時呼叫後端工具或內部 API。
  • 使用 stream 與 stream_options.include_usage 讓模型回覆可以分段傳回,改善聊天 UI 的即時體驗。
  • 透過 content parts、modalities、audio 等設定,處理圖片、音訊、檔案與其他多模態輸入輸出。

如果第一篇是在建立 Chat Completions API 的基礎心智模型,那第二篇就是把它往產品實作推進了一步。

因為實務上,AI 應用通常不只是「使用者問一句,模型回一句」。更常見的需求是:

  • 模型要回傳可以被後端解析的資料。
  • 模型要根據資料庫或內部 API 的結果回答。
  • 模型要支援即時串流輸出。
  • 模型要能處理圖片、收據、截圖、音訊或文件。
  • 模型產生的資料要能接到正式產品流程中。

這些能力讓 Chat Completions API 不只是聊天介面,而是可以成為產品中的一層 AI workflow。

不過,當這些功能真的進入正式產品後,下一個問題就會出現:我們要如何追蹤、除錯、維護與控管這些 AI 請求?

例如:

  • 每次請求花了多少 token?
  • 哪個功能最耗成本?
  • 哪個 prompt version 效果比較好?
  • 模型為什麼回答被截斷?
  • streaming chunk 要怎麼完整組回 assistant message?
  • Tool Calling 的結果要怎麼記錄?
  • 舊版 functions、function_call、max_tokens 應該怎麼遷移?
  • 正式產品中應該記錄哪些欄位,方便日後除錯?

這些問題就會進入第三篇的主題。

下一篇會接著介紹:

打造專屬 ChatGPT(三):回傳物件、除錯與實務設定

我們會整理 store、metadata、user、service_tier、usage 等可觀測性相關參數,接著介紹 logprobs、top_logprobs、seed、system_fingerprint 等除錯與分析工具。

之後也會完整拆解非串流模式的 chat.completion 回傳物件,以及串流模式的 chat.completion.chunk。最後再集中整理已棄用或相容性參數,並用幾個實務場景示範如何組合這些設定。

第二篇是在說明 Chat Completions API 能做什麼,第三篇就會回到正式產品最重要的問題:如何讓這些能力可追蹤、可除錯、可維護。