Zum Hauptinhalt springen
Lektion 12 von 16 · Chat Completions: Beherrschung der API

API-Antimuster

Lektion 12 von 16 Aktualisiert am 2. November 2026 Entwickler
Auf dieser Seite
  1. Fehler, die in der Produktion Geld kosten
  2. Ein abgeschnittenes Ergebnis als vollständig betrachten
  3. Gesprächsverlauf ohne Grenzen wachsen lassen
  4. Annahme, dass das Netzwerk immer reagiert
  5. Das Modell bitten, Dinge zu tun, die es nicht kann
  6. Deployment ohne Spur zu hinterlassen
  7. Eine Debugging-Routine
  8. Kernaussagen

Fehler, die in der Produktion Geld kosten

Nachdem wir uns mit bewährten Praktiken beschäftigt haben, wenden wir uns nun der anderen Seite zu. Die nachfolgenden Antimuster stellen die häufigsten Fallstricke beim Einsatz der Chat-Completions-API dar. Sie teilen ein unangenehmes Merkmal: Sie treten während der Entwicklung nicht zutage. Von zehn Testanfragen funktionieren alle einwandfrei. Erst in der Produktion explodieren sie und verursachen unerwartete Kosten, inkohärente Antworten, hohe Latenz oder im schlimmsten Fall das Leaken sensibler Daten.

Ein abgeschnittenes Ergebnis als vollständig betrachten

Der häufigste Fehler besteht darin, finish_reason zu ignorieren. Sie bitten um einen Vertrag, erhalten Text zurück und speichern ihn in der Datenbank: Doch max_tokens=100 hat die Generierung mitten in einem Klauselabschnitt unterbrochen, und niemand bemerkt dies, bis der Kunde unterschreibt. Die Lösung erfordert nur drei Zeilen Code und sollte zur Reflex werden.

# SCHLECHT — keine Verifikation
response = client.chat.complete(
    model="mistral-large-latest",
    messages=[{"role": "user", "content": "Entwerfen Sie einen vollständigen Vertrag."}],
    max_tokens=100  # Zu kurz für einen Vertrag!
)
contract = response.choices[0].message.content  # Potenziell abgeschnitten!

# GUT — systematische Verifikation
response = client.chat.complete(
    model="mistral-large-latest",
    messages=[{"role": "user", "content": "Entwerfen Sie einen vollständigen Vertrag."}],
    max_tokens=4000
)

if response.choices[0].finish_reason == "length":
    print("WARNUNG: Antwort abgeschnitten, max_tokens erhöhen")
else:
    contract = response.choices[0].message.content

Gesprächsverlauf ohne Grenzen wachsen lassen

Die zweite Falle löst sich langsam aus. Ein Chat-Loop, der bei jedem Durchlauf den gesamten Gesprächsverlauf sendet, funktioniert für die ersten zehn Austausche perfekt, kostet im zwanzigsten Durchlauf das Doppelte und überschreitet bei dem hundertsten Durchlauf schließlich das Kontextfenster – die Rechnung folgt demselben Verlauf. Die Lösung besteht darin, den Systemprompt (unverhandelbar) zu erhalten und nur die letzten N Nachrichten zu speichern.

# SCHLECHT — Verlauf wächst unbegrenzt
conversation = []
while True:
    user_input = input("Sie: ")
    conversation.append({"role": "user", "content": user_input})
    response = client.chat.complete(
        model="mistral-large-latest",
        messages=conversation  # 100 Durchläufe = Tausende von Tokens!
    )
    reply = response.choices[0].message.content
    conversation.append({"role": "assistant", "content": reply})

# GUT — gleitendes Fenster mit Trimmfunktion
MAX_MESSAGES = 20

def trim_conversation(messages: list, max_messages: int = MAX_MESSAGES) -> list:
    """Behält den Systemprompt + die letzten N Nachrichten."""
    system = [m for m in messages if m["role"] == "system"]
    history = [m for m in messages if m["role"] != "system"]

    if len(history) > max_messages:
        history = history[-max_messages:]

    return system + history

Annahme, dass das Netzwerk immer reagiert

