Zum Hauptinhalt springen
Lektion 4 von 20 · API-Tools: Suche, Dateien und Code Interpreter

Fehlerbehandlung und Fallbacks

Lektion 4 von 20 Entwickler
Auf dieser Seite
  1. Fehlerbehandlung und Fallbacks
  2. Rückgabe von Fehlern an das Modell
  3. Wiederholung mit exponentiellem Backoff
  4. Zeitlimits pro Funktion
  5. Fallback zwischen Funktionen
  6. Schalter (Circuit Breaker)
  7. Vollständiger robuster Zyklus
  8. Kernaussagen

Fehlerbehandlung und Fallbacks

Funktionsaufrufe stellen eine Herausforderung dar, die klassischer Code nicht kennt: Wenn eine Funktion fehlschlägt, gibt es zwei Empfänger für diesen Fehler. Ihre Anwendung, wie üblich, und das Modell, das auf ein Ergebnis wartet, um fortzufahren. Dieses Vergessen führt zu dem verwirrendsten Symptom in der Produktion: ein Gespräch, das abrupt ohne Erklärung abbricht, weil eine Ausnahme auf Serverseite verschluckt wurde und das Modell weiterhin wartet.

Die Regel, die diese gesamte Lektion strukturiert, lautet: geben Sie immer ein function_call_output zurück, auch bei Fehlern. Ein Modell, das informiert wird, dass eine API nicht verfügbar ist, kann eine Alternative vorschlagen, um Klarheit bitten oder die Situation dem Nutzer erklären. Ein Modell, das nichts erhält, kann nichts tun.

Rückgabe von Fehlern an das Modell

Das Format des Fehlers ist ebenso wichtig wie die Übermittlung selbst. Ein bloßes “Error” lehrt das Modell nichts; ein JSON-Objekt, das die Fehlertypen unterscheidet, ob sie vorübergehend oder dauerhaft sind, und die nächsten Schritte, ermöglicht es dem Modell, eine Entscheidung zu treffen. Dies ist kontraintuitiv: Sie verfassen Fehlermeldungen, die von einer vernunftfähigen Maschine gelesen werden sollen, nicht protokolliert für Menschen.

import json
from openai import OpenAI

client = OpenAI()

def execute_function(name: str, arguments: str, call_id: str) -> dict:
    try:
        args = json.loads(arguments)
        result = dispatcher[name](**args)
        return {
            "type": "function_call_output",
            "call_id": call_id,
            "output": json.dumps(result)
        }
    except KeyError:
        return {
            "type": "function_call_output",
            "call_id": call_id,
            "output": json.dumps({
                "error": "unknown_function",
                "message": f"The function '{name}' does not exist"
            })
        }
    except json.JSONDecodeError:
        return {
            "type": "function_call_output",
            "call_id": call_id,
            "output": json.dumps({
                "error": "invalid_arguments",
                "message": "The JSON arguments are malformed"
            })
        }
    except Exception as e:
        return {
            "type": "function_call_output",
            "call_id": call_id,
            "output": json.dumps({
                "error": "internal_error",
                "message": str(e)
            })
        }

Wiederholung mit exponentiellem Backoff

Wiederholungen machen nur Sinn für Probleme, die sich bei einem zweiten Versuch lösen könnten: Netzunterbrechungen, Statuscode 429 oder vorübergehende Ausfälle. Das Wiederholen einer Anfrage, die aufgrund ungültiger Authentifizierung oder falscher Parameter abgelehnt wurde, verzögert lediglich den Misserfolg und vervielfacht die Aufrufe. Deshalb filtert der folgende Code explizit Fehler aus – das Filtern erledigt die Arbeit, während Backoff lediglich die Höflichkeit ist, die Sie dem entfernten Dienst schulden.

import time
import random

def execute_with_retry(func, args, max_retries=3, base_delay=1.0):
    """Retry with exponential backoff and jitter."""
    for attempt in range(max_retries + 1):
        try:
            return func(**args)
        except (ConnectionError, TimeoutError) as e:
            if attempt == max_retries:
                raise
            delay = base_delay * (2 ** attempt) + random.uniform(0, 0.5)
            time.sleep(delay)
        except ValueError:
            # Data error: do not retry
            raise

def execute_function_robust(name, arguments, call_id):
    args = json.loads(arguments)
    try:
        result = execute_with_retry(dispatcher[name], args)
        return {
            "type": "function_call_output",
            "call_id": call_id,
            "output": json.dumps(result)
        }
    except Exception as e:
        return {
            "type": "function_call_output",
            "call_id": call_id,
            "output": json.dumps({
                "error": type(e).__name__,
                "message": str(e),
                "retries_performed": 3
            })
        }

Zeitlimits pro Funktion

Ein einzelnes Zeitlimit für alle Funktionen ist immer falsch konfiguriert: zu kurz tötet es schwere Verarbeitung, zu lang lässt den Nutzer bei einer einfachen Cache-Abfrage warten. Basieren Sie jeden Wert auf beobachteten Dauern unter normalen Bedingungen, mit einem Puffer – nicht auf dem schlimmst vorstellbaren Fall, was effektiv bedeuten würde, kein Zeitlimit zu haben.

import signal
from functools import wraps

TIMEOUTS = {
    "search_client": 5,
    "generate_report": 30,
    "send_email": 10,
}

