Zurück zum Blog

API & SDKAnleitung

Gültiges JSON von einer LLM-API erhalten: response_format und Retries

Eine kopierbare Python-Anfrage, die parsebares JSON zurückgibt, ein json.loads-Wrapper, der leere oder abgeschnittene Antworten behandelt, und eine Checkliste, warum JSON bricht.

Von CheapTokens TeamVeröffentlicht am 5 Min. Lesezeit

Ein Terminal, das eine Chat-Completion-Anfrage mit response_format json_object zeigt
Auf dieser Seite
  1. Die Aufgabe: eine E-Mail rein, eine Zeile raus
  2. Warum das Beispiel wichtiger ist als das Flag
  3. Sicher parsen: leer, abgeschnitten oder einfach kaputt
  4. Prüfe die Form, nicht nur die Syntax
  5. Was ein Stapel E-Mails kostet
  6. Kurze Prüfung

Um JSON aus einem Chat-Modell zu bekommen, sind drei Dinge nötig: response_format={"type": "json_object"} übergeben, das Wort "json" und ein Beispiel der Struktur in den Prompt schreiben und max_tokens hoch genug für das gesamte Objekt setzen. Dann niemals blind der Antwort vertrauen: prüfen, dass sie nicht leer ist, finish_reason prüfen und erst dann json.loads aufrufen.

Das ist die Kurzfassung. Der Rest dieses Leitfadens ist ein vollständiges Skript für eine sehr häufige Aufgabe: Felder aus einer E-Mail extrahieren. Die Beispiele verwenden die Basis-URL https://api.cheaptokens.dev/v1 und die Modell-ID deepseek-ai/DeepSeek-V4-Flash-0731 (Dokumentation).

Die Aufgabe: eine E-Mail rein, eine Zeile raus#

Angenommen, dein Skript liest Support-E-Mails und muss jede in einen Datensatz umwandeln: Absender, Datum, Betrag und was der Absender möchte. Nimm diese hier:

Hallo, hier ist Anna Petrova von Lumen Ltd. Am 14. Oktober habe ich Rechnung #3381 über 1.250 EUR bezahlt, aber die Quittung zeigt sie immer noch als unbezahlt an. Bitte beheben Sie das vor Freitag.

Du möchtest genau das zurückbekommen und nichts anderes:

json
{"sender": "Anna Petrova", "date": "2026-10-14", "amount": 1250, "currency": "EUR", "action": "fix receipt status"}

Ein einfacher Prompt wie "extrahiere die Felder" kann dir einen freundlichen Satz, einen Code-Fence oder beides liefern. Dein json.loads erstickt am ersten Backtick. Das ist das Problem, das response_format und ein guter Prompt gemeinsam lösen.

Dieselbe Anfrage in Python und curl

Python

python
import json
import os

from openai import OpenAI

client = OpenAI(
    base_url="https://api.cheaptokens.dev/v1",
    api_key=os.environ["CHEAPTOKENS_API_KEY"],
)
MODEL = "deepseek-ai/DeepSeek-V4-Flash-0731"

SYSTEM = """Extract fields from the email and answer with json only.
Example of the json format:
{"sender": "Jane Roe", "date": "2026-01-31", "amount": 99.5, "currency": "USD", "action": "short phrase"}
Use null for a field that is not in the email."""

resp = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": SYSTEM},
        {"role": "user", "content": email_text},
    ],
    response_format={"type": "json_object"},
    max_tokens=1000,
    temperature=0,
)
print(resp.choices[0].message.content)

curl

bash
curl https://api.cheaptokens.dev/v1/chat/completions \
  -H "Authorization: Bearer $CHEAPTOKENS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "deepseek-ai/DeepSeek-V4-Flash-0731",
    "messages": [
      {"role": "system", "content": "Extract fields from the email and answer with json only. Example: {\"sender\": \"Jane Roe\", \"amount\": 99.5}"},
      {"role": "user", "content": "Hi, this is Anna Petrova from Lumen Ltd. I paid invoice #3381 for 1,250 EUR..."}
    ],
    "response_format": {"type": "json_object"},
    "max_tokens": 1000
  }'