Ein nackter API-Aufruf geht davon aus, dass nichts schiefgehen wird: keine Timeouts, keine 429er-Statuscodes, keine 500er-Fehler. In der Produktion treten alle drei Fälle ein, und der dritte passiert zum ungünstigsten Zeitpunkt. Beachten Sie den Unterschied im Code: Ein 429er-Fehler rechtfertigt exponentielles Backoff, ein Serverfehler erfordert eine Sekunde Geduld, aber ein Client-Fehler wie 400 oder 401 darf niemals wiederholt werden – Ihr Schlüssel wird sich nicht durch den dritten Versuch gültig machen; Sie verzögern lediglich die Diagnose.

# SCHLECHT — keine Fehlerbehandlung
response = client.chat.complete(
    model="mistral-large-latest",
    messages=[{"role": "user", "content": "Hallo"}]
)
# Absturz bei Timeout, 429, 500...

# GUT — Wiederholung mit Backoff
from mistralai.client.errors import SDKError
import time

def safe_complete(client, messages, max_retries=3, **kwargs):
    """API-Aufruf mit Wiederholung und exponentiellem Backoff."""
    for attempt in range(max_retries):
        try:
            return client.chat.complete(messages=messages, **kwargs)
        except SDKError as e:
            if e.status_code == 429:
                wait = 2 ** attempt
                print(f"Rate-Limit erreicht, warte {wait}s...")
                time.sleep(wait)
            elif e.status_code >= 500:
                time.sleep(1)
            else:
                raise  # Client-Fehler (400, 401) — nicht wiederholen
    raise Exception("Nach 3 Versuchen fehlgeschlagen")

Das Modell bitten, Dinge zu tun, die es nicht kann

Drei Antimuster treffen hier zusammen. Das erste ist subjektive Sprache: Begriffe wie „kurz”, „viele” oder „interessant” beziehen sich auf nichts Überprüfbares, und das Modell wird von einem Aufruf zum nächsten anders entscheiden. Ersetzen Sie jedes Adjektiv durch eine Zahl oder ein überprüfbares Kriterium.

# SCHLECHT — subjektiv, nicht messbar
"Schreiben Sie eine kurze Zusammenfassung."           # Kurz = 1 Satz? 1 Absatz?
"Geben Sie viele Beispiele."              # Viele = 3? 10? 50?
"Antworten Sie auf eine interessante Weise."    # Interessant nach welchen Kriterien?

# GUT — objektive Kriterien
"Schreiben Sie eine Zusammenfassung von 50 bis 80 Wörtern."
"Geben Sie genau 5 Beispiele, eins pro Zeile."
"Beginnen Sie jede Antwort mit einer relevanten Statistik."

Das zweite besteht darin, das Modell zählen zu lassen. LLMs sind notoriously schlecht darin, Wörter, Zeichen und Elemente zu zählen: Die Bitte um „genau 280 Zeichen” führt zu Texten mit 240 oder 310 Zeichen. Das Zählen gehört in Ihre Anwendung, nicht ins Modell.

# SCHLECHT — das Modell kann nicht präzise zählen
"Schreiben Sie genau 280 Zeichen für einen Tweet."

# GUT — zählen auf der Anwendungsebene
response = client.chat.complete(
    model="mistral-large-latest",
    messages=[{"role": "user", "content": "Schreiben Sie einen Tweet über KI in Frankreich."}],
    max_tokens=100
)

tweet = response.choices[0].message.content
if len(tweet) > 280:
    tweet = tweet[:277] + "..."

print(f"Tweet ({len(tweet)} Zeichen): {tweet}")

Das dritte fällt in dieselbe Kategorie: numerische Skalen. „Bewerten Sie von 1 bis 5” geht davon aus, dass das Modell eine stabile Darstellung des Unterschieds zwischen 3 und 4 hat, was es nicht tut. Eine verbale Skala, bei der jeder Schritt durch ein Kriterium definiert ist, liefert viel reproduzierbarere Bewertungen.

# SCHLECHT — Modelle interpretieren numerische Skalen schlecht
"Bewerten Sie die Qualität dieses Textes von 1 bis 5."

