Zum Hauptinhalt springen
Lektion 11 von 12 · Strukturierte Ausgabe: JSON und Schemata

Validierung und Retry-Strategien

Lektion 11 von 12 Aktualisiert am 2. November 2026 Entwickler
Auf dieser Seite
  1. Validierung: Die letzte Verteidigungslinie vor dem Produktivbetrieb
  2. Geschäftsregeln im Schema verankern
  3. Retry mit Feedback
  4. Eine Kaskade aus Fallback-Plänen
  5. Den Wasserhahn rechtzeitig abdrehen
  6. Sichtbar machen
  7. Kernaussagen

Validierung: Die letzte Verteidigungslinie vor dem Produktivbetrieb

Selbst bei Verwendung von benutzerdefinierten strukturierten Ausgaben bleibt die Validierung unerlässlich. Das Schema garantiert die Struktur, nicht jedoch die semantische Kohärenz. Ein Feld price könnte beispielsweise -999.0 enthalten: ein gültiger Float, der das Schema perfekt erfüllt, aber für einen Preis keinen Sinn ergibt.

Der Unterschied ist klar. Pydantic und Zod übernehmen die strukturelle Validierung automatisch: erforderliche Felder, Datentypen, Enum-Beschränkungen und verschachtelte Strukturen. Sie müssen hierfür keinen Code schreiben. Die semantische Validierung liegt jedoch vollständig in Ihrer Verantwortung. Dazu gehören numerische Grenzen (ein Preis größer als null, eine Bewertung zwischen 0 und 1), die Konsistenz zwischen Feldern (die Summe muss der Summe der Positionen entsprechen), String-Formate (eine wirklich gültige E-Mail-Adresse, ein korrekt formatiertes Datum) und Geschäftslogik (ein Fälligkeitsdatum, das nach dem Rechnungsdatum liegt).

Geschäftsregeln im Schema verankern

Pydantic ermöglicht es, diese Prüfungen direkt in das Modell zu integrieren, sodass sie untrennbar mit der Struktur verbunden sind. @field_validator übernimmt die Prüfung einzelner Felder – beispielsweise die Sicherstellung, dass eine Menge positiv ist oder der Einheitspreis nicht negativ. @model_validator(mode="after") wird ausgeführt, sobald alle Felder besetzt sind, und ermöglicht Prüfungen über mehrere Felder hinweg, wie die Überprüfung, ob die angegebene Summe der Menge mal Einheitspreis innerhalb von einem Cent entspricht.

from pydantic import BaseModel, Field, field_validator, model_validator

class InvoiceLine(BaseModel):
    description: str
    quantity: float
    unit_price: float
    total: float

    @field_validator("quantity")
    @classmethod
    def quantity_positive(cls, v):
        if v <= 0:
            raise ValueError(f"Menge muss positiv sein, erhalten: {v}")
        return v

    @field_validator("unit_price")
    @classmethod
    def price_non_negative(cls, v):
        if v < 0:
            raise ValueError(f"Einheitspreis kann nicht negativ sein: {v}")
        return v

    @model_validator(mode="after")
    def check_total(self):
        expected = round(self.quantity * self.unit_price, 2)
        if abs(self.total - expected) > 0.01:
            raise ValueError(
                f"Inkonsistente Summe: {self.total} != {self.quantity} x {self.unit_price} = {expected}"
            )
        return self

Zod drückt dieselbe Logik aus, indem es eingebaute Einschränkungen wie .positive() und .nonnegative() für Felder sowie .refine() für Regeln über das gesamte Objekt hinweg verwendet.

import { z } from "zod";

const InvoiceLineSchema = z
  .object({
    description: z.string(),
    quantity: z.number().positive("Menge muss positiv sein"),
    unitPrice: z.number().nonnegative("Preis kann nicht negativ sein"),
    total: z.number(),
  })
  .refine(
    (data) => Math.abs(data.total - data.quantity * data.unitPrice) < 0.01,
    { message: "Summe entspricht nicht Menge x Einheitspreis" }
  );