Die DeepSeek-API-Referenz für JSON Output (geprüft am 27. Oktober 2026; die Seite hat kein Veröffentlichungsdatum) nennt die Regeln, denen diese Anfrage folgt: response_format auf json_object gesetzt, das Wort "json" irgendwo im System- oder Benutzer-Prompt und ein Beispiel der Zielstruktur. Ohne das Beispiel muss das Modell deine Feldnamen erraten, und es wird bei der nächsten E-Mail anders raten.

Warum das Beispiel wichtiger ist als das Flag#

response_format fordert die API auf, ein JSON-Objekt zurückzugeben. Es sagt dem Modell nicht, welches Objekt. Stell es dir vor, als würdest du einem Kollegen sagen: "Schick mir eine Tabelle", ohne zu sagen, welche Spalten. Du bekommst eine Tabelle, nur nicht deine.

Das Beispiel im Prompt ist die Kopfzeile der Spalten. Zwei Gewohnheiten machen es besser:

  • Zeige die echte Form, einschließlich Typen: 1250 ist eine Zahl, "2026-10-14" ist ein String in einem festen Datumsformat.
  • Sag, was zu tun ist, wenn ein Feld fehlt (null oben). Sonst erfindet das Modell einen Wert, um die Lücke zu füllen.

Behandle response_format als starken Hinweis, nicht als Garantie. Modelle geben manchmal trotzdem einen leeren String zurück oder verpacken das Objekt in Prosa. Deshalb gibt es den nächsten Teil.

Sicher parsen: leer, abgeschnitten oder einfach kaputt#

Drei Fehler treten in der Praxis auf, und jeder hinterlässt eine andere Spur:

  • Leerer Inhalt. Die DeepSeek-Dokumentation sagt, dass der JSON-Modus gelegentlich leeren Inhalt zurückgeben kann und empfiehlt, den Prompt umzuformulieren. Auf deiner Seite sieht es so aus, dass message.content "" oder None ist und json.loads("") Expecting value: line 1 column 1 (char 0) auslöst.
  • Abgeschnitten. max_tokens ging mitten im Objekt aus. finish_reason ist length, und der Text endet mitten im String. Mehr dazu in warum eine KI-Antwort mitten im Satz aufhört.
  • Kein JSON. Ein Satz vor dem Objekt oder ein Fence darum.
json.loads auf eine leere Antwort löst JSONDecodeError aus
Wie eine leere JSON-Modus-Antwort in Python aussieht

Die Lösung für den ersten und dritten Fall ist ein Retry mit einem festeren Prompt. Für den zweiten ändert ein Retry mit demselben Limit nichts, also wirft der Wrapper und sagt dir, du sollst max_tokens erhöhen:

python
def ask_json(email_text: str, retries: int = 1) -> dict:
    user = email_text
    for attempt in range(retries + 1):
        resp = client.chat.completions.create(
            model=MODEL,
            messages=[
                {"role": "system", "content": SYSTEM},
                {"role": "user", "content": user},
            ],
            response_format={"type": "json_object"},
            max_tokens=1000,
            temperature=0,
        )
        choice = resp.choices[0]
        if choice.finish_reason == "length":
            raise ValueError("JSON cut off by max_tokens: raise the limit")
        text = (choice.message.content or "").strip()
        if text:
            try:
                return json.loads(text)
            except json.JSONDecodeError:
                pass
        # empty or not JSON: ask again, more firmly
        user = email_text + "\n\nReply with one json object only, in the format of the example."
    raise ValueError("no valid JSON after retry")

Ein Retry reicht für ein Skript. Wenn es zweimal fehlschlägt, protokolliere die E-Mail-ID und mach weiter; eine dritte identische Anfrage hilft selten.

