Strukturierte Ausgabevalidierung von LLMs in Python, die standhält

Hören Sie auf, auf Vibes zu vertrauen. Validieren Sie Verträge.

Inhaltsverzeichnis

Die meisten Tutorials zu „strukturierten Ausgaben“ von LLMs sind wenig ernst gemeint. Sie lehren Sie, höflich um JSON zu bitten und darauf zu hoffen, dass das Modell sich entsprechend verhält. Das ist keine Validierung. Das ist Optimismus mit geschweiften Klammern.

Die eigene Dokumentation von OpenAI macht diesen Unterschied explizit. Der JSON-Modus liefert gültiges JSON, während strukturierte Ausgaben die Einhaltung des Schemas durchsetzen. OpenAI empfiehlt die Verwendung von strukturierten Ausgaben anstelle des JSON-Modus, wann immer möglich.

Infografik zur Validierung strukturierter Ausgaben

Das macht die Payload jedoch nicht vertrauenswürdig. JSON Schema definiert Struktur und erlaubte Werte, Pydantic bietet typisierte Validierung in Python, und OpenAI weist explizit darauf hin, dass eine schema-gültige Antwort dennoch inkorrekte Werte enthalten kann. Darüber hinaus können Absagen und unvollständige Ausgaben die erwartete Struktur umgehen. In der Produktion ist die Validierung strukturierter Ausgaben eine Pipeline, kein einfacher Schalter. Dieselbe Grenze muss auch im weiteren Kontext von Durchsatz, Wiederholungsversuchen und Scheduler-Limits auf dem Hub für LLM-Performance-Engineering berücksichtigt werden.

Strukturierte Ausgabevalidierung ist ein Vertrag

Strukturierte Ausgabevalidierung für LLMs bedeutet, dass Sie die Struktur der Antwort von vornherein definieren, das Modell dazu zwingen, diese Struktur (soweit möglich) zu erzeugen, und das Ergebnis erneut validieren, bevor Ihre Anwendung ihm vertraut. In der Praxis bedeutet dies, erforderliche Felder, Typen, Enums, geschlossene Objektstrukturen und Domänenregeln zu überprüfen, bevor die Payload Ihre Datenbank, Benutzeroberfläche, Warteschlange oder nachgelagerte Dienste erreicht. JSON Schema existiert genau für diese Art der strukturellen Validierung, Pydantic wurde entwickelt, um unzuverlässige Daten gegen Python-Typ-Hinweise zu validieren, und die Bibliothek jsonschema von Python bietet einen direkten Weg, eine Instanz gegen ein Schema zu validieren.

Es gibt auch eine klare Trennung zwischen zwei häufigen Anwendungsfällen. Wenn das Modell den Benutzer in einem strukturierten Format beantworten soll, verwenden Sie ein strukturiertes Antwortformat. Wenn das Modell die Tools oder Funktionen Ihrer Anwendung aufrufen soll, verwenden Sie Function Calling. Die Dokumentation von OpenAI legt diesen Unterschied dar und empfiehlt für Function Calling die Aktivierung von strict: true, damit die Argumente zuverlässig dem Funktionsschema entsprechen.

Meine feste Überzeugung ist einfach: Behandeln Sie jede strukturierte LLM-Antwort als API-Grenze. Sobald Sie in Begriffen von Verträgen statt von Prompts denken, wird die Architektur sauberer, die Fehler billiger und das Problem „warum hat das Modell in der Produktion ein neues Feld erfunden“ verschwindet größtenteils. Das ist die wahre Antwort auf „was ist strukturierte Ausgabevalidierung für LLMs“ und sie ist viel besser als „bitten Sie das Modell nett um JSON“.

JSON-Modus ist keine Validierung

Wenn Sie nur eine Sache aus diesem Artikel behalten sollten, dann diese: Der JSON-Modus ist keine Schema-Validierung. Das Help Center von OpenAI besagt, dass der JSON-Modus nicht garantiert, dass die Ausgabe einem bestimmten Schema entspricht, sondern nur, dass es sich um gültiges JSON handelt, das ohne Fehler geparst wird. Die Anleitung zu strukturierten Ausgaben sagt dasselbe, aber klarer. Sowohl der JSON-Modus als auch strukturierte Ausgaben können gültiges JSON erzeugen, aber nur strukturierte Ausgaben erzwingen die Einhaltung des Schemas.

