OpenAI 能輸出你想要的格式 (JSON Schema)
目錄
OpenAI 在八月推出了 “Structured Output” 的功能, 能夠 100% 讓模型以「你想要的 JSON 結構」的格式輸出,例如有一欄 “explanation”、另一欄 “output” 等等
這個只在 API 層支援;普羅大眾去 ChatGPT 網站聊天是沒有的
結構化輸出:為什麼重要? #
GPT 不只是聊天機器人
GPT 能處理自由、無結構的文字,做各種推測彙整。也可以是軟體內眾多環節的一步:他做了很聰明的處理後,再分工給後面的程式做其他的處理
然而,程式通常是一板一眼、看不懂自由的文字。相反地,GPT 雖然看得懂自由的文字,但輸出也通常是自由的文字
OpenAI API 這次提供 “Structured Output” 的功能,就能把自由的 GPT 跟實際的程式銜接起來,讓開發者寫的軟體變得更厲害
以前就可以了不是嗎? #
- 可以用 prompt 告訴 GPT 「請輸出 JSON 格式,第一欄要…」,但不見得 100% 會是合法的 JSON object
- OpenAI 也支援
response_format
填{ "type": "json_object" }
,雖然回答會是 JSON,但裡面的欄位名字不一定是你期望的
身為一個工程師,當你真的很希望 GPT 輸出固定的結構、固定的欄位名字,就是賭骰子。根據 OpenAI 的評估 複雜的結構,用 prompt 或 JSON 模式有 85% ~ 93% 的成功率
… 你要賭那 7% 失敗率嗎?所以會做例外處理:retry, correction, fallback 等等
OpenAI 這次的 json_schema
就是讓工程師安心,讓例外處理變稍微簡單(還是有例外,請見後文)
等等,什麼是 JSON Schema? #
當你要描述輸出的 JSON 格式,你是用 JSON Schema 來描述之(繞口令)
例如這個 JSON Schema
{
"type": "object",
"properties": {
"SCORES": {
"type": "array",
"items": { "type": "integer" }
},
"COMMENT": { "type": "string" }
},
"required": ["SCORES", "COMMENT"]
}
描述著類似像這樣的 JSON 物件
{
"SCORES": [50, 70, 20],
"COMMENT": "U SHALL NOT PASS"
}
題外話 JSON Schema https://json-schema.org/ 是業界標準,並不是 OpenAI 發明的。JSON Schema 本身的目的,就是去描述跟驗證一個 JSON 物件該長怎樣
怎麼讓 OpenAI API 的輸出結構化? #
兩個場景:Tool Use (function calling) 跟普通聊天 (chat completition)
模型必須是 gpt-4o-mini
, gpt-4o-2024-08-06
或更之後的
要讓 OpenAI 輸出固定的格式,你要告訴他「格式」,以及兩三個重要的參數(見下)
Tool Use #
你本來就會指定輸出的格式了(亦即 function 參數的格式,所以 GPT 會盡量照著格式回傳),所以你只要
- 在
function
物件內加strict: true
- 確保
function
的parameters
每個參數 (properties) 都是必須 required function
的parameters
內要寫"additionalProperties": False
(因為 type 是 object)
{
"type": "function",
"function": {
"strict": True,
...
"parameters": {
"type": "object",
"properties": {
"dim": {
"type": "integer", ...
},
"perk": {
"type": "string", ...
}
},
"required": ["dim", "perk"],
"additionalProperties": False
}
}
}
輸出則沒有改變,跟之前一樣
普通聊天 #
API 傳入的參數,最上層多傳個 response_format
,其值為
{
"type": "json_schema",
"json_schema": {
# 你期待輸出的 JSON Schema
"name": "...",
"schema": { ... },
"strict": True
}
}
並且
- 在
json_schema
物件內加strict: true
- 確保所有 “object” type 的屬性,每個欄位 (properties) 都是必須 required,並且指定
"additionalProperties": False
(表示不能輸出你沒定義的欄位)
官方文件的例子
POST /v1/chat/completions
{
...
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "math_response",
"strict": true,
"schema": {
"type": "object",
"properties": {
"steps": {
"type": "array",
"items": {
"type": "object",
"properties": {
"explanation": { "type": "string" },
"output": { "type": "string" }
},
"required": ["explanation", "output"],
"additionalProperties": false
}
},
"final_answer": { "type": "string" }
},
"required": ["steps", "final_answer"],
"additionalProperties": false
}
}
}
}
這 schema 描述著巢狀的 output, 每層 object 都有 required 跟 "additionalProperties": false
輸出就會照這格式
{
"steps": [
{
"explanation": "We start with the equation...",
"output": "8x + 7 = -23"
},
{
"explanation": "To move the +7...",
"output": "8x + 7 - 7 = -23 - 7"
},
(...以下省略)
],
"final_answer": "x = -3.75"
}
輸出的訊息 (message.content) 型態仍是字串,只不過這個字串可以被解析成 JSON
Python SDK #
前面的例子雖然類似 HTTP / REST API 傳進去的參數,但就算用 openai python library 道理也差不多,就是在 client.chat.completions.create
加 response_format=
或是 tools=
參數,裡面放個 python dict
建議安裝 openai 1.40.0 以後的(手寫 schema 用舊的 package 也沒問題,不過至少 release note 是 1.40.0 以後加的)
然而這都需要你「手寫 schema」,可能是個挑戰
根據官方文件,你可以用 pydantic BaseModel 描述你的輸出,且用另一個 API,就會讓整件事變得很容易
class Distance(BaseModel):
amount: int
unit: str
completion = client.beta.chat.completions.parse(
model="gpt-4o-mini",
messages=[
{
"role": "user",
"content": "台北到高雄多遠?"
}
],
response_format=Distance
)
r = completion.choices[0].message.parsed
print(type(r)) # Distance
print(r) # amount=344 unit='公里'
response_format
是傳入那個 BaseModel class,API 回傳直接是他的 object。不用處理又臭又長的 json / dict
… 只不過這 API 是 client.beta
,我們期待他變成正式版本的一天
有什麼限制? #
可能會跑很久 #
OpenAI 要先了解你指定的格式:把你輸入的 JSON Schema 轉成內部的 parser,所以官方提到第一次呼叫會比較慢;之後如果用同一個 schema 的話因為有 cache 就不會額外花時間 (官方並沒有說 cache 多久)
我隨便拿兩層 object 實測,大概第一次會有額外 5 秒的延遲,但第二次以後就沒有了(5 這個數字不是重點,會跟 schema 複雜程度有關)。題外話就算架構一樣,key 名字改了也算是新的 schema
Schema 的限制 #
- 輸出裡,支援的型態:String, Number, Boolean, Object, Array, Enum, anyOf
- 如前面所說,「每個」type 是 object 的裡面,必須指定「每個欄位」是 “required”,以及
"additionalProperties": false
但邏輯上,如果某個欄位就真的不一定需要的話,欄位型態 type 裡可以多加 “null” 來代替
"parameters": {
"type": "object",
"properties": {
"unit": {
"type": ["string", "null"],
"description": "The unit to return the temperature in",
...
更多請見官方文件: https://platform.openai.com/docs/guides/structured-outputs/supported-schemas
例外:還是會害怕 #
雖然指定 JSON Schema 以後絕大部分都會照著輸出,但還是有極端例子要處理
Refusal (拒絕):模型可能基於某些原因無法符合你的格式限制,這時候他不會硬去輸出,而是有個提示 “refusal”
{
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"refusal": "I'm sorry, I cannot assist with that request."
},
...
}
切掉:跟格式限制無關,OpenAI 輸出本來就有可能因為長度過長切掉 ("finish_reason": "length"
),只不過輸出切掉的話,很大機率是不會符合你的格式
官方有建議怎麼處理例外: https://platform.openai.com/docs/guides/structured-outputs/how-to-use
一些神秘的 JSON Schema 功能不支援 #
業界的 JSON Schema 很泛用,甚至可以驗證屬性值的特性,例如數字可加 minimum 告訴其他人這個值不應該小於多少
然而 OpenAI 還沒辦法處理這種驗證(例如確保輸出的數字不會小於多少);不支援的清單請見此
OpenAI 用了什麼魔法? #
不,這其實不是 OpenAI 新發明的:這在其他 LLM 或服務也可以看到,例如 llama.cpp 很早就提供這個功能。不如說 OpenAI 還慢了一步
其中一種方法的背後原理是:
- 把 JSON Schema 轉成語法規則 (CFG, context-free grammar)
- 每次模型預測下一個字 (token) 的時候,把不合規則的字先拿掉
GPT 能夠回答問題、文字接龍的原理是,模型有預先定義的字彙,之後根據前文預測每個可能的字 (token) 適合接在後面的機率,再丟骰子決定下一個字選哪一個
而如果你訂了規則,例如輸出的 key 名字一定是 "colour"
(包含引號),那當前文是 { "colo
的時候,就算下個字是 r 的機率比較高,因為不符合規則,所以也會先拿掉
(當然 token 不是一個個字母,這只是舉例)
如果說 GPT 文字接龍是擲骰子,那輸出格式限制就是把不合法的那幾面擦掉 (Constrained Decoding)
上述的原理只提到 CFG ,不一定要是 JSON schema,所以以 llama.cpp 為舉例,只要你寫得出語法規則,他都可以把輸出變成你想要的格式(例如只輸出數字!)
但也因為這樣,如果有太多限制,導致合理的回答並無法用你指定的格式好好說出來,那模型就會亂說亂幻想了(例如你問巴黎旅遊計畫,但又限制他只能回答數字)
JSON Schema 會讓 GPT 變笨嗎? #
如果太多限制會讓模型亂說話,那 JSON Schema 呢?
以直覺來說,這或許也跟你怎麼設計 schema 有關:你的架構與 key 名字要跟問答的情境有關聯(或通用)
最近,台灣有篇研究剛好跟這有關: https://arxiv.org/abs/2408.02442 :他們研究各個模型在 JSON Mode (也是限制 token 只不過僅規定 JSON 就好) 以及用 prompt 軟性跟模型說想要的 schema,會不會讓模型變笨
好玩的是,這個你想要模型做什麼有關
- 想要他「分類」:限制他反而比較厲害
- 想要他「推理」:限制他就比較差
不過作者有在其他地方說明(配合 Appendix D / Figure 8 )當模型比較強大的時候,變笨的情況差異很小,雖然 Appendix D 是用 prompt 限制而不是限制 token,總之可以參考一下
以上只是對一些官方文件的整理(跟自己的實驗),如果真的有興趣,建議還是以官方文件為主
- https://openai.com/index/introducing-structured-outputs-in-the-api/
- https://platform.openai.com/docs/guides/structured-outputs/examples
- https://platform.openai.com/docs/guides/structured-outputs/json-mode
- https://platform.openai.com/docs/api-reference/chat/create