Retry mit Feedback

Wenn die Validierung fehlschlägt, ist das effektivste Muster, den Fehler an das Modell zurückzusenden, damit es sich selbst korrigieren kann. Die folgende Funktion verknüpft drei Prüfungen in Reihenfolge ihrer Wichtigkeit: vollständige Antwort, nicht-null-Objekt und Einhaltung der Geschäftsregeln. Bei einem Fehler fügt sie die fehlerhafte Antwort und die Fehlermeldung zum Gespräch hinzu, wodurch der nächsten Versuchsversuch präziser diagnostischer Kontext zur Verfügung steht. Beachten Sie die Kopie der Nachrichtenliste am Anfang der Funktion – ohne diese würden Sie die Gesprächshistorie des Aufrufers verseuchen.

from mistralai.client import Mistral
from pydantic import BaseModel, ValidationError
import json

def extract_with_retry(
    client: Mistral,
    messages: list,
    schema,
    max_retries: int = 3,
    model: str = "mistral-large-latest"
):
    """Extraktion mit Retry und strukturiertem Fehlerfeedback."""
    conversation = list(messages)  # Kopie erstellen, um das Original nicht zu verändern

    for attempt in range(max_retries):
        try:
            response = client.chat.parse(
                model=model,
                messages=conversation,
                response_format=schema,
                temperature=0,
                max_tokens=2048
            )

            # Prüfen, ob die Antwort vollständig ist
            if response.choices[0].finish_reason != "stop":
                raise ValueError(
                    f"Unvollständige Antwort (finish_reason={response.choices[0].finish_reason})"
                )

            parsed = response.choices[0].message.parsed
            if parsed is None:
                raise ValueError("Geparsedte Antwort ist None")

            # Zusätzliche semantische Validierung
            validate_business_rules(parsed)

            return parsed

        except (ValidationError, ValueError) as e:
            error_msg = str(e)
            print(f"Versuch {attempt + 1}/{max_retries} fehlgeschlagen: {error_msg}")

            if attempt < max_retries - 1:
                # Feedback zum Kontext hinzufügen
                raw = response.choices[0].message.content if response else ""
                conversation.append({"role": "assistant", "content": raw})
                conversation.append({
                    "role": "user",
                    "content": f"Ihre Antwort enthält einen Fehler: {error_msg}. Bitte korrigieren Sie diese."
                })
            else:
                raise RuntimeError(
                    f"Fehlschlag nach {max_retries} Versuchen. Letzter Fehler: {error_msg}"
                )

Eine Kaskade aus Fallback-Plänen

Retry löst nicht alles: manchmal ist ein Dokument für das Schema zu untypisch. Anstatt abrupt zu versagen, degradieren Sie sanft. Der erste Versuch nutzt benutzerdefinierte strukturierte Ausgaben, die zuverlässigste Option. Scheitert dieser, wechseln Sie auf JSON Mode, der toleranter ist, und leiten Sie die Ausgabe dann durch model_validate() weiter – dies behält die Schema-Einschränkungen bei, ohne darauf angewiesen zu sein, dass das Modell sie perfekt generiert. Als letzter Rückgriff gibt die Funktion None zurück und überlässt es dem Aufrufer, zwischen manueller Warteschlangeneintragung oder Aufgabe zu entscheiden.

def extract_with_fallback(client, text, schema, system_prompt):
    """Extraktion mit Fallback-Kaskade."""

    # Versuch 1: Benutzerdefinierte strukturierte Ausgabe (bestes Ergebnis)
    try:
        return extract_with_retry(
            client,
            [
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": text}
            ],
            schema,
            max_retries=2,
            model="mistral-large-latest"
        )
    except RuntimeError:
        print("Benutzerdefiniertes Parsing fehlgeschlagen, Wechsel auf JSON Mode")

    # Versuch 2: JSON Mode (flexibler)
    try:
        response = client.chat.complete(
            model="mistral-large-latest",
            messages=[
                {"role": "system", "content": system_prompt + "\nGeben Sie gültiges JSON zurück."},
                {"role": "user", "content": text}
            ],
            response_format={"type": "json_object"},
            temperature=0
        )
        raw = json.loads(response.choices[0].message.content)
        return schema.model_validate(raw)  # Pydantic-Validierung versuchen
    except Exception:
        print("JSON Mode fehlgeschlagen, Wechsel auf manuelle Extraktion")

    # Versuch 3: Teilobjekt oder Fehler zurückgeben
    return None