Dieser Unterschied ist wichtiger, als viele zugeben. In seinem Launch-Post zu strukturierten Ausgaben berichtete OpenAI, dass gpt-4o-2024-08-06 mit strukturierten Ausgaben bei seinen komplexen JSON-Schema-Evaluierungen 100 Prozent erreichte, während gpt-4-0613 unter 40 Prozent lag. Sie müssen diese Zahlen nicht als universelle Wahrheit betrachten, um den größeren Punkt zu erkennen. Die Durchsetzung des Schemas verändert die Fehleroberfläche von „alles kann passieren“ zu „der Vertrag ist viel enger“.

Es gibt immer noch Randfälle, und das Gegenteil zu tun, ist der Weg, wie Demo-Apps zur Pager-Duty werden. OpenAI dokumentiert, dass das Modell eine unsichere Anfrage ablehnen kann, und diese Ablehnungen werden außerhalb Ihres normalen Schema-Pfades angezeigt. Es dokumentiert auch unvollständige Antworten, einschließlich Fällen wie dem Erreichen von max_output_tokens oder einer Unterbrechung durch einen Inhaltsfilter. Die FAQ „Ist der JSON-Modus für zuverlässige LLM-Ausgaben ausreichend?“ hat also eine kurze und eine lange Antwort. Die kurze Antwort ist nein. Die lange Antwort ist, dass selbst strikte strukturierte Ausgaben eine explizite Fehlerbehandlung benötigen.

Wo strukturierte Ausgaben dennoch versagen

Die Schema-Durchsetzung verkleinert das Problem. Sie löscht es nicht. Im echten Verkehr sehen Sie immer noch defekte oder überraschende Payloads aus Gründen, die wenig mit Ihrer Prompt-Formulierung zu tun haben.

Fehlerstrukturen, für die man entwickeln sollte

Modelle und Clients stimmen in Details nicht überein. Sie können zusätzlichen Prolog vor oder nach dem JSON erhalten, Markdown-fenced-Blöcke um die Payload herum oder einen Tool-Call, dessen Name gültig ist, dessen Argumente aber JSON sind, das nicht Ihrem Pydantic-Modell entspricht. Streaming verschärft das Problem, da Sie einen halbfertigen Puffer validieren könnten. Defensiver Code sollte „String rein, vielleicht JSON drin“ annehmen, statt „Bytes auf der Leitung entsprechen bereits meinem Modell“.

Unterschiede zwischen Anbietern und APIs

Nicht jeder Host bietet dieselbe Oberfläche für strukturierte Ausgaben. Ein Stack könnte Ihnen eine erstklassige, schema-gebundene Vollendung bieten, ein anderer garantiert möglicherweise nur JSON-Syntax, und lokale Laufzeiten könnten hinter gehosteten APIs zurückliegen. Das ist einer der Gründe, warum die FAQ „Wie validiert man LLM-JSON in Python?“ mit der Anbieterdurchsetzung beginnt, wenn diese existiert, und dennoch mit der Validierung auf der Python-Seite endet. Für einen breiteren Überblick darüber, wie sich die Anbieter vergleichen, sehen Sie den Vergleich strukturierter Ausgaben über beliebte LLM-Anbieter hinweg. Wenn Sie Modelle lokal ausführen, gilt dieselbe Validierungspipeline, nachdem Sie das Leitungsformat normalisiert haben, beispielsweise nach der Extraktion mit Ollama, wie in Strukturierte LLM-Ausgabe mit Ollama in Python und Go. Wenn eine Laufzeit JSON immer noch mit seltsamen Präfixen oder Reasoning-Traces umhüllt, erwarten Sie dieselbe Klasse von Parser-Fehlern, die in Ollama GPT-OSS Probleme mit strukturierten Ausgaben beschrieben wird.