def with_timeout(func, timeout_seconds):
    """Executes a function with a timeout."""
    import concurrent.futures
    with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
        future = executor.submit(func)
        try:
            return future.result(timeout=timeout_seconds)
        except concurrent.futures.TimeoutError:
            raise TimeoutError(
                f"Function interrupted after {timeout_seconds}s"
            )

def execute_with_timeout(name, arguments, call_id):
    args = json.loads(arguments)
    timeout = TIMEOUTS.get(name, 10)  # 10s by default

    try:
        result = with_timeout(
            lambda: dispatcher[name](**args),
            timeout
        )
        return {
            "type": "function_call_output",
            "call_id": call_id,
            "output": json.dumps(result)
        }
    except TimeoutError as e:
        return {
            "type": "function_call_output",
            "call_id": call_id,
            "output": json.dumps({
                "error": "timeout",
                "message": str(e),
                "suggestion": "Retry with more specific criteria"
            })
        }

Fallback zwischen Funktionen

Fallback ist ein Kompromiss, kein Reflex: das Bereitstellen von sechs Stunden alten gecachten Daten ist für ein Dashboard besser als nichts, aber für ein Bankkonto weit schlechter. Wenn Sie die Leistung mindern, sagen Sie es – im Ergebnis, das an das Modell zurückgegeben wird, damit es den Nutzer warnen kann, anstatt veraltete Daten als frisch darzustellen.

async def search_product(term: str) -> dict:
    """Search with fallback: primary API -> cache -> local database."""
    # Attempt 1: Catalog API
    try:
        return await api_catalog.search(term)
    except Exception:
        pass

    # Attempt 2: Redis cache
    try:
        cached = await redis_client.get(f"product:{term}")
        if cached:
            result = json.loads(cached)
            result["_source"] = "cache"
            result["_warning"] = "Data may be outdated"
            return result
    except Exception:
        pass

    # Attempt 3: Local SQLite database
    try:
        row = db.execute(
            "SELECT * FROM products WHERE name LIKE ?",
            (f"%{term}%",)
        ).fetchone()
        if row:
            return {
                "name": row["name"],
                "price": row["price"],
                "_source": "local_db",
                "_warning": "Data from last synchronization"
            }
    except Exception:
        pass

    return {
        "error": "unavailable",
        "message": "No data source accessible for this search"
    }

Schalter (Circuit Breaker)

Der Schalter (Circuit Breaker) bewältigt, was Wiederholungen nicht können: einen Dienst, der für längere Zeit ausgefallen ist. Nach N Fehlern schaltet er um und gibt sofort einen Fehler zurück, ohne auch nur zu versuchen, die Verbindung herzustellen, und lässt gelegentlich eine Testanfrage durch, um die Wiederherstellung zu prüfen. Der Gewinn ist zweifach – Ihre Nutzer warten nicht mehr auf ein Timeout bei jeder Anfrage, und der kämpfende Dienst wird nicht durch Ihre Wiederholungen weiter heruntergehalten.

from datetime import datetime, timedelta

class CircuitBreaker:
    def __init__(self, failure_threshold=5, open_duration=60):
        self.threshold = failure_threshold
        self.duration = open_duration
        self.failures = 0
        self.last_failure = None
        self.state = "closed"  # closed, open, half-open

    def can_execute(self) -> bool:
        if self.state == "closed":
            return True
        if self.state == "open":
            if datetime.now() - self.last_failure > timedelta(seconds=self.duration):
                self.state = "half-open"
                return True
            return False
        return True  # half-open: allow one test attempt

    def record_success(self):
        self.failures = 0
        self.state = "closed"

    def record_failure(self):
        self.failures += 1
        self.last_failure = datetime.now()
        if self.failures >= self.threshold:
            self.state = "open"

# One circuit breaker per external service
breakers = {
    "weather_api": CircuitBreaker(failure_threshold=3, open_duration=30),
    "catalog_api": CircuitBreaker(failure_threshold=5, open_duration=60),
}

Vollständiger robuster Zyklus

Das vollständige Muster bringt die fünf oben genannten Mechanismen zusammen. Eine Vorsichtsmaßnahme macht es wirklich sicher, und sie ist leicht zu übersehen: begrenzen Sie die Anzahl der Iterationen. Ein Modell, das in einer Schleife eine Funktion aufruft, deren Ergebnis es nicht mag, ist selten, aber ohne eine Obergrenze würde es Ihr Budget bis zur Erschöpfung verbrennen – und das ist die Art von Vorfall, den Sie erst auf der Rechnung entdecken.

def robust_loop(prompt, tools, max_turns=10):
    messages = prompt
    for turn in range(max_turns):
        response = client.responses.create(
            model="gpt-5.6-terra",
            input=messages,
            tools=tools
        )

        calls = [i for i in response.output if i.type == "function_call"]
        if not calls:
            return response.output_text

        results = []
        for call in calls:
            result = execute_function_robust(
                call.name, call.arguments, call.call_id
            )
            results.append(result)

        messages = response.output + results

    return "Workflow interrupted: turn limit reached"

Kernaussagen

  • Geben Sie immer ein function_call_output zurück, auch bei Fehlern
  • Strukturieren Sie Fehler als JSON, damit das Modell sie nutzen kann
  • Wiederholen Sie nur vorübergehende Fehler mit exponentiellem Backoff
  • Implementieren Sie Fallbacks zwischen Datenquellen
  • Verwenden Sie einen Schalter (Circuit Breaker), um fehlerhafte Dienste zu schützen