打造專屬 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_schema | Structured 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_object | valid 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。
簡單來說,工具呼叫的流程是:
- 你先告訴模型有哪些工具可以使用。
- 使用者提出問題。
- 模型判斷是否需要呼叫工具。
- 如果需要,模型回傳工具名稱與參數。
- 你的程式實際執行工具。
- 你的程式把工具結果回填給模型。
- 模型根據工具結果產生最後回答。
工具呼叫常見用途包括:
- 查詢資料庫。
- 查詢訂單狀態。
- 查詢天氣。
- 取得使用者帳號資訊。
- 建立行事曆事件。
- 查詢旅程、花費或收據資料。
- 呼叫內部 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,
)
這樣可以讓工具流程更容易控制,尤其是當工具之間有依賴關係時。
例如:
- 先建立旅程。
- 再把花費加到該旅程。
- 再產生結算結果。
這種流程不適合讓模型一次平行呼叫三個工具,因為後面的工具可能依賴前面工具產生的 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
可以這樣理解:
| 舊用法 | 新用法 |
|---|---|
| functions | tools |
| function_call | tool_choice |
| function role | tool role |
| function result | tool 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.completion | completion.choices[0].message.content | 後端任務、摘要、分類、資料抽取 |
| 串流 | stream=True | chat.completion.chunk | chunk.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 出現 |
| usage | token 使用量,啟用 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..."
}
}
實務上,如果圖片是使用者上傳到你的系統,通常會有兩種做法:
- 產生模型可讀取的圖片 URL。
- 將圖片轉成 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"
}
}
其中:
| 欄位 | 說明 |
|---|---|
| data | base64 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_data | base64 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 這類產品來說,流程可能會是:
- 使用者上傳收據圖片。
- 後端把圖片傳給模型。
- 模型抽取可能的消費資訊。
- 後端驗證資料格式。
- 建立一筆「待確認」的花費草稿。
- 使用者確認後才正式寫入旅程花費。
這裡要特別注意:收據辨識結果不應該未經確認就直接進入正式帳務。模型可以協助降低輸入成本,但最後仍應讓使用者確認金額、幣別與日期。
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 |
實務上,我會建議先問自己三個問題:
- 這個任務是否真的需要圖片、音訊或檔案?
- 模型是否支援該類輸入或輸出?
- 輸出結果是否需要後端驗證或人工確認?
如果答案不明確,先用純文字流程會比較簡單,也比較容易除錯。
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 能做什麼,第三篇就會回到正式產品最重要的問題:如何讓這些能力可追蹤、可除錯、可維護。