Der Python-Stack, der tatsächlich funktioniert

Meine Empfehlung ist absichtlich langweilig. Erstens, lassen Sie den Modellanbieter den strukturellen Vertrag durchsetzen, wann immer möglich. Zweitens, validieren Sie die zurückgegebene Payload in Python mit Pydantic. Drittens, verwenden Sie explizite Validierung von Geschäftsregeln für Fakten, die ein Schema allein nicht beweisen kann. Viertens, testen Sie den Vertrag mit Fixtures und adversarischen Beispielen, anstatt auf einen Playground-Screenshot zu winken und es damit getan zu sein. Die Dokumentation von OpenAI zu strukturierten Ausgaben, das Validierungsmodell von Pydantic, die Tools von jsonschema in Python und die eigenen Evaluierungsbeispiele von OpenAI für strukturierte Ausgaben zeigen alle in diese Richtung.

Pydantic ist der richtige Schwerpunkt für Python. Es ermöglicht Ihnen, die Ausgabe als normale Python-Typen zu modellieren, JSON Schema mit model_json_schema() zu generieren und rohes JSON mit model_validate_json() zu validieren. Die Dokumentation von Pydantic merkt auch an, dass model_validate_json() im Allgemeinen der bessere Weg ist, als zuerst json.loads(...) zu verwenden und dann zu validieren, da dieser zweistufige Weg zusätzliche Parsing-Arbeit in Python hinzufügt.

Wenn Sie eigenständige Schema-Dateien in Ihrem Repository behalten oder möchten, dass CI Fixture-Payloads unabhängig vom Modellcode validiert, bietet Ihnen das Paket jsonschema von Python die einfachste mögliche Vertragsprüfung mit jsonschema.validate(...). Wenn Sie das in Pre-Commit möchten, existiert check-jsonschema speziell als CLI- und Pre-Commit-Hook, der auf jsonschema basiert. Das ist eine sehr gute Übereinstimmung für Teams, die Schema-Änderungen wie Code-Änderungen reviewed haben möchten.

Frameworks können die Verkabelung reduzieren, beseitigen aber nicht die Notwendigkeit tatsächlicher Validierung. LangChain wählt jetzt automatisch native strukturierte Ausgaben des Anbieters aus, wenn der Anbieter sie unterstützt, und fällt sonst auf eine Tool-Strategie zurück. Instructor legt Pydantic-Antwortmodelle, Validierung, Wiederholungsversuche und Multi-Provider-Support über Modellaufrufe hinweg. Guardrails konzentriert sich auf Validatoren und Input-Output-Guard-Schichten. Nützliche Tools, alle. Aber das Schema und die Geschäftsregeln gehören immer noch Ihnen. Wenn Sie zwischen höherwertigen Bibliotheken wählen, ist der Vergleich von BAML vs. Instructor für Python eine nützliche Ergänzung zu diesem Artikel.

Ein minimales Beispiel mit OpenAI und Pydantic

Das kleinste produktionsreife Beispiel hat einige nicht verhandelbare Punkte. Verwenden Sie nach Möglichkeit eine geschlossene Menge von enum-ähnlichen Werten. Verbieten Sie zusätzliche Schlüssel. Fügen Sie Felddescriptions hinzu, damit das Schema für Menschen verständlich und für das Modell lesbarer ist. Halten Sie das Root-Objekt explizit und langweilig. OpenAI empfiehlt klare Namen plus Titel und Beschreibungen für wichtige Schlüssel, JSON Schema verwendet enum, um Werte zu beschränken, und Pydantic kann die Objektstruktur mit extra="forbid" schließen.

from typing import Literal

from openai import OpenAI
from pydantic import BaseModel, ConfigDict, Field


class TicketClassification(BaseModel):
    model_config = ConfigDict(extra="forbid")

    category: Literal["billing", "bug", "how_to", "abuse"] = Field(
        description="Kategorie des Support-Tickets."
    )
    priority: Literal["low", "medium", "high"] = Field(
        description="Operative Dringlichkeit."
    )
    needs_human: bool = Field(
        description="Ob ein Mensch den Fall überprüfen sollte."
    )
    summary: str = Field(
        description="Eine einsatzige Zusammenfassung des Problems."
    )


