讓模型輸出 JSON
模型的回答要交給程式處理時,得是格式可靠的 JSON。講 JSON 模式、用 Pydantic 校驗並在失敗時讓模型改正,以及用嚴格模式的工具呼叫拿到符合 schema 的結構化輸出。
- 約 40 分鐘
- 難度:入門
- 實測:2026-09-14 deepseek-flash,pydantic 2
程式碼和執行結果保留原樣(簡體中文),與實際執行時完全一致。
到目前為止,模型的回答都是給人看的。可一旦要把回答交給程式處理,比如把使用者的求助自動整理後存進資料庫、按分類結果分派給不同的人,你需要的就是一個格式固定、欄位齊全、能直接解析的 JSON。
讓模型"輸出 JSON"很容易,讓它每次都輸出合法的、欄位正確的 JSON,需要一點工程上的功夫。這一課講三層保障:JSON 模式保證語法,Pydantic 校驗保證內容,嚴格模式的工具呼叫保證結構。
只靠提示詞會出什麼問題
最直接的辦法是在提示詞裡寫"請輸出 JSON"。大多數時候它能做到,但總有一些時候:
- 在 JSON 前面加一句"好的,以下是提取結果:",或者用 ```json 代码块包起来,
json.loads直接報錯。 - 欄位名不一致,這次叫
httpx_version,下次叫version。 - 該是數字的欄位給了字串,該是列表的給了一個逗號分隔的字串。
- 回答太長,被
max_tokens截斷,JSON 缺了最後的括號。
一個每天呼叫幾萬次的程式,1% 的失敗率也意味著每天幾百次出錯。所以要一層層加保障。
第一層:JSON 模式
DeepSeek 和很多相容 OpenAI 介面的服務都支援 JSON 模式:在請求里加上 response_format={"type": "json_object"},模型的輸出保證是一段語法合法的 JSON,不會有多餘的開場白。
DeepSeek 的文件(截至 2026 年 9 月)對 JSON 模式有三個要求:
- 設定
response_format={"type": "json_object"}。 - 在 system 或 user 訊息裡出現 "json" 這個詞,並給出期望格式的示例。
max_tokens設得足夠大,防止 JSON 被截斷。
第二條是硬性要求。我試了一下,提示詞裡不寫 json,伺服器直接拒絕:
提示词里没有 json,报错: Error code: 400 - {'error': {'message': "Prompt must contain the word 'json' in some form to use 'response_format' of type 'json_object'.", 'type': 'invalid_request_error', 'param': None, 'code': 'invalid_request_error'}}
文件裡還有一句提醒:API 偶爾可能返回空的內容。這意味著即使開了 JSON 模式,你的程式碼也不能假設一定能拿到結果。
第二層:用 Pydantic 校驗
JSON 模式只保證語法,不保證內容:欄位可能缺了,型別可能不對。所以拿到 JSON 之後,要用程式檢查一遍。
Python 裡最方便的工具是 Pydantic。安裝 openai 時它已經作為依賴一起裝好了。先用一個類定義你想要的結構:
from pydantic import BaseModel
class BugReport(BaseModel):
title: str
httpx_version: str | None # 原话里没提就是 null
python_version: str | None
os: str | None
error: str | None
missing_info: list[str]
str | None 表示這個欄位可以是字串,也可以是 null。BugReport.model_validate_json(text) 會解析 JSON 並檢查每個欄位:缺欄位、型別不對都會丟擲 ValidationError,並且清楚地說明哪個欄位出了什麼問題。
提示詞裡說清楚要什麼,並給出格式示例,這同時滿足了 DeepSeek"要出現 json 這個詞"的要求:
SYSTEM = """从用户的求助原话中提取信息,输出 JSON。原话里没有的字段填 null,不要猜。
JSON 格式示例:
{"title": "一句话概括问题", "httpx_version": "0.27", "python_version": "3.11",
"os": "macOS", "error": "报错类型或信息", "missing_info": ["排查还需要知道的信息"]}"""
校驗失敗了怎麼辦:把錯誤告訴模型
校驗失敗時,最簡單有效的做法是把錯誤資訊原樣發回給模型,讓它改正。這是一段可以直接複用的程式碼:
def extract(report, max_attempts=3):
messages = [{"role": "system", "content": SYSTEM}, {"role": "user", "content": report}]
for attempt in range(1, max_attempts + 1):
response = client.chat.completions.create(
model=MODEL,
messages=messages,
response_format={"type": "json_object"},
max_tokens=1000,
extra_body={"thinking": {"type": "disabled"}},
)
content = response.choices[0].message.content
try:
return BugReport.model_validate_json(content), attempt
except ValidationError as e:
# 把错误原样告诉模型,让它改正。json 语法错误和字段错误都会走到这里
print(f"第 {attempt} 次校验失败:{e.errors()[0]['msg']}")
messages += [
{"role": "assistant", "content": content or ""},
{"role": "user", "content": f"你的输出没有通过校验:{e}\n请重新输出完整、正确的 JSON。"},
]
raise RuntimeError(f"{max_attempts} 次都没有得到合法的输出")
幾個細節:
- 重試時,把模型上一次的錯誤輸出作為
assistant訊息放回去,再在user訊息裡說明錯在哪。模型能看到自己錯在哪裡,改正的成功率比從頭再問一次高。 - 空內容(
content是None或空字串)也會在校驗時失敗,同樣會觸發重試,這就處理了文件裡說的"偶爾返回空內容"。 - 設定最大重試次數。重試三次還不行,多半是提示詞或者資料本身有問題,繼續重試只是浪費錢,應該報錯讓人來看。
用第 1 課那段 httpx 求助試一下(完整程式碼見 code/02-prompting/json_output.py):
第 1 次成功:
{
"title": "httpx stream下载大文件中途ReadTimeout",
"httpx_version": "0.27",
"python_version": "3.11",
"os": "macOS",
"error": "ReadTimeout",
"missing_info": [
"具体的ReadTimeout异常堆栈",
"当前timeout配置值",
"重试逻辑或下载代码片段",
"网络代理或内网限制情况"
]
}
這次第一次就通過了。在我的測試中,有了 JSON 模式和格式示例之後,校驗失敗的情況很少見。但"很少"不等於"沒有",重試的程式碼是給那少數情況準備的保險。
拿到的 report 是一個 Python 物件,可以直接用 report.httpx_version 訪問欄位,編輯器也能自動補全。這比在字典裡用字串取值可靠得多。
第三層:用工具呼叫拿結構化輸出
還有一種辦法,能讓結構從一開始就受到約束:讓模型"呼叫一個工具",工具的參數就是你想要的結構。
工具呼叫(function calling)本來是讓模型呼叫外部函式用的,03 模組第 3 課會詳細講。這裡只借用它的一個特性:你用 JSON Schema 描述工具的參數,模型生成的參數會按這個結構來。DeepSeek 還提供了嚴格模式(strict),開啟後模型輸出的參數會嚴格符合 schema,列舉值也只能從你給的選項裡選。
截至 2026 年 9 月,DeepSeek 的嚴格模式是 Beta 功能,要把 base_url 換成 https://api.deepseek.com/beta,在函式定義裡寫 "strict": True,並且 schema 裡要有 "additionalProperties": False:
client = OpenAI(
api_key=os.environ["LLM_API_KEY"],
base_url="https://api.deepseek.com/beta",
)
tool = {
"type": "function",
"function": {
"name": "save_bug_report",
"description": "保存从用户原话中提取出的问题信息",
"strict": True,
"parameters": {
"type": "object",
"properties": {
"title": {"type": "string", "description": "一句话概括问题"},
"httpx_version": {"type": "string", "description": "原话里没有就填空字符串"},
"os": {"type": "string", "enum": ["macOS", "Windows", "Linux", "未知"]},
"severity": {"type": "string", "enum": ["阻塞", "严重", "一般"]},
},
"required": ["title", "httpx_version", "os", "severity"],
"additionalProperties": False,
},
},
}
response = client.chat.completions.create(
model=MODEL,
messages=[{"role": "user", "content": "提取这段求助里的信息并保存:\n" + REPORT}],
tools=[tool],
# 强制调用这个工具,而不是让模型自己决定要不要调用
tool_choice={"type": "function", "function": {"name": "save_bug_report"}},
extra_body={"thinking": {"type": "disabled"}},
)
call = response.choices[0].message.tool_calls[0]
print("模型调用了:", call.function.name)
print(json.dumps(json.loads(call.function.arguments), ensure_ascii=False, indent=2))
執行結果:
模型调用了: save_bug_report
{
"title": "用httpx stream下载2G大文件到一半报ReadTimeout连接中断",
"httpx_version": "0.27",
"os": "macOS",
"severity": "严重"
}
os 和 severity 都落在了給定的列舉值裡。模型並沒有真的去"儲存"什麼,save_bug_report 這個函式根本不存在,我們只是借它的參數拿到結構化資料。
tool_choice 指定了必須呼叫哪個工具。不指定的話,模型可能決定不呼叫工具,直接用文字回答。
三種方法怎麼選
| 方法 | 保證什麼 | 適合 |
|---|---|---|
| JSON 模式 + Pydantic 校驗 + 重試 | 語法由模式保證,內容由校驗和重試兜底 | 大多數情況的首選,各家服務商都支援 |
| 嚴格模式的工具呼叫 | 輸出嚴格符合 schema | 結構複雜、列舉值多、不想寫重試邏輯時 |
| 只靠提示詞 | 什麼都不保證 | 模型或服務商不支援以上兩種時,一定要配合校驗 |
不管用哪種,程式裡的校驗都不要省。嚴格模式能保證結構,卻保證不了內容:模型仍然可能把版本號提取錯,把"一般"的問題標成"嚴重"。結構正確只是第一步,內容對不對,要靠 06 模組講的評估來檢查。
另外兩個小提醒:
- 欄位越少越穩定。一次提取二十個欄位,出錯的機率遠高於五個欄位。欄位多時,考慮拆成幾次呼叫。
- 允許"沒有"。一定要給模型一個表達"原文裡沒有這個資訊"的方式,比如
null或者空字串,並在提示詞裡說明。否則模型為了填滿欄位,就會開始編。
練習
- 把
BugReport裡的missing_info改成list[int](故意定義錯),執行json_output.py,看看校驗失敗和重試的過程是什麼樣的。 - 給
BugReport加一個欄位severity,限定只能是"阻塞"、"嚴重"、"一般"之一(提示:用typing.Literal)。故意給一段看不出嚴重程度的求助,看模型怎麼處理。 - 用
json_strict.py的方法,給第 2 課的留言分類做一個工具,類別用enum限定,處理 20 條留言,看看格式是不是全部正確。
自測
1. 開啟了 JSON 模式,為什麼還要用 Pydantic 校驗?
JSON 模式只保證輸出是語法合法的 JSON,不保證欄位齊全、名字正確、型別正確。另外,DeepSeek 文件也提到 API 偶爾會返回空內容。Pydantic 校驗能發現這些問題,配合重試把它們處理掉。
2. 校驗失敗後重試時,為什麼要把模型上一次的錯誤輸出也放回訊息裡?
這樣模型能看到自己上次輸出了什麼,結合你給出的錯誤資訊,有針對性地改正。如果只是把原問題再問一遍,模型不知道哪裡錯了,很可能犯同樣的錯。
3. 用工具呼叫拿結構化輸出時,tool_choice 起什麼作用?
指定模型必須呼叫某個工具。不指定時,模型會自己決定調不呼叫工具,可能直接用文字回答,這樣就拿不到結構化的參數了。
提問與討論
這一課沒看懂的地方,在這裡問。看到別人的問題,也歡迎你來回答。
提問 +3 點,回答別人 +6 點。內容經審核後公開。
正在載入討論…