Zum Inhalt springen
Projekt besprechen

6. Oktober 2026

Dein JSON-Schema ist valide. Die Antwort ist trotzdem falsch.

AILLMTesting

Struktur ist nicht Semantik

Structured Output ist für LLM-Integrationen ein wichtiger Fortschritt. Ein JSON Schema LLM-Output kann Felder erzwingen, Typen begrenzen und mit additionalProperties: false unerwartete Zusatzfelder verhindern. Mit strict: true wirkt die Schnittstelle damit beinahe wie ein normaler API-Vertrag.

Der entscheidende Unterschied bleibt jedoch: Das Schema beschreibt die Form der Antwort. Es beschreibt nicht zuverlässig, ob die Antwort die fachliche Regel erfüllt.

Ein reales Fehlermuster zeigt das deutlich. Das Schema wurde exakt eingehalten: strict: true, keine zusätzlichen Properties, valides JSON, und der Request endete mit finish_reason stop. Trotzdem war das Ergebnis inhaltlich falsch. Erwartet war ein Eintrag pro angefordertem Element. Das Modell lieferte stattdessen eine flache Liste mit acht oder neun Einträgen, obwohl die geforderte Struktur eine andere Zuordnung verlangte.

Der Parser hatte nichts zu beanstanden. Das JSON Schema ebenfalls nicht. Für die nachgelagerte Anwendung war die Antwort dennoch nicht verwendbar.

Randwerte machen den Fehler sichtbar

Besonders aufschlussreich waren die Eingabemengen. Bei einer Anzahl von zwei kam nur ein Ergebnis zurück. Bei sechs kamen korrekt sechs Ergebnisse zurück.

Das ist genau die Art Fehler, die bei einem Test mit einem einzigen Standardfall unsichtbar bleibt. Wer nur prüft, ob ein Feld items ein Array enthält und jedes Element die erwarteten Properties hat, testet lediglich die technische Hülle.

Die fachliche Invariante lautet hier dagegen sinngemäß:

  • Für jedes angeforderte Element existiert genau ein Ergebnis.
  • Jedes Ergebnis ist dem richtigen Eingangselement zugeordnet.
  • Es gibt keine fehlenden, doppelten oder zusätzlichen Ergebnisse.

Keine dieser Regeln folgt automatisch aus einem Schema, das nur die Form eines einzelnen Listeneintrags kennt.

Warum handgeschriebene Fixtures nicht reichen

Handgeschriebene Test-Fixtures sind sinnvoll, aber sie haben eine klare Grenze: Sie bilden meistens ab, was wir vom Modell erwarten. Nicht, was das Modell tatsächlich liefert.

Ein typisches Fixture könnte etwa so aussehen:

{
  "items": [
    {
      "sourceId": "a",
      "result": "..."
    },
    {
      "sourceId": "b",
      "result": "..."
    }
  ]
}

Damit lässt sich gut testen, ob die eigene Verarbeitung funktioniert. Es testet aber nicht, ob das LLM bei zwei, sechs oder anderen Randwerten dieselbe fachliche Beziehung einhält.

Mein Rat ist daher: Jeden LLM-Call mindestens einmal echt ausführen und die Antwort aufzeichnen. Das VCR-Prinzip ist hier praktisch: Die echte Antwort wird als Testartefakt gespeichert und später reproduzierbar gegen die eigene Verarbeitung getestet. So landen nicht nur ideale Antworten im Testbestand, sondern auch Antworten mit den Eigenheiten des Modells.

Semantische Regeln im Code fail-closed prüfen

Die LLM Validierung braucht zwei Stufen. Zuerst wird die Struktur validiert. Danach prüft eigener Code die fachlichen Invarianten. Schlägt eine dieser Prüfungen fehl, darf die Antwort nicht stillschweigend weiterverarbeitet werden.

Ein vereinfachtes Beispiel:

function validateResult(inputIds: string[], output: Result): void {
  if (output.items.length !== inputIds.length) {
    throw new Error("Ungültige LLM-Antwort: falsche Anzahl Ergebnisse");
  }
 
  const outputIds = output.items.map((item) => item.sourceId);
  const uniqueIds = new Set(outputIds);
 
  if (uniqueIds.size !== outputIds.length) {
    throw new Error("Ungültige LLM-Antwort: doppelte Zuordnung");
  }
 
  for (const id of inputIds) {
    if (!uniqueIds.has(id)) {
      throw new Error("Ungültige LLM-Antwort: Ergebnis fehlt");
    }
  }
}

Fail-closed bedeutet: Bei einer verletzten Regel gibt es kein „wird schon passen“. Die Antwort wird verworfen, erneut angefordert oder in einen kontrollierten manuellen Prozess gegeben. Welche Reaktion sinnvoll ist, hängt vom Anwendungsfall ab. Die semantische Prüfung selbst sollte aber nicht optional sein.

Testfälle aus echten Antworten ableiten

Für Structured Output würde ich mindestens diese Fälle gezielt testen:

  • minimale sinnvolle Eingabemenge,
  • typische Eingabemenge,
  • Randwerte, bei denen die Anzahl oder Gruppierung relevant ist,
  • leere oder fachlich nicht verarbeitbare Ergebnisse,
  • doppelte und fehlende Zuordnungen.

OpenAI strict mode bleibt dabei wertvoll: Er reduziert Fehler an der Schnittstelle und macht Antworten besser verarbeitbar. Er ersetzt aber keine Domänenlogik. Ein valides JSON ist nur der Anfang der Prüfung.

Welche Regel prüft bei euch, ob die Antwort stimmt – nicht nur, ob sie parst?