client = OpenAI()

response = client.responses.parse(
    model="gpt-4o-2024-08-06",
    input=[
        {
            "role": "system",
            "content": "Klassifizieren Sie Support-Tickets. Geben Sie nur das strukturierte Ergebnis zurück.",
        },
        {
            "role": "user",
            "content": "Kunde meldet doppelte Belastungen nach dem Aktualisieren des Checkouts.",
        },
    ],
    text_format=TicketClassification,
)

result = response.output_parsed
print(result.model_dump())

Zwei Details in diesem Beispiel sind leicht zu übersehen und absolut wertvoll. extra="forbid" auf der Pydantic-Seite spiegelt die JSON-Schema-Idee von additionalProperties: false wider, was auch eine Anforderung für strikte Tool-Schemas in den OpenAI-Dokumenten zu Function Calling ist. Und Enums sind nicht nur dekorativ. Sie sind einer der einfachsten Wege, um das Modell davon abzuhalten, einen Wert zu erfinden, den Ihr Code nicht versteht.

Das OpenAI Python SDK unterstützt client.responses.parse(...) mit einem Pydantic-Modell, das als text_format bereitgestellt wird, und das geparste Objekt wird auf response.output_parsed zurückgegeben. Dasselbe SDK unterstützt auch client.chat.completions.parse(...), wobei das geparste Objekt auf message.parsed liegt. Wenn Sie direkte strukturierte Datenauswertung mit minimalem Klebstoff wünschen, sind diese Helfer der sauberste Ausgangspunkt.

Parsen, normalisieren, dann validieren

Strukturierte Ausgaben und model_validate_json entfernen viel Parsing-Schmerz, wenn der Stack von Anfang bis Ende ausgerichtet ist. In dem Moment, in dem Sie einen Anbieter unterstützen, der reinen Chat-Text zurückgibt, ein Modell, das JSON in Zäunen umhüllt, oder einen Logging-Pfad, der die rohe Vollendungszeichenkette speichert, möchten Sie einen Engpass, der Text in ein Dict umwandelt, bevor Pydantic läuft.

import json


def parse_json_from_llm_text(text: str) -> dict:
    cleaned = text.strip()
    if cleaned.startswith("```"):
        cleaned = cleaned.split("\n", 1)[1]
        cleaned = cleaned.rsplit("```", 1)[0].strip()

    # Übliches „Sicher, hier ist das JSON:“-Präfix vor dem Objekt.
    if not cleaned.startswith("{") and "{" in cleaned and "}" in cleaned:
        start = cleaned.find("{")
        end = cleaned.rfind("}")
        if end > start:
            cleaned = cleaned[start : end + 1]

    return json.loads(cleaned)


ticket_dict = parse_json_from_llm_text(raw_completion_text)
ticket = TicketClassification.model_validate(ticket_dict)

Dieser Helfer ist absichtlich langweilig. Er behandelt gefencte „json ... “-Blöcke und eine führende natürliche Sprach-Einleitung, wenn die Payload immer noch ein einzelnes Top-Level-Objekt ist. Es ist kein vollständiger JSON-Extraktor. Wenn das Modell Klammern innerhalb von String-Werten verschachtelt, kann naives Slicing brechen, und die richtige Lösung ist in der Regel strikteres Prompting, schema-gebundene Vollendungen oder eine dedizierte Parser-Bibliothek.

Streaming-Vollendungen

Wenn Sie Chat-Token streamen, führen Sie nicht json.loads oder model_validate_json auf jedes Delta aus. Puffern Sie, bis die API eine abgeschlossene Meldung meldet (prüfen Sie Ihren Client auf Stream-Termination oder finish_reason), konkatenieren Sie den Text und parsen Sie einmal. Dieselbe Regel gilt, wenn Tool-Call-Argumente in Chunks eintreffen. Sie validieren erst, wenn die Argument-Zeichenkette vollständig ist.

chunks: list[str] = []
for chunk in completion_stream:
    delta = chunk.choices[0].delta.content or ""
    chunks.append(delta)