Den Wasserhahn rechtzeitig abdrehen

In einem Hochfrequenz-Pipeline verwandelt eine API-Ausfall Retry in einen Verstärker: jedes Dokument schlägt dreimal fehl, die Rechnung verdreifacht sich und die Latenz steigt. Ein Circuit Breaker überwacht Fehler in einem gleitenden Fenster und blockiert Aufrufe, sobald ein Schwellenwert überschritten wird – fünf Fehler in 60 Sekunden in der hier gezeigten Konfiguration. Ein Erfolg schließt den Circuit und setzt den Zähler zurück.

from collections import deque
from time import time

class CircuitBreaker:
    def __init__(self, failure_threshold=5, window_seconds=60):
        self.failures = deque()
        self.threshold = failure_threshold
        self.window = window_seconds
        self.is_open = False

    def record_failure(self):
        now = time()
        self.failures.append(now)
        # Fehler außerhalb des Fensters bereinigen
        while self.failures and self.failures[0] < now - self.window:
            self.failures.popleft()
        if len(self.failures) >= self.threshold:
            self.is_open = True

    def record_success(self):
        self.is_open = False
        self.failures.clear()

    def can_proceed(self) -> bool:
        if not self.is_open:
            return True
        # Prüfen, ob das Fenster abgelaufen ist
        now = time()
        while self.failures and self.failures[0] < now - self.window:
            self.failures.popleft()
        if len(self.failures) < self.threshold:
            self.is_open = False
            return True
        return False

# Verwendung
breaker = CircuitBreaker(failure_threshold=5, window_seconds=60)

def safe_extract(client, text, schema):
    if not breaker.can_proceed():
        raise RuntimeError("Circuit offen – zu viele kürzliche Fehler")
    try:
        result = extract_with_retry(client, [...], schema)
        breaker.record_success()
        return result
    except RuntimeError:
        breaker.record_failure()
        raise

Sichtbar machen

Alle diese Mechanismen bleiben blind ohne Spuren. Protokollieren Sie jeden Aufruf mit seiner Dauer und dem beteiligten Schema, unabhängig davon, ob er erfolgreich war oder nicht. Diese Daten ermöglichen es Ihnen später zu bestimmen, ob die Degradation auf ein Modellproblem, eine Dokumentart oder eine Prompt-Änderung zurückzuführen ist.

import logging
import time

logger = logging.getLogger("structured_output")

def monitored_extract(client, text, schema, **kwargs):
    start = time.time()
    try:
        result = extract_with_retry(client, [...], schema, **kwargs)
        duration = time.time() - start
        logger.info(f"Extraktion erfolgreich in {duration:.2f}s — schema={schema.__name__}")
        return result
    except Exception as e:
        duration = time.time() - start
        logger.error(f"Extraktion fehlgeschlagen in {duration:.2f}s — schema={schema.__name__} — {e}")
        raise

Kernaussagen

  • Strukturelle Validierung (Typen) ist automatisch; semantische Validierung (Logik) liegt in Ihrer Verantwortung
  • Retry mit Feedback ist das effektivste Muster: Senden Sie den Fehler an das Modell zurück
  • Implementieren Sie eine Fallback-Kaskade: Benutzerdefiniert > JSON Mode > Standardwert
  • Verwenden Sie in hochfrequenten Produktivsystemen einen Circuit Breaker, um Fehlerkaskaden zu verhindern
  • Protokollieren Sie jeden Aufruf zur Überwachung und Diagnose