Das Modell JSON ausgeben lassen
Wenn ein Programm die Antwort des Modells weiterverarbeitet, muss sie verlässliches JSON sein. JSON-Modus, Validierung mit Pydantic samt Korrektur durch das Modell bei Fehlern, und Tool-Aufrufe im strikten Modus, um schemakonforme strukturierte Ausgaben zu bekommen.
- Etwa 40 Minuten
- Niveau: Einsteiger
- Getestet: 2026-09-14 deepseek-flash, pydantic 2
Code und Programmausgaben stehen genau so da, wie sie gelaufen sind – Kommentare und Ausgaben sind daher auf Chinesisch.
Bisher waren die Antworten des Modells für Menschen gedacht. Sobald aber ein Programm die Antwort verarbeitet, etwa um Hilfeanfragen automatisch aufzubereiten und in einer Datenbank zu speichern oder je nach Klassifikation an verschiedene Personen weiterzuleiten, brauchst du JSON mit festem Format, vollständigen Feldern und direkt parsbar.
Das Modell „JSON ausgeben“ zu lassen, ist leicht; es dazu zu bringen, jedes Mal gültiges JSON mit den richtigen Feldern zu liefern, braucht etwas Handwerk. Diese Lektion zeigt drei Sicherungsebenen: Der JSON-Modus sichert die Syntax, die Pydantic-Validierung den Inhalt, strikte Tool-Aufrufe die Struktur.
Was schiefgeht, wenn man sich nur auf den Prompt verlässt
Am naheliegendsten ist, in den Prompt „gib bitte JSON aus“ zu schreiben. Meistens klappt das, aber manchmal:
- steht vor dem JSON noch „Gut, hier ist das Ergebnis der Extraktion:“, oder es ist in einen ```json-Codeblock eingepackt, und
json.loadswirft sofort einen Fehler. - sind die Feldnamen uneinheitlich, diesmal
httpx_version, nächstes Malversion. - ist ein Feld, das eine Zahl sein sollte, ein String, und eines, das eine Liste sein sollte, ein kommagetrennter String.
- ist die Antwort zu lang, wird von
max_tokensabgeschnitten, und dem JSON fehlt die letzte Klammer.
Bei einem Programm mit zehntausenden Aufrufen täglich bedeuten 1 % Fehler jeden Tag Hunderte Fehler. Also baut man Ebene um Ebene Sicherungen ein.
Ebene eins: JSON-Modus
DeepSeek und viele andere Dienste mit OpenAI-kompatibler Schnittstelle haben einen JSON-Modus: Mit response_format={"type": "json_object"} in der Anfrage ist die Ausgabe garantiert syntaktisch gültiges JSON, ohne überflüssige Einleitung.
Die Dokumentation von DeepSeek (Stand September 2026) nennt für den JSON-Modus drei Anforderungen:
response_format={"type": "json_object"}setzen.- In der system- oder user-Nachricht muss das Wort „json“ vorkommen, und man gibt ein Beispiel für das gewünschte Format.
max_tokensgroß genug wählen, damit das JSON nicht abgeschnitten wird.
Die zweite Anforderung ist hart. Ich habe es probiert: Ohne „json“ im Prompt lehnt der Server ab:
提示词里没有 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'}}
Die Dokumentation warnt außerdem: Die API kann gelegentlich leeren Inhalt zurückgeben. Auch mit JSON-Modus darf dein Code also nicht annehmen, dass er garantiert ein Ergebnis bekommt.
Ebene zwei: mit Pydantic validieren
Der JSON-Modus sichert nur die Syntax, nicht den Inhalt: Felder können fehlen, Typen falsch sein. Nach dem Empfang des JSON prüft man es daher im Programm.
Das bequemste Werkzeug in Python ist Pydantic. Es wurde bei der Installation von openai bereits als Abhängigkeit mitinstalliert. Zuerst definierst du die gewünschte Struktur als Klasse:
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 heißt, das Feld darf ein String oder null sein. BugReport.model_validate_json(text) parst das JSON und prüft jedes Feld: Fehlende Felder und falsche Typen lösen einen ValidationError aus, der klar sagt, welches Feld welches Problem hat.
Der Prompt sagt klar, was gewünscht ist, und gibt ein Formatbeispiel; das erfüllt zugleich die Anforderung von DeepSeek, dass „json“ vorkommen muss:
SYSTEM = """从用户的求助原话中提取信息,输出 JSON。原话里没有的字段填 null,不要猜。
JSON 格式示例:
{"title": "一句话概括问题", "httpx_version": "0.27", "python_version": "3.11",
"os": "macOS", "error": "报错类型或信息", "missing_info": ["排查还需要知道的信息"]}"""
Wenn die Validierung scheitert: dem Modell den Fehler sagen
Scheitert die Validierung, ist das Einfachste und Wirksamste, die Fehlermeldung unverändert an das Modell zurückzuschicken und es korrigieren zu lassen. Hier ein direkt wiederverwendbares Stück Code:
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} 次都没有得到合法的输出")
Ein paar Details:
- Beim erneuten Versuch kommt die fehlerhafte Ausgabe des Modells als
assistant-Nachricht zurück, und in deruser-Nachricht steht, was falsch war. Das Modell sieht, wo es danebenlag, und korrigiert erfolgreicher, als wenn man die Frage einfach neu stellt. - Leerer Inhalt (
contentistNoneoder ein leerer String) scheitert ebenfalls an der Validierung und löst auch einen neuen Versuch aus; damit ist der in der Dokumentation erwähnte „gelegentlich leere Inhalt“ abgedeckt. - Eine Höchstzahl an Versuchen festlegen. Klappt es nach drei Versuchen nicht, liegt es meist am Prompt oder an den Daten selbst; weitere Versuche kosten nur Geld, besser mit Fehler abbrechen und einen Menschen draufschauen lassen.
Probieren wir es mit der httpx-Hilfeanfrage aus Lektion 1 (vollständiger Code in 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配置值",
"重试逻辑或下载代码片段",
"网络代理或内网限制情况"
]
}
Diesmal klappte es beim ersten Versuch. In meinen Tests waren Validierungsfehler mit JSON-Modus und Formatbeispiel selten. Aber „selten“ heißt nicht „nie“; der Wiederholungscode ist die Versicherung für diese wenigen Fälle.
Das erhaltene report ist ein Python-Objekt; man greift direkt mit report.httpx_version auf Felder zu, und der Editor kann automatisch vervollständigen. Das ist viel verlässlicher, als Werte per String aus einem Dictionary zu holen.
Ebene drei: strukturierte Ausgabe über Tool-Aufrufe
Es gibt noch einen Weg, der die Struktur von vornherein festlegt: Man lässt das Modell „ein Tool aufrufen“, und die Parameter dieses Tools sind die gewünschte Struktur.
Tool-Aufrufe (function calling) sind eigentlich dafür da, das Modell externe Funktionen aufrufen zu lassen; Modul 03, Lektion 3 erklärt das ausführlich. Hier nutzen wir nur eine Eigenschaft: Du beschreibst die Parameter des Tools mit JSON Schema, und die vom Modell erzeugten Parameter folgen dieser Struktur. DeepSeek bietet zusätzlich einen strikten Modus (strict); ist er an, halten sich die Parameter streng an das Schema, und Aufzählungswerte können nur aus deinen Vorgaben stammen.
Stand September 2026 ist der strikte Modus bei DeepSeek eine Beta-Funktion: base_url muss https://api.deepseek.com/beta sein, in der Funktionsdefinition steht "strict": True, und das Schema braucht "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))
Ergebnis:
模型调用了: save_bug_report
{
"title": "用httpx stream下载2G大文件到一半报ReadTimeout连接中断",
"httpx_version": "0.27",
"os": "macOS",
"severity": "严重"
}
os und severity liegen beide innerhalb der vorgegebenen Aufzählungswerte. Das Modell hat nicht wirklich etwas „gespeichert“; die Funktion save_bug_report gibt es gar nicht, wir leihen uns nur ihre Parameter, um strukturierte Daten zu bekommen.
tool_choice legt fest, welches Tool aufgerufen werden muss. Ohne diese Angabe entscheidet das Modell vielleicht, kein Tool aufzurufen, und antwortet direkt mit Text.
Welche der drei Methoden
| Methode | Was sie sichert | Passt für |
|---|---|---|
| JSON-Modus + Pydantic-Validierung + Wiederholung | Syntax durch den Modus, Inhalt durch Validierung und Wiederholung | erste Wahl in den meisten Fällen, von allen Anbietern unterstützt |
| Tool-Aufruf im strikten Modus | Ausgabe folgt streng dem Schema | komplexe Struktur, viele Aufzählungswerte, wenn man keine Wiederholungslogik schreiben will |
| Nur der Prompt | nichts | wenn Modell oder Anbieter keines der beiden anderen unterstützen; dann unbedingt mit Validierung |
Egal welche Methode: Die Validierung im Programm nie weglassen. Der strikte Modus sichert die Struktur, nicht den Inhalt: Das Modell kann immer noch eine falsche Versionsnummer extrahieren oder ein „normales“ Problem als „schwer“ einstufen. Korrekte Struktur ist nur der erste Schritt; ob der Inhalt stimmt, prüft die Evaluation in Modul 06.
Noch zwei kleine Hinweise:
- Je weniger Felder, desto stabiler. Zwanzig Felder auf einmal zu extrahieren, ist viel fehleranfälliger als fünf. Bei vielen Feldern überleg, ob man es auf mehrere Aufrufe verteilt.
- „Nichts“ erlauben. Gib dem Modell unbedingt eine Möglichkeit auszudrücken, dass „diese Information im Original nicht steht“, etwa
nulloder einen leeren String, und erklär das im Prompt. Sonst beginnt das Modell zu erfinden, um die Felder zu füllen.
Übungen
- Ändere in
BugReportdas Feldmissing_infoinlist[int](absichtlich falsch definiert), führjson_output.pyaus und schau, wie Validierungsfehler und Wiederholung ablaufen. - Füg
BugReportein Feldseverityhinzu, das nur „阻塞“ (blockierend), „严重“ (schwer) oder „一般“ (normal) sein darf (Tipp:typing.Literal). Gib absichtlich eine Hilfeanfrage, bei der die Schwere nicht erkennbar ist, und schau, wie das Modell damit umgeht. - Bau mit der Methode aus
json_strict.pyein Tool für die Beitragsklassifikation aus Lektion 2, beschränk die Kategorie perenum, verarbeite 20 Beiträge und prüf, ob das Format durchweg stimmt.
Selbsttest
1. Warum braucht man trotz eingeschaltetem JSON-Modus noch eine Validierung mit Pydantic?
Der JSON-Modus sichert nur, dass die Ausgabe syntaktisch gültiges JSON ist, nicht, dass die Felder vollständig, richtig benannt und vom richtigen Typ sind. Außerdem erwähnt die DeepSeek-Dokumentation, dass die API gelegentlich leeren Inhalt zurückgibt. Die Pydantic-Validierung erkennt diese Probleme, und zusammen mit der Wiederholung werden sie behoben.
2. Warum gibt man beim erneuten Versuch nach einem Validierungsfehler auch die fehlerhafte Ausgabe des Modells zurück?
So sieht das Modell, was es beim letzten Mal ausgegeben hat, und kann zusammen mit deiner Fehlermeldung gezielt korrigieren. Stellt man nur die ursprüngliche Frage neu, weiß das Modell nicht, was falsch war, und macht wahrscheinlich denselben Fehler.
3. Welche Rolle spielt tool_choice, wenn man strukturierte Ausgaben über Tool-Aufrufe bekommt?
Es legt fest, dass das Modell ein bestimmtes Tool aufrufen muss. Ohne diese Angabe entscheidet das Modell selbst, ob es ein Tool aufruft, und antwortet vielleicht direkt mit Text; dann bekommt man keine strukturierten Parameter.
Fragen und Diskussion
Hängst du in dieser Lektion fest? Frag hier. Und wenn du die Frage von jemandem beantworten kannst, tu es gern.
Eine Frage bringt 3 Punkte, eine Antwort 6. Beiträge erscheinen nach der Prüfung.
Diskussion wird geladen…