raw_completion_text = "".join(chunks)
ticket = TicketClassification.model_validate_json(raw_completion_text)

Sie können raw_completion_text zuerst durch parse_json_from_llm_text schicken, wenn Sie Zäune oder Plauderei um das JSON herum erwarten.

Sobald Sie das Parsing von Plain-Strings übernehmen, ist die nächste Einschränkung oft nicht Python, sondern der JSON-Schema-Dialekt des Anbieters und was die entfernte API tatsächlich akzeptiert.

Anbieter-Schema-Limits (bevor Sie es clever in Python versuchen)

Schütten Sie nicht blind jede Ausgabe eines Schema-Generators in eine API und nehmen Sie an, jedes JSON-Schema-Feature wird unterstützt. OpenAI unterstützt eine Teilmenge von JSON Schema, erfordert, dass alle Felder für Strukturierte Ausgaben erforderlich sind, erfordert, dass das Root-Element ein Objekt ist und keine Top-Level-anyOf, und dokumentiert Limits für die Nesting-Tiefe und die Gesamtanzahl der Eigenschaften. Halten Sie das an den Anbieter gerichtete Schema einfach. Das ist kein Kompromiss. Das ist gutes Engineering.

Wenn Sie einen anbieterunabhängigen Validierungspfad benötigen oder gespeicherte Fixtures und Mocks validieren möchten, ist Pydantic plus jsonschema nach wie vor eine großartige Kombination.

from jsonschema import validate as validate_json

schema = TicketClassification.model_json_schema()

payload = {
    "category": "bug",
    "priority": "high",
    "needs_human": True,
    "summary": "Checkout dupliziert Belastungen nach Aktualisierung.",
}

validate_json(instance=payload, schema=schema)
ticket = TicketClassification.model_validate(payload)
print(ticket)

Dieses Muster ist besonders nützlich in Tests, Vertrags-Fixtures und Integrationen, in denen der Modellanbieter keine native Durchsetzung strukturierter Ausgaben bietet. Denken Sie jedoch daran, dass ein lokal generiertes Schema breiter sein kann als die unterstützte Teilmenge eines bestimmten Anbieters, sodass „lokal gültig“ nicht automatisch „von jeder LLM-API akzeptiert“ bedeutet. Beachten Sie auch, dass einige Anbieter Schema-Artefakte vorverarbeiten und zwischenspeichern, sodass die erste Anfrage für ein neues Schema langsamer sein kann als warme Anfragen.

Tool-Calls sind ein zweiter Vertrag

Function- oder Tool-Calling ist die andere große Struktur für strukturierte Ausgaben. Das Modell wählt einen Namen und übergibt Argumente, die einem von Ihnen kontrollierten JSON Schema entsprechen sollten. OpenAI empfiehlt strict: true bei Tool-Definitionen, damit die Argumente mit diesem Schema übereinstimmen. In agentenlastigen Stacks führt schlechtes Sampling schnell zu ungültigem Tool-JSON; halten Sie Sampler-Einstellungen mit mehrstufiger Arbeit synchron, wie in der Referenz für agentische Inferenzparameter für Qwen und Gemma.

Die unten stehenden Snippets gehen davon aus, dass Sie das Tool-Call-Objekt des Anbieters bereits in einen name-String und ein arguments-Dict abgebildet haben, beispielsweise durch Parsen von tool_calls[].function bei Chat-Vollendungen (JSON-String-Argumente werden zuerst mit json.loads geparst). dispatch_tool ist der Schritt nach dieser Normalisierung.

Zwei praktische Regeln helfen in Python. Erstens, validieren Sie den Tool-Namen gegen eine explizite Allowlist, bevor Sie die Ausführung weiterleiten. Zweitens, validieren Sie das Argumente-Dict mit demselben Pydantic-Modell, das Sie in Tests verwenden, nicht mit ad-hoc-Schlüsselzugriffen. Das Fehlermodell, das Sie vermeiden, ist „gültige JSON-Argumente, falsche Struktur für das ausgelöste Tool“, das an String-Checks vorbeischlüpft.

