iT邦幫忙

2026 iThome 鐵人賽

DAY 4
0
AI Engineering

從呼叫 API 到打造 Gateway:LLM 工程化 30 天系列 第 4

Day 04:Structured Output / Tool Use 讓 LLM 回傳程式看得懂的資料

  • 分享至 

  • xImage
  •  

Day 4|Structured Output / Tool Use:讓 LLM 回傳程式看得懂的資料

前面分享了 LLM 回應的形式,streaming 分段是方便人讀懂沒錯,但要讓程式直接處理的話,單單只是一段自然語言文字還是有點困擾。

例如要把使用者輸入的訂單描述,轉成一個可以直接寫進資料庫的物件,結構化資料就顯得格外重要。


如何獲得結構化的資料?

傳統上的做法是取得資料後再做處理(post-hoc parsing),但現在透過 LLM 本身的能力與廠商提供的 API,我們也可以在向模型發送請求時,要求模型回傳指定的形式。

最直覺的做法:在 prompt 裡直接要求「請用 JSON 格式回答,欄位是 xxx」。這個做法雖然能動,但 LLM 沒辦法保證每次的輸出都合乎規定,漏欄位、加入多餘內容等都有機率會發生。在這種情況下讓程式直接對回應做 json.loads,會伴隨著模型的不穩定而爆炸!

正規的做法是使用 API 原生支援的機制——Tool UseStructured Outputs


方法一:Tool Use

Tool Use 原本的設計目的是讓模型能夠呼叫外部工具(查天氣、查資料庫等),但只要我們定義一個「工具」,把它的 input_schema 設計成我們想要的資料格式,模型在決定呼叫這個工具時,就會生成符合該 schema 的參數——這組參數本質上就是我們要的結構化資料。

實際範例:從一句話擷取訂單資訊

假設使用者輸入「我要訂兩件白色的 T 恤,寄到台北市信義區松高路 1 號」,我們想擷取出商品、數量、地址。看看 Anthropic 的寫法:

import os
import anthropic

client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])

order_tool = {
    "name": "extract_order",
    "description": "從使用者描述中擷取訂單資訊",
    "input_schema": {
        "type": "object",
        "properties": {
            "product": {"type": "string", "description": "商品名稱"},
            "quantity": {"type": "number", "description": "數量"},
            "address": {"type": "string", "description": "收件地址"},
        },
        "required": ["product", "quantity", "address"],
    },
}

response = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=1024,
    tools=[order_tool],
    tool_choice={"type": "tool", "name": "extract_order"},
    messages=[
        {"role": "user", "content": "我要訂兩件白色的T恤,寄到台北市信義區松高路1號"}
    ],
)

tool_use = next(block for block in response.content if block.type == "tool_use")
print(tool_use.input)
# {'product': '白色T恤', 'quantity': 2, 'address': '台北市信義區松高路1號'}

拿到的 tool_use.input 已經是一個現成的物件(dict),不用自己再解析一次。


方法二:Structured Outputs

雖然方法一已經能獲得想要的結構化資料,後來各家廠商還是推出了可以直接控制回應格式的 API,不需要再假借工具名義。看看 Anthropic 的寫法:

import os
import anthropic

client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])

order_schema = {
    "type": "object",
    "properties": {
        "product": {"type": "string", "description": "商品名稱"},
        "quantity": {"type": "number", "description": "數量"},
        "address": {"type": "string", "description": "收件地址"},
    },
    "required": ["product", "quantity", "address"],
    "additionalProperties": False,
}

response = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=1024,
    output_config={
        "format": {
            "type": "json_schema",
            "schema": order_schema,
        }
    },
    messages=[
        {"role": "user", "content": "我要訂兩件白色的T恤,寄到台北市信義區松高路1號"}
    ],
)

text_block = next(block for block in response.content if block.type == "text")
print(text_block.text)
# {"product": "白色T恤", "quantity": 2, "address": "台北市信義區松高路1號"}

跟 Tool Use 不同的是,text 區塊本身就已經是合法 JSON。透過 API 的 constrained decoding(限制性取樣)在生成階段就強制輸出符合 schema,而不是生成完再驗證重試,因此不再需要處理 JSON 解析錯誤、缺欄位或型別不一致等問題,是官方提供的可靠途徑。

OpenAI 的 Structured Outputs

import os
from openai import OpenAI

client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])

order_schema = {
    "name": "extract_order",
    "schema": {
        "type": "object",
        "properties": {
            "product": {"type": "string", "description": "商品名稱"},
            "quantity": {"type": "number", "description": "數量"},
            "address": {"type": "string", "description": "收件地址"},
        },
        "required": ["product", "quantity", "address"],
        "additionalProperties": False,
    },
    "strict": True,
}

response = client.chat.completions.create(
    model="gpt-4.1",
    max_tokens=1024,
    response_format={"type": "json_schema", "json_schema": order_schema},
    messages=[
        {"role": "user", "content": "我要訂兩件白色的T恤,寄到台北市信義區松高路1號"}
    ],
)

print(response.choices[0].message.content)
# {"product": "白色T恤", "quantity": 2, "address": "台北市信義區松高路1號"}

跟 Anthropic 不同點:

  1. 參數名稱不同(output_config.format vs. response_format)。
  2. schema 包在 json_schema 底下。
  3. OpenAI 多一道手續,要先幫 schema 命名,才能放在 json_schema 參數中。

兩者都會讓模型回傳一段合法的 JSON 字串,但不像 Tool Use 會自動幫忙解析成物件,要自己用 json.loads 轉換後才能當一般資料使用。


今日小結

有了結構化輸出,LLM 才能真正被接進程式的資料流裡——它的輸出不再只是給人看的文字,而是可以直接被下一段程式邏輯使用的資料。

其實目前 OpenAI 跟 Anthropic 都有提供更方便的方法,透過搭配 Pydantic model 來定義 schema,並回傳型別驗證過的物件,但今天示範的做法比較能看清楚底層 schema 回傳的原理。在正式專案中如果要使用較嚴謹的方法,也可以到官網上參考相關資訊。

我們已經學會如何將 LLM 的回應做成結構化資料,目前為止範例都很順利,但跑程式的時候不會永遠都是成功的,明天將會分享遇到 API 呼叫失敗的情況!


上一篇
Day 03:Streaming 回應:串流的用途
系列文
從呼叫 API 到打造 Gateway:LLM 工程化 30 天4
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言