Einschränkungen des JSON-Modus und Fehlerbehandlung
Auf dieser Seite
Gültig bedeutet nicht korrekt
Der JSON-Modus garantiert syntaktisch gültiges JSON, nicht semantisch korrektes JSON. Diese Unterscheidung mag sich in der Entwicklung subtil anfühlen, wird aber in der Produktion kritisch. JSON, das ohne Fehler parsen lässt, aber unerwartete Schlüssel enthält, erzeugt genau die Art von Fehler, die Tage zur Aufspürung benötigen, da keine Ausnahme ausgelöst wird, wenn das Problem tatsächlich auftritt.
Keine Schema-Garantien
Der Mechanismus weiß nichts über Ihr Schema: Er hat die Struktur noch nie gesehen, die Ihr Code erwartet. Das Modell könnte ein Feld weglassen, das Sie benötigen, eines hinzufügen, das Sie nicht angefordert haben, einen Schlüssel umbenennen – firstName statt first_name – oder einen Typ ändern, indem es "42" als String zurückgibt, obwohl Sie die Zahl 42 erwartet haben. Keines davon ist ungültiges JSON.
# Sie erwarten dies:
# {"name": "Alice", "age": 30, "city": "Paris"}
# Aber Sie könnten dies erhalten:
# {"nom": "Alice", "âge": "trente ans", "ville": "Paris", "pays": "France"}
Das JSON ist gültig, aber die Struktur stimmt nicht mit dem überein, was Ihr Code erwartet. Dies ist der fundamentale Unterschied zu benutzerdefinierten strukturierten Ausgaben.
Zwingende Validierung auf Client-Seite
Da das Schema nicht garantiert wird, liegt die Validierung in Ihrer Verantwortung. Das minimale Muster führt drei Schritte in der richtigen Reihenfolge aus: Es versucht das Parsen, überprüft, ob die Wurzel ein Objekt ist (keine Liste oder ein Skalar), und prüft dann auf erforderliche Schlüssel. Jeder Fehler wirft eine ValueError mit einer Nachricht, die Sie in Ihren Protokollen verwenden können.
import json
from typing import Optional
def parse_and_validate(raw_content: str, required_keys: list[str]) -> dict:
"""Parse JSON and verify required keys."""
try:
data = json.loads(raw_content)
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON: {e}")
if not isinstance(data, dict):
raise ValueError(f"Expected JSON object, received {type(data).__name__}")
missing = [k for k in required_keys if k not in data]
if missing:
raise ValueError(f"Missing keys: {missing}")
return data
# Usage
try:
result = parse_and_validate(
response.choices[0].message.content,
required_keys=["name", "category", "priority"]
)
process_ticket(result)
except ValueError as e:
log_error(f"Invalid output: {e}")
fallback_processing(raw_content)
Die TypeScript-Version geht einen Schritt weiter, indem sie über das Vorhandensein der Schlüssel hinaus überprüft, ob die zurückgegebene Kategorie zur erlaubten Liste gehört. Dies ist die Prüfung, die am häufigsten fehlt, und die verhindert, dass ein Ticket mit der Klassifizierung "urgent_billing" stumm durch Ihre Routing-Logik gleitet.
interface TicketClassification {
category: string;
priority: string;
sentiment: string;
summary: string;
}
function validateTicket(raw: string): TicketClassification {
const data = JSON.parse(raw);
const requiredKeys = ["category", "priority", "sentiment", "summary"];
const missing = requiredKeys.filter((k) => !(k in data));
if (missing.length > 0) {
throw new Error(`Missing keys: ${missing.join(", ")}`);
}
const validCategories = ["billing", "account", "technical", "feature_request", "other"];
if (!validCategories.includes(data.category)) {
throw new Error(`Invalid category: ${data.category}`);
}
return data as TicketClassification;
}
Drei Fehler, die Sie antreffen werden
Der erste ist abgeschnittenes JSON. Wenn die Antwort das Limit von max_tokens erreicht, wird sie mitten im Stream abgeschnitten und wird ungültig – das schließende geschweifte Klammer kommt nie an. Sie können das Symptom noch vor dem Parsen erkennen: finish_reason ist gleich "length" statt "stop".
response = client.chat.complete(
model="mistral-small-latest",
messages=messages,
response_format={"type": "json_object"},
max_tokens=50 # Too small for complete JSON
)
# response.choices[0].finish_reason will be "length" instead of "stop"
if response.choices[0].finish_reason == "length":
print("Warning: Truncated response, increase max_tokens")
Überprüfen Sie immer finish_reason: Wenn es gleich "length" ist, ist die Antwort unvollständig, unabhängig davon, was der Rest des Inhalts besagt.
Der zweite Fehler betrifft Typen. Ein Alter könnte als String ankommen, ein Preis als Integer, wenn Sie einen Float erwarten. Vertrauen Sie dem Output nicht; umschließen Sie den Zugriff in einen Helfer, der einen Konversionsversuch unternimmt und auf einen Standardwert zurückfällt, wenn dies fehlschlägt.
def safe_get_int(data: dict, key: str, default: int = 0) -> int:
"""Extract an integer, with conversion if needed."""
value = data.get(key)
if value is None:
return default
if isinstance(value, int):
return value
if isinstance(value, float):
return int(value)
if isinstance(value, str):
try:
return int(value)
except ValueError:
return default
return default
# Usage
age = safe_get_int(data, "age", default=0)
Der dritte ist die Umbenennung von Schlüsseln. Bei einem französischen Prompt könnte das Modell phone leicht zu telephone oder name zu nom wechseln. Eine Synonym-Mapping-Aufnahme absorbiert diese Variationen, ohne dass Sie den Aufruf erneut versuchen müssen.
def normalize_keys(data: dict, key_mapping: dict) -> dict:
"""Normalize keys using a synonym mapping."""
normalized = {}
for standard_key, aliases in key_mapping.items():
for alias in aliases:
if alias in data:
normalized[standard_key] = data[alias]
break
return normalized
# Synonym mapping
mapping = {
"name": ["name", "nom", "full_name", "fullName"],
"email": ["email", "mail", "e-mail", "emailAddress"],
"phone": ["phone", "telephone", "tel", "phoneNumber"],
}
normalized = normalize_keys(raw_data, mapping)
Wiederversuch mit Feedback
Wenn die Validierung trotz Ihrer besten Bemühungen fehlschlägt, ist die beste Reaktion nicht, den exakt gleichen Aufruf erneut zu versuchen: Es ist, dem Modell mitzuteilen, was schiefgelaufen ist. Indem Sie die eigene Antwort zusammen mit der Fehlermeldung in den Gesprächsverlauf einfügen, geben Sie ihm den Kontext, den es benötigt, um sich selbst zu korrigieren, meist bereits beim zweiten Versuch.
import json
def call_with_retry(client, messages, max_retries=3):
"""Call with retry and error feedback."""
for attempt in range(max_retries):
response = client.chat.complete(
model="mistral-large-latest",
messages=messages,
response_format={"type": "json_object"},
temperature=0
)
raw = response.choices[0].message.content
try:
data = json.loads(raw)
validate_schema(data) # Your validation function
return data
except (json.JSONDecodeError, ValueError) as e:
if attempt < max_retries - 1:
# Add error feedback to context
messages.append({"role": "assistant", "content": raw})
messages.append({
"role": "user",
"content": f"Your response is invalid: {e}. Correct it and return valid JSON."
})
else:
raise RuntimeError(f"Failed after {max_retries} attempts: {e}")
Das Signal, den Ansatz zu ändern
Wenn Sie feststellen, dass Sie umfangreichen Validierungscode schreiben, ist dies ein starkes Signal dafür, dass es Zeit ist, zu benutzerdefinierten strukturierten Ausgaben zu migrieren. Drei typische Schwellenwerte bestätigen dies: Wenn mehr als fünf Felder zu validieren sind, wird die Kontrollfunktion länger als der Aufruf selbst; sobald Typen komplex werden mit verschachtelten Listen oder Aufzählungen, wird manuelle Validierung untrennbar von den Fehlern, die sie einführt; und für jede kritische Produktionspipeline ist es einfach sicherer, Einschränkungen dem Schema zu überlassen, als sie manuell nachzubauen.
Kernaussagen
- Der JSON-Modus garantiert gültiges JSON, nicht ein gültiges Schema
- Immer Output auf Client-Seite validieren: Prüfen auf erforderliche Schlüssel, korrekte Typen und Werte innerhalb der Grenzen
finish_reasonüberprüfen, um abgeschnittene Antworten zu erkennen- Einen Wiederversuchsmechanismus mit Fehlerfeedback implementieren
- Wenn Validierung komplex wird, migrieren Sie zu benutzerdefinierten strukturierten Ausgaben