from typing import Any, Callable

from pydantic import BaseModel

ToolHandler = Callable[[dict[str, Any]], str]


def dispatch_tool(
    *,
    name: str,
    arguments: dict[str, Any],
    handlers: dict[str, tuple[type[BaseModel], ToolHandler]],
) -> str:
    if name not in handlers:
        raise ValueError(f"nicht unterstütztes Tool {name}")
    model_cls, handler = handlers[name]
    validated = model_cls.model_validate(arguments)
    return handler(validated.model_dump())


handlers: dict[str, tuple[type[BaseModel], ToolHandler]] = {
    "classify_ticket": (
        TicketClassification,
        lambda data: f"als {data['category']} in die Warteschlange gestellt",
    ),
}

Dieses Muster hält Routing und Validierung an einem Ort. Ihre echten Handler werden reicher sein, aber die Trennung sollte gleich bleiben: erlaubte Namen, typisierte Argumente, dann Seiteneffekte.

Schema-Validierung benötigt dennoch Geschäftsregeln

Ein gültiges Objekt ist nicht dasselbe wie ein korrektes Objekt. OpenAI sagt dies direkt. Strukturierte Ausgaben verhindern keine Fehler innerhalb der Werte des JSON-Objekts. Deshalb hat die FAQ „Warum sind sowohl Schema-Validierung als auch Validierung von Geschäftsregeln wichtig?“ eine knappe Antwort. Weil eine Antwort dem Schema perfekt entsprechen kann und dennoch auf eine Weise falsch sein kann, die dem Unternehmen schadet.

Hier ist ein realistisches Beispiel. Die Struktur kann gültig sein, aber die Preislogik kann dennoch Unsinn sein.

from decimal import Decimal
from typing import Literal
from typing_extensions import Self

from pydantic import BaseModel, ConfigDict, Field, model_validator


class Offer(BaseModel):
    model_config = ConfigDict(extra="forbid")

    currency: Literal["USD", "EUR", "GBP"]
    amount: Decimal = Field(gt=0)
    original_amount: Decimal | None
    discounted: bool

    @model_validator(mode="after")
    def check_discount_logic(self) -> Self:
        if self.discounted:
            if self.original_amount is None:
                raise ValueError(
                    "original_amount ist erforderlich, wenn discounted wahr ist"
                )
            if self.original_amount <= self.amount:
                raise ValueError(
                    "original_amount muss größer als amount sein"
                )
        return self

Dieser Validator macht etwas, das Schemas allein in echten Systemen oft schlecht können. Er prüft Cross-Field-Semantiken, nachdem das gesamte Modell geparst wurde. Pydantics model_validator existiert genau für diese Art der Validierung des gesamten Objekts. Beachten Sie das Feld Decimal | None ohne Standardwert. Das hält das Feld vorhanden, ermöglicht aber dennoch null, was dem von OpenAI dokumentierten Muster für optionale Werte unter strikten Strukturierten Ausgaben entspricht.

Wenn Sie möchten, dass Validierungsfehler automatisch an das Modell zurückgemeldet werden, ist Instructor eine praktische Schicht über Pydantic. Seine Dokumentation beschreibt eine Retry-Schleife, in der Validierungsfehler erfasst, als Feedback formatiert und verwendet werden, um das Modell erneut zu bitten, es zu versuchen.

import instructor

retrying_client = instructor.from_provider("openai/gpt-4o", max_retries=2)

offer = retrying_client.create(
    response_model=Offer,
    messages=[
        {
            "role": "user",
            "content": (
                "Extrahieren Sie das Angebot aus diesem Text. "
                "War 49,00 USD, jetzt 19,00 USD."
            ),
        }
    ],
)

Dies ist eines der wenigen Komfortfeatures, die ich gerne empfehle. Automatische Wiederholungsversuche, die mit echten Validierungsfehlern verknüpft sind, sind nützlich. Stille Typumwandlung ist es nicht. Die Modell-Schicht von Instructor, die Retry-Dokumentation und die Validierungsdokumentation stützen sich alle auf diese Idee, und das tun sie zu Recht.