# GUT — verbale Skala mit Definitionen
"""Bewerten Sie die Qualität dieses Textes nach folgender Skala:
- EXZELENT: Klar, gut strukturiert, keine Fehler
- GUT: Grundsätzlich korrekt, kleine Verbesserungen möglich
- DURCHSCHNITT: Verständlich, aber ohne Struktur oder mit Fehlern
- SCHLECHT: Schwer verständlich, erhebliche Fehler
- SEHR SCHLECHT: Unverständlich oder vom Thema abweichend

Antworten Sie nur mit dem Label in Großbuchstaben."""

Deployment ohne Spur zu hinterlassen

Das letzte Antimuster ist das diskreteste: API aufrufen, Inhalt zurückgeben und nichts speichern. An einem Tag, an dem ein Nutzer eine abnorme Antwort meldet, haben Sie weder die Anfrage-ID, noch das tatsächlich eingesetzte Modell, noch den Tokenverbrauch. Strukturierte Protokollierung pro Aufruf kostet nur wenige Millisekunden und macht Untersuchungen möglich.

# SCHLECHT — keine Spur
response = client.chat.complete(
    model="mistral-large-latest",
    messages=messages
)
return response.choices[0].message.content

# GUT — strukturierte Protokollierung
import logging
import json

logger = logging.getLogger("mistral_api")

def logged_complete(client, messages, **kwargs):
    """API-Aufruf mit strukturierter Protokollierung."""
    response = client.chat.complete(messages=messages, **kwargs)

    logger.info(json.dumps({
        "request_id": response.id,
        "model": response.model,
        "prompt_tokens": response.usage.prompt_tokens,
        "completion_tokens": response.usage.completion_tokens,
        "finish_reason": response.choices[0].finish_reason
    }))

    return response

Eine Debugging-Routine

Wenn eine Antwort Ihren Erwartungen nicht entspricht, ist die Versuchung groß, den Prompt umzuschreiben. Beginnen Sie stattdessen mit fünf mechanischen Checks, in dieser Reihenfolge: Ist die Antwort vollständig? Hat das richtige Modell geantwortet? Wie viele Tokens wurden verbraucht? Ist die Antwort leer? Ist das Verhältnis von Ausgabe zu Eingabe plausibel? Ein Verhältnis unter 1 % bei einem großen Prompt signalisiert fast immer, dass das Modell die Anfrage nicht verarbeitet hat, statt sie falsch zu handhaben.

def debug_response(response):
    """Debugging-Checkliste für unerwartete Antworten."""
    choice = response.choices[0]

    # 1. Ist die Antwort vollständig?
    if choice.finish_reason == "length":
        print("PROBLEM: Antwort abgeschnitten — max_tokens erhöhen")

    # 2. Wurde das richtige Modell verwendet?
    print(f"Modell: {response.model}")

    # 3. Wie viele Tokens wurden verbraucht?
    print(f"Eingabetokens: {response.usage.prompt_tokens}")
    print(f"Ausgabetokens: {response.usage.completion_tokens}")

    # 4. Ist die Antwort leer?
    if not choice.message.content or not choice.message.content.strip():
        print("PROBLEM: Leere Antwort")

    # 5. Verdächtiges Eingabe/Ausgabe-Verhältnis?
    ratio = response.usage.completion_tokens / max(response.usage.prompt_tokens, 1)
    if ratio < 0.01:
        print(f"WARNUNG: Sehr niedriges Ausgabe/Eingabe-Verhältnis ({ratio:.4f})")

Kernaussagen

  • Prüfen Sie immer finish_reason, um Abschnitte zu erkennen
  • Begrenzen Sie den Gesprächsverlauf mit einem gleitenden Fenster
  • Implementieren Sie Wiederholungen mit exponentiellem Backoff für Netzwerkfehler
  • Verwenden Sie in Ihren Prompts objektive, messbare Kriterien
  • Lassen Sie das Modell niemals zählen – validieren Sie auf der Anwendungsebene
  • Protokollieren Sie jeden API-Aufruf in der Produktion zur Fehleranalyse und Kostenüberwachung