Wenn Sie ein Modell um eine Antwort in JSON bitten, funktioniert json.loads()
meistens problemlos. Die Ausfälle sind die, die Sie eine Debugging-Sitzung
kosten: ein einzeiliger Kommentar innerhalb des Objekts, ein Markdown-Code-Fence
darum, ein Schlüssel als userEmail geschrieben, wo Ihr Code user_email
erwartet. Ein strikterer Prompt (“antworte NUR mit dem JSON-Objekt, kein
weiterer Text”) macht diese Fälle seltener. Unmöglich macht er sie nicht,
denn das Modell kann die Anweisung trotzdem ignorieren. Strukturierte
Ausgabe macht diese Anweisung überflüssig: Sie übergeben der API ein JSON
Schema, und das schränkt das Token-Sampling des Modells selbst so ein, dass
die Antwort dazu passen muss.
JSON-Mode vs. strukturierte Ausgabe: Was jedes garantiert
Der “JSON-Mode” (response_format: {"type": "json_object"} in der älteren
OpenAI-API, und das Äquivalent bei den meisten anderen Anbietern) garantiert
eine Sache: Die Ausgabe lässt sich als JSON parsen. Er sagt nichts darüber,
welche Schlüssel existieren, welchen Typ sie haben, oder ob ein Feld, auf das
Sie sich verlassen, überhaupt vorhanden ist. Ein Modell im JSON-Mode kann
{"result": "yes"} zurückgeben, wenn Ihr Code {"status": "approved", "confidence": 0.91} erwartet, und nichts zeigt Ihnen an, dass etwas
schiefgelaufen ist.
Strukturierte Ausgabe (OpenAI nennt es Structured Outputs, Anthropic nennt es JSON outputs) ist eine andere Garantie: Die Antwort entspricht dem JSON Schema, das Sie angeben, mit allen erforderlichen Schlüsseln und dem korrekten Typ für jeden. Function Calling löst ein verwandtes Problem: Das Modell bittet Ihren Code, etwas auszuführen. Die beiden werden leicht verwechselt, weil beide ein Schema übergeben. Was sich unterscheidet, ist, was das Schema formt:
| Funktion | Was sie garantiert | Wofür sie gedacht ist |
|---|---|---|
JSON-Mode (json_object) | Nur gültige JSON-Syntax | Legacy-Fallback, kein Schema durchzusetzen |
| Strukturierte Ausgabe (strict Schema) | JSON entspricht genau Ihrem Schema | Antwort für den Nutzer oder Ihren Code formen |
| Function Calling / Tool Use | Tool-Name und Argumente entsprechen dessen Schema | Das Modell entscheidet, Ihren Code aufzurufen |
Eine Produktions-Pipeline nutzt oft die letzten beiden zusammen. Strict Tool Use verhindert, dass die Funktionsaufrufe eines Agenten fehlerhaft ankommen, und ein strict Schema auf der finalen Antwort typisiert die Zusammenfassung, die an den Nutzer zurückgeht.
Wie Constrained Decoding die Übereinstimmung erzwingt
Das Modell vergleicht seine Antwort nie nach der Generierung mit Ihrem Schema. Das Schema ändert, was es generieren darf — Token für Token.
Die Serving-Schicht kompiliert Ihr JSON Schema in eine Grammatik: eine Zustandsmaschine, die an jeder Position der Ausgabe verfolgt, welche Token noch zu etwas Gültigem führen würden. Vor jedem Sampling-Schritt setzt die Serving-Schicht den Logit jedes Vokabular-Tokens, das die Grammatik an dieser Position ausschließt, auf minus unendlich — nach dem Softmax hat es dann eine Wahrscheinlichkeit von null. Das Modell sampelt aus dem, was übrig bleibt, und die relativen Wahrscheinlichkeiten unter den verbleibenden Token bleiben erhalten. Ein falsch geschriebener Schlüssel, eine schließende geschweifte Klammer am falschen Ort, ein String, wo das Schema eine Zahl verlangt: Sobald die Grammatik sie ausschließt, sind diese Token keine Option mehr.
Dieser Kompilierungsschritt hat seinen Preis. Eine Grammatik für ein neues Schema zu bauen, braucht echte Arbeit, daher ist die erste Anfrage mit diesem Schema langsamer als die folgenden. Die API von Anthropic cacht die kompilierte Grammatik 24 Stunden nach der letzten Nutzung, und spätere Aufrufe mit demselben Schema überspringen die Kompilierung.
Der Mechanismus erklärt auch die Grenze. Constrained Decoding legt nur die
Form fest, sonst nichts. Die Grammatik weiß, welche Token syntaktisch
zulässig sind, nicht, welcher davon wahr ist — das Modell kann also
weiterhin ein gültiges "email"-Feld mit einer erfundenen Adresse füllen.
So erhalten Sie schema-valides JSON von OpenAI, Claude und Ollama
Die Responses API von OpenAI nimmt das Schema in text.format, mit
aktiviertem strict:
| |
Claude nimmt das Schema in output_config.format, nicht in einer
Tool-Definition:
| |
Das JSON kommt im Textinhalt der Antwort zurück, nicht in einem Tool-Call-Block. Der alte Workaround (ein Fake-Tool definieren, das Modell zum Aufruf zwingen, die Argumente auslesen) funktioniert weiterhin bei Modellen, die nur Tool Use unterstützen. Wo native strukturierte Ausgabe existiert, brauchen Sie ihn nicht: Der Decoder erzwingt das Schema direkt, statt zu hoffen, dass die Tool-Argumente zufällig passen.
Ein Modell, das lokal mit Ollama läuft,
erhält das Schema über das Feld format von /api/chat:
| |
Der Mechanismus ist derselbe. Die llama.cpp-Runtime von Ollama baut aus
dem Schema eine Grammatik und maskiert Token genauso wie die gehosteten APIs.
Welche JSON-Schema-Funktionen unterstützen die Anbieter?
Jeder Anbieter setzt nur eine Teilmenge von JSON Schema durch, und diese Teilmengen unterscheiden sich genug, dass ein Schema, das Sie von einer API in eine andere kopieren, dort nicht mehr funktioniert.
OpenAI verlangt, dass jedes Objekt "additionalProperties": false trägt und
jede seiner Eigenschaften in required aufgeführt wird — es gibt keine
Möglichkeit, ein Feld als wirklich optional zu markieren, modellieren Sie es
also als required und nullable. Anthropic ist großzügiger: Eine Eigenschaft,
die nicht in required steht, ist wirklich optional, und required-Eigenschaften
erscheinen in der Ausgabe zuerst, gefolgt von den vorhandenen optionalen.
Anthropic unterstützt außerdem lokale $ref/$defs, anyOf und const,
lehnt aber rekursive Schemas, numerische Grenzen wie minimum und maximum
sowie Zeichenketten-Längenbeschränkungen wie maxLength ab. Die Beispiele
von OpenAI enthalten ein rekursives $ref: "#" für baumförmige Ausgaben, was
die Dokumentation von Anthropic ausdrücklich ausschließt.
Wenn Ihr Schema eine Einschränkung braucht, die der Anbieter nicht
unterstützt, verschieben Sie sie in die description des Feldes (“muss eine
positive ganze Zahl unter 100 sein”) und prüfen Sie sie selbst nach dem
Parsen. Die SDK-Schema-Helfer beider Plattformen tun das automatisch, wenn
Sie das Schema aus einem Pydantic- oder Zod-Modell bauen, statt rohes JSON
Schema von Hand zu schreiben.
So validieren Sie strukturierte Ausgabe nach dem Parsen
Eine gültige Form ist keine korrekte Antwort. Das Schema garantiert, dass
severity einer Ihrer vier Enum-Werte ist. Ob “blocking” die richtige
Einstufung für dieses Ticket ist, ist eine andere Frage. Behandeln Sie das
geparste Objekt so, wie Sie die Eingabe eines Formulars behandeln würden:
Input zum Validieren, keine gesicherte Wahrheit.
| |
Instructor umschließt OpenAI, Anthropic
und die meisten anderen Anbieter mit einer Validierungs- und
Retry-Schleife. Sie geben ihm ein Pydantic-Modell. Bei einem
Validierungsfehler gibt es den Fehler an das Modell zurück und fragt erneut,
bis zu einem Retry-Limit, das Sie festlegen, statt beim ersten Fehlschlag
einen ValidationError auszulösen. Es ist auch der praktische Weg zu einer
Schema-Garantie bei einem Anbieter oder einem älteren Modell ohne nativen
strict Mode: Die Retry-Schleife ersetzt das Enforcement auf Decoder-Ebene,
und jeder Fehlschlag kostet einen zusätzlichen Roundtrip.
Feld-Validatoren erkennen fehlerhafte Werte in einer einzelnen Antwort. Um zu prüfen, ob die Antworten über Hunderte von Eingaben hinweg korrekt sind, und ob eine Prompt-Änderung sie verschlechtert hat, brauchen Sie eine Eval Suite.
Wenn strukturierte Ausgabe trotzdem fehlschlägt
Der strict Mode deckt diese Fälle nicht ab. Prüfen Sie sie, bevor Sie einen Bug in Ihrem eigenen Code suchen:
- Refusals. Ein Modell kann die Antwort verweigern, und der
Refusal-Text entspricht nicht Ihrem Schema. Prüfen Sie
stop_reason(oder das Äquivalent) vor dem Parsen, nicht erst, wenn der Parser einen Fehler auslöst. - Trunkierung. Wird
max_tokensmitten in einem Objekt erreicht, entsteht unvollständiges JSON. Die Grammatik kontrolliert, welches Token als Nächstes kommt, nicht, wie viel Platz noch zum Abschließen bleibt. Erhöhen Sie das Limit oder verkürzen Sie das Schema. - Nicht erzwungene Pfade. JSON-Mode, eine Retry-Bibliothek auf einem
Modell ohne strict Mode, oder ein Anbieter, der ein Schema-Keyword, das er
nicht unterstützt, stillschweigend ignoriert, können alle
"Blocking"zurückgeben, wenn Ihr Enum"blocking"erwartet. Wenn eine Anfrage in Ihrem System einen dieser Pfade nimmt, normalisieren Sie die Werte beim Eingang. - Schemagröße. Ein großes Schema mit vielen
anyOf-Zweigen fügt jeder Anfrage zusätzliche Token hinzu, zusätzlich zur Kompilierungsverzögerung für ein neues Schema. Halten Sie Schemas so flach und klein, wie es die Aufgabe zulässt.
Die Garantie des Anbieters ist real, aber Ihr Parsing-Code sollte nicht davon ausgehen, dass sie gehalten hat.
JSON-Mode, strict Schema oder Function Calling: Was passt wann
Der einfache JSON-Mode eignet sich nur, wenn ein Modell oder SDK-Pfad kein Schema unterstützt — als Fallback, nicht als Standard. Ein strict JSON Schema brauchen Sie, wenn Sie eine Antwort für Ihren eigenen Code oder einen Nutzer formen: Extraktion, Klassifikation, eine typisierte Zusammenfassung. Function Calling gehört dahin, wo das Modell selbst entscheidet, ob etwas ausgeführt wird. Aktivieren Sie den strict Mode auch für diese Tool-Definition, damit ein fehlerhafter Aufruf an der API scheitert und nicht in Ihrem Code.
Wenn Sie nur ein Label aus einem kurzen Enum brauchen, vergleichen Sie auch ein System-One-Modell. Ist die Ausgabe auf eine bekannte Menge von Antworten beschränkt, kann ein Modell eine davon auswählen, statt sie zu generieren, und die Kompromisse bei Latenz und Kosten unterscheiden sich genug, dass Sie sie an Ihrem eigenen Traffic messen sollten.
Für alles andere ist die Standardwahl ein strict Schema plus ein Validator auf dem geparsten Ergebnis. Das Schema hält die Form korrekt. Zu prüfen, ob der Inhalt stimmt, bleibt Ihre Aufgabe.