Sie können dieselbe Idee ohne ein Framework implementieren. Die Schleife ist klein. Fragen Sie das Modell, validieren Sie mit Pydantic, und wenn die Validierung fehlschlägt, senden Sie die Fehlerdetails in einer nachfolgenden Benutzermeldung zurück und bitten Sie nur um korrigiertes JSON. Begrenzen Sie Versuche, protokollieren Sie den endgültigen Fehler und melden Sie einen kontrollierten Fehler an die Aufrufer. Wenn Sie bereits auf responses.parse oder andere schema-gebundene Helfer angewiesen sind, werden Sie diesen Pfad wahrscheinlich selten nutzen. Er ist dennoch wichtig für den JSON-Modus, ältere Chat-Endpunkte oder jedes Gateway, das Ihnen eine rohe Zeichenkette übergibt.

from openai import OpenAI
from pydantic import ValidationError

client = OpenAI()

messages = [
    {"role": "system", "content": "Geben Sie nur JSON zurück, das dem Ticket-Schema entspricht."},
    {"role": "user", "content": "Kunde meldet doppelte Belastungen nach dem Aktualisieren des Checkouts."},
]

ticket: TicketClassification | None = None
for attempt in range(2):
    completion = client.chat.completions.create(
        model="gpt-4o-2024-08-06",
        messages=messages,
        response_format={"type": "json_object"},
    )
    raw_text = completion.choices[0].message.content or ""
    try:
        ticket = TicketClassification.model_validate_json(raw_text)
        break
    except ValidationError as exc:
        messages.append(
            {
                "role": "user",
                "content": f"Validierung fehlgeschlagen mit {exc.errors()}. Geben Sie nur korrigiertes JSON zurück.",
            }
        )
else:
    raise RuntimeError("strukturierte Ausgabe-Wiederholungsversuche erschöpft")

assert ticket is not None

In echten Diensten würden Sie Tracing-IDs anhängen, Kundentext in Logs redigieren und zwischen wiederherstellbaren Validierungsfehlern und Absagen oder unvollständigen Antworten unterscheiden. Der wichtige Teil ist, dass der Wiederholungsversuch durch echte Validator-Ausgabe angetrieben wird, nicht durch eine generische „Versuchen Sie es erneut“-Meldung.

Testen, wiederholen und geschlossen versagen

Was sollte passieren, wenn die LLM-Validierung fehlschlägt? Nicht ein Schultern. Verwerfen Sie die Payload, protokollieren Sie den Fehler, wiederholen Sie mit begrenzten Versuchen, wenn die Aufgabe es wert ist, und versagen Sie geschlossen, anstatt Müll in etwas zu normalisieren, das nur akzeptabel aussieht. Hier vergessen viele Teams auch, Absagen und unvollständige Ausgaben explizit zu behandeln, obwohl die Anbieterdokumente ihnen sagen, dass diese Pfade existieren.

Für die OpenAI Responses API sollte die Fehlerbehandlung erstklassiger Code sein, kein nachträglicher Gedanke. Die Variable ist response von client.responses.create oder parse, nicht completion aus dem Chat-Streaming an anderer Stelle in diesem Artikel.

if response.status == "incomplete":
    raise RuntimeError(response.incomplete_details.reason)

content = response.output[0].content[0]

if content.type == "refusal":
    raise RuntimeError(content.refusal)

Das ist keine defensive Über-Engineering. Es ist direkt mit den dokumentierten Fehlermodi ausgerichtet. Wenn das Modell ablehnt, halten Sie keine schema-gültige Payload. Wenn die Antwort unvollständig ist, halten Sie keine schema-gültige Payload. Behandeln Sie beide als explizite Verzweigungen in Ihrer Steuerung.

Sie sollten den Vertrag auch außerhalb des Modellaufrufs selbst testen.

import pytest
from jsonschema import validate as validate_json
from pydantic import ValidationError


def test_ticket_fixture_matches_schema():
    payload = {
        "category": "bug",
        "priority": "high",
        "needs_human": True,
        "summary": "Checkout dupliziert Belastungen nach Aktualisierung.",
    }
    validate_json(instance=payload, schema=TicketClassification.model_json_schema())


