結構化輸出:讓 AI 回傳 JSON
模型的回答是給人讀的文字,程式卻需要固定格式的資料。學會用 Pydantic 定義規格,讓模型從訂單或發票文字裡抽出欄位,回傳程式能直接使用的 JSON。
到目前為止,模型的回答都是印出來給人讀的。但很多實用的工作,下一步是交給程式處理:把訂單寫進試算表、把發票金額加總、把客訴信件分類後轉給對的人。
這時候自由發揮的文字就很麻煩。你問「這筆訂單買了什麼?」,它可能回「王小姐買了兩盒鳳梨酥喔!」,也可能回一個表格。程式沒辦法可靠地從各種寫法裡把數字挖出來。
我們需要模型每次都用同一種格式回答,這就是結構化輸出。
先認識 JSON 和 Pydantic
JSON 是程式之間交換資料最常見的格式,你在「資料與自動化」單元已經用過。它長這樣:
{"customer": "王小美", "items": [{"name": "鳳梨酥", "quantity": 2, "unit_price": 350}]}
Pydantic 是一個 Python 套件,讓你用類別寫出「資料應該長什麼樣子」:有哪些欄位、每個欄位是文字還是數字。它就是你和模型之間的規格書。
pip install pydantic
你負責定義規格,模型負責從文字裡把資料填進去。
從訂單文字抽出欄位
假設你在經營一個小網店,客人常用私訊下單,內容五花八門。我們要把它整理成固定的訂單資料。
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 使用。
我有一個用 Pydantic 定義的 Order 類別,用來從客人訊息抽出訂單。請幫我改寫程式,讓它讀取 orders 資料夾裡所有 .txt 檔,每個檔案抽出一筆訂單,最後把全部訂單存成一個 orders.csv,金額對不上的訂單在 CSV 裡標記出來。
- 執行訂單抽取程式,確認
order.items有兩個品項。 - 故意把訊息裡的總金額改成
900,確認核對程式會提醒你。 - 把訊息裡的電話刪掉,確認
order.phone變成None,而不是被編造出來。 - 換一個主題自己設計規格:例如從一張發票文字抽出「發票號碼、日期、賣方、品項、總金額」,或從求職信抽出「姓名、應徵職位、年資」。
讀完了嗎?
標記完成後,單元與專案的進度會自動更新(只存在這台裝置的瀏覽器)。
留言與討論
卡關了、有心得,或發現內容需要更新?都歡迎留言。留言使用 GitHub 帳號登入。