所有入門單元免費開放 · 課程內容持續更新中

結構化輸出:讓 AI 回傳 JSON

模型的回答是給人讀的文字,程式卻需要固定格式的資料。學會用 Pydantic 定義規格,讓模型從訂單或發票文字裡抽出欄位,回傳程式能直接使用的 JSON。

第 10.5 堂約 15 分鐘

到目前為止,模型的回答都是印出來給人讀的。但很多實用的工作,下一步是交給程式處理:把訂單寫進試算表、把發票金額加總、把客訴信件分類後轉給對的人。

這時候自由發揮的文字就很麻煩。你問「這筆訂單買了什麼?」,它可能回「王小姐買了兩盒鳳梨酥喔!」,也可能回一個表格。程式沒辦法可靠地從各種寫法裡把數字挖出來。

我們需要模型每次都用同一種格式回答,這就是結構化輸出。

先認識 JSON 和 Pydantic

JSON 是程式之間交換資料最常見的格式,你在「資料與自動化」單元已經用過。它長這樣:

{"customer": "王小美", "items": [{"name": "鳳梨酥", "quantity": 2, "unit_price": 350}]}

Pydantic 是一個 Python 套件,讓你用類別寫出「資料應該長什麼樣子」:有哪些欄位、每個欄位是文字還是數字。它就是你和模型之間的規格書。

pip install pydantic

左邊是語氣隨意的訂單文字,中間是用 Pydantic 定義的欄位規格,右邊是模型照規格回傳的 JSON,最後程式可以直接計算總金額

你負責定義規格,模型負責從文字裡把資料填進去。

從訂單文字抽出欄位

假設你在經營一個小網店,客人常用私訊下單,內容五花八門。我們要把它整理成固定的訂單資料。

from dotenv import load_dotenv
from pydantic import BaseModel
import anthropic

load_dotenv()
MODEL = "claude-sonnet-5-5"  # 可以換成其他模型 ID
client = anthropic.Anthropic()


class Item(BaseModel):
    name: str
    quantity: int
    unit_price: int  # 單價,新台幣


class Order(BaseModel):
    customer: str
    phone: str | None  # 訊息裡沒有就是 None
    address: str
    items: list[Item]
    stated_total: int | None  # 客人自己寫的總金額,沒寫就是 None


message = """嗨~我是王小美,想訂 2 盒鳳梨酥,一盒 350 元,
再加 1 罐烏龍茶 280。寄到台中市西區民生路 100 號,
電話 0912-345-678,總共 980 對嗎?謝謝!!"""

response = client.messages.parse(
    model=MODEL,
    max_tokens=16000,
    system="你負責把客人的訂單訊息整理成資料。只填訊息裡明確出現的資訊,不要猜測。",
    messages=[{"role": "user", "content": message}],
    output_format=Order,
)

order = response.parsed_output
print(order)
print(order.customer, order.address)
for item in order.items:
    print(f"- {item.name} x {item.quantity},單價 {item.unit_price}")

這段程式的關鍵是兩件事:

  • output_format=Order:把規格交給模型。SDK 會把 Pydantic 類別轉成 JSON 規格,模型的回答會被限制在這個格式裡。
  • response.parsed_output:拿到的已經是 Order 物件,可以直接用 order.items 取值,不用自己處理字串或 json.loads。

格式對了,內容還要檢查

模型很擅長抽取資料,但它仍然可能看錯數字。好消息是,現在資料有結構了,你可以用程式檢查。例如把明細加總,和客人寫的總金額比對:

computed = sum(i.quantity * i.unit_price for i in order.items)
print("明細加總:", computed)

if order.stated_total is not None and order.stated_total != computed:
    print(f"注意:客人寫的總額是 {order.stated_total},和明細不符,請人工確認")
else:
    print("金額核對無誤")

這是一個很重要的習慣:讓模型做它擅長的事(讀懂雜亂的文字),讓程式做它擅長的事(精確計算與比對)。

你可能會卡在這裡

  • parsed_output 是 None:少數情況下模型可能拒絕回答,或回答被 max_tokens 截斷。先印出 response.stop_reason 看原因。
  • 欄位型別寫錯:例如把金額設成 str,就會拿到 "350元" 這種字串,沒辦法直接相加。數字欄位請用 int 或 float。
  • 規格太複雜:欄位一多、層層巢狀,出錯機率也會提高。先從最少的必要欄位開始,確定可行再慢慢加。

不用 Pydantic 也可以

如果你想直接寫 JSON 規格,也可以用 client.messages.create 搭配 output_config 參數傳入 JSON Schema,再用 json.loads 讀取回傳的文字。Pydantic 的寫法比較短、也幫你做了驗證,所以這堂課用它示範。

OpenAI、Google 的 API 也都有類似的「結構化輸出」或「JSON 模式」功能,同樣可以搭配 Pydantic 使用。

可以這樣問 AI

我有一個用 Pydantic 定義的 Order 類別,用來從客人訊息抽出訂單。請幫我改寫程式,讓它讀取 orders 資料夾裡所有 .txt 檔,每個檔案抽出一筆訂單,最後把全部訂單存成一個 orders.csv,金額對不上的訂單在 CSV 裡標記出來。

行動練習
  1. 執行訂單抽取程式,確認 order.items 有兩個品項。
  2. 故意把訊息裡的總金額改成 900,確認核對程式會提醒你。
  3. 把訊息裡的電話刪掉,確認 order.phone 變成 None,而不是被編造出來。
  4. 換一個主題自己設計規格:例如從一張發票文字抽出「發票號碼、日期、賣方、品項、總金額」,或從求職信抽出「姓名、應徵職位、年資」。

讀完了嗎?

標記完成後,單元與專案的進度會自動更新(只存在這台裝置的瀏覽器)。

卡住了?讓 AI 當你的家教複製這段提問範本,貼到 Claude 或其他 AI 工具,再補上你卡住的地方。

留言與討論

卡關了、有心得,或發現內容需要更新?都歡迎留言。留言使用 GitHub 帳號登入。

留言區即將開放。現在可以先到 GitHub 討論區 發問,或看看 交流頁 的其他管道。