def test_discount_logic_rejects_broken_offer():
    with pytest.raises(ValidationError):
        Offer.model_validate(
            {
                "currency": "USD",
                "amount": "19.00",
                "original_amount": "10.00",
                "discounted": True,
            }
        )


def test_ticket_rejects_unknown_category_string():
    with pytest.raises(ValidationError):
        TicketClassification.model_validate(
            {
                "category": "refund",
                "priority": "high",
                "needs_human": True,
                "summary": "Kunde möchte eine Rückerstattung.",
            }
        )


def test_ticket_rejects_extra_keys():
    with pytest.raises(ValidationError):
        TicketClassification.model_validate(
            {
                "category": "bug",
                "priority": "high",
                "needs_human": True,
                "summary": "Unterbrochener Workflow.",
                "severity": "critical",
            }
        )

Das ist die richtige Form der Teststrategie für die Validierung von LLM-Ausgaben in Python. Validieren Sie goldene Fixtures mit jsonschema, damit jedes Feld im Vertrag ausgeübt wird. Validieren Sie Semantik mit Pydantic und fügen Sie dann adversarische Fälle wie illegale Enum-Strings, verbotene zusätzliche Schlüssel und Cross-Field-Widersprüche hinzu, die Sie interessieren. Wenn Sie echte Modelloutputs snapshoten, bereinigen Sie PII und behandeln Sie sie als Regressions-Fixtures.

Wenn Ihr Team im OpenAI-Stack lebt, enthält die Evals API auch Rezepte zur Evaluierung strukturierter Ausgaben, die speziell zum Testen und Iterieren von Aufgaben gedacht sind, die von maschinenlesbaren Formaten abhängen. Und wenn Sie rohe Schema-Dateien im Repository behalten, verkabeln Sie check-jsonschema in CI oder Pre-Commit. Versenden Sie Verträge, keine Vibes.

Produktionsprüfungen, die Sie später retten

Wenn die Validierung fehlschlägt, ist die FAQ-Antwort knapp. Verwerfen Sie die Payload, protokollieren Sie warum, wiederholen Sie mit gezieltem Feedback, wenn die Aufgabe einen weiteren Versuch wert ist, und versagen Sie geschlossen, anstatt schlechte Daten in eine Warteschlange zu zwingen.

Eine kurze Operations-Checkliste hilft Teams, wiederkehrende Vorfälle zu vermeiden.

  • Protokollieren Sie die Schema-Version oder einen Hash des JSON Schemas, das Sie an den Anbieter gesendet haben, damit Sie Fehler genau nachvollziehen können.
  • Redigieren Sie Modell-Inputs und Outputs in Logs. Strukturierte Logs sind nutzlos, wenn sie Kundentext lecken.
  • Emittieren Sie Zähler oder Metriken für die Ablehnungsrate, die Rate unvollständiger Antworten, die Validierungsfehlerrate und die Reparaturerfolgsrate. Spitzen dort schlagen das Raten, wann ein Modell- oder Prompt-Change ausgeliefert wurde.

Umfassendere Observability für LLM-Systeme-Anleitungen helfen, diese Signale in Dashboards, Traces und SLO-Reviews einzubinden, sobald die Zähler existieren.

Die beste Praxis ist nicht kompliziert. Verwenden Sie provider-seitige Strukturierte Ausgaben oder strikte Tool-Schemas, wann immer Sie können. Normalisieren Sie rohen Text, wenn Sie müssen. Spiegeln Sie den Vertrag in Python mit Pydantic. Fügen Sie Validierung von Geschäftsregeln für das hinzu, was das Schema nicht beweisen kann. Behandeln Sie Absagen und unvollständige Antworten als normale Verzweigungen. Testen Sie den Vertrag, bis er aufhört, eine Demo zu sein, und beginnt, Software zu sein. Alles andere ist nur Prompt-Engineering-Cosplay.

Abonnieren

Neue Beiträge zu Systemen, Infrastruktur und KI-Engineering.