Paare zuordnen

Welches Symptom weist auf welche Ursache hin?

  • finish_reason ist "length"max_tokens ist zu klein für das Objekt
  • content ist ein leerer StringDer JSON-Modus gab nichts zurück; formuliere den Prompt um und versuche es erneut
  • JSONDecodeError bei einem Zeichen, das ein Backtick istDas Modell hat das JSON in einen Code-Fence verpackt
  • Feldnamen unterscheiden sich zwischen E-MailsDer Prompt enthält kein Beispiel der Struktur

Prüfe die Form, nicht nur die Syntax#

json.loads beweist nur, dass der Text gültiges JSON ist. Es beweist nicht, dass er die Felder enthält, die du angefordert hast. Füge direkt nach dem Parsen eine günstige Prüfung hinzu:

python
REQUIRED = {"sender", "date", "amount", "currency", "action"}

data = ask_json(email_text)
missing = REQUIRED - data.keys()
if missing:
    raise ValueError(f"missing fields: {sorted(missing)}")

Von hier aus geht das Dict direkt in eine Datenbankzeile oder eine Tabelle. Wenn du möchtest, dass das Modell deine eigenen Funktionen aufruft, anstatt einen festen Datensatz zu füllen, ist das ein anderes Werkzeug: siehe Function Calling in Python.

Der Weg von der E-Mail zum geprüften Datensatz: Prompt mit Beispiel, json_object, Parsen, Feldprüfung
Der gesamte Weg in fünf Schritten

Was ein Stapel E-Mails kostet#

Eine kurze Extraktion ist die günstige Art von Anfrage: ein paar hundert Tokens rein, ein paar Dutzend raus. Angenommen, 1.000 E-Mails pro Tag bei etwa 400 Input-Tokens (Prompt, Beispiel und E-Mail) und etwa 80 Output-Tokens (das JSON). Bei DeepSeek-V4-Flash sind das $0.014 pro Tag, und die Tabelle zeigt dieselbe Last auf den anderen Modellen:

Modell Pro Anfrage 1.000 Anfragen OpenRouter, 1.000 Anfragen
GLM-5.3-Flash $0.000011 $0.0112 $0.0226
DeepSeek-V4-Flash $0.000014 $0.014 $0.0315
MiniMax-M2.7 $0.000032 $0.032 $0.216

400 Input- und 80 Output-Tokens pro Anfrage. Die Preise pro 1 Mio. Tokens sind aktuell und ändern sich zusammen mit unseren Tarifen; die OpenRouter-Spalte zeigt Referenzpreise für dieselbe Arbeitslast.

Insgesamt, Input und Output zusammengerechnet, ist DeepSeek-V4-Flash für diese Last günstiger als der Referenzpreis von OpenRouter. Es erlaubt außerdem bis zu 32.768 Output-Tokens pro Antwort, sodass eine lange Liste von Positionen nicht abgeschnitten wird, wie es bei einem kleinen Limit der Fall wäre, und der Preis ist zu jeder Stunde gleich. GLM-5.3-Flash (zai-org/GLM-5.3-Flash) ist eine weitere Option unter derselben API-Adresse: bis zu 8.192 Output-Tokens, was für einen Datensatz pro E-Mail reichlich ist. Aktuelle Preise findest du auf der Preisseite; die DeepSeek-Modellseite enthält die Details zum hier verwendeten Modell.

Kurze Prüfung#

Kurzer Check

Dein JSON ist mitten in einem String abgeschnitten. Was schaust du dir zuerst an?

Antwort anzeigen

Antwort: finish_reason und max_tokens

Ein abgeschnittenes Objekt endet mit finish_reason auf length. Das Limit wurde erreicht, also erhöhe max_tokens; die Temperatur oder die Adresse zu ändern hilft nicht.

Wenn du noch keine Anfrage gesendet hast, beginne mit deiner ersten API-Anfrage und komm dann zurück und tausche den obigen Code ein.