Retry mit Backoff und Jitter, M1-Abschluss
Track KI · M1 Baustein 05 und Abschluss-Check 06 · Teil 2 von 2 · ca. 40 Min. plus Projektaufgabe
Worum es geht
Ein LLM-Aufruf schlägt gelegentlich fehl, ohne dass dein Code schuld ist: Rate Limit, Überlast, Timeout. Die Quelle sagt: Solche Fehler verschwinden meist von selbst, wenn du mit Retry und Backoff (Wiederholung mit wachsender Wartezeit) reagierst, statt abzustürzen. Falsch gemacht, verschlimmert Retry die Überlast.
Was du aus Teil 1 brauchst: die Kostenfunktion und das Logging pro Aufruf (die Projektaufgabe setzt beides ein) und die Idee, Zeit nicht echt zu warten, sondern als Parameter hereinzureichen. Die Rechnung steht in Teil 1: Kosten und Streaming.
In diesem Teil baust du ein retry_call() mit Exponential Backoff und Jitter gegen ein Fake-Modell, das 429 und “Overloaded” meldet, mit einer virtuellen Uhr. Echte API-Aufrufe gibt es erst in der Projektaufgabe am Ende, lokal im Lernlabor. Das ist der Abschluss von M1: Eine Funktion extract_rechnung mit Validierung, Kostenlog, Retry und austauschbarem Modell.
Die Konzepte hinter Retries (Thundering Herd, Retry-Budget, Circuit Breaker) stehen in konzepte/07a (Retry, Backoff, Budget) und konzepte/07b (Circuit Breaker). Hier wendest du sie auf LLM-Aufrufe an. Fehlerklassen Wiederholbar und Endgueltig kennst du aus ki/04.
Von JS/TS her gedacht
| Idee | JS/TS | Python |
|---|---|---|
| Warten | await sleep(ms) (Millisekunden) |
time.sleep(s) (Sekunden, blockiert!) oder await asyncio.sleep(s) |
| Zufall | Math.random() * max |
rng.uniform(0, max) mit einem random.Random-Objekt |
| Fehler nach Klasse | if (e instanceof RateLimitError) |
except RateLimitError: |
| Zeit im Test fälschen | jest.useFakeTimers() |
eine kleine Uhr-Klasse, die du hineinreichst (siehe unten) |
Ein Unterschied ist wichtig. time.sleep blockiert den ganzen Thread. Für Tests und Übungen reichst du darum eine Uhr als Parameter herein.
Konzept
Schritt 4: Retry mit Backoff und Jitter
Welche Fehler wiederholst du? Die Quelle nennt RateLimitError (zu viele Anfragen) und OverloadedError (Anbieter überlastet). Beide sind vorübergehend (transient), also in der Sprache aus ki/04 Wiederholbar. Ein ungültiger Schlüssel ist Endgültig: Ein zweiter Versuch kostet nur Zeit.
Die Stolperfalle der Quelle: Retry ohne Obergrenze und ohne Backoff verschlimmert eine bestehende Überlast, statt sie zu überbrücken. Also immer eine feste Anzahl Versuche (die Quelle sagt 3 bis 4) und eine steigende Wartezeit, nie eine enge Schleife.
Die Wartezeit wächst exponentiell (Exponential Backoff): Vor dem n-ten Wiederholen ist die Obergrenze basis * 2 ** (n - 1), gedeckelt bei deckel. Mit basis = 1 und deckel = 8 sind das 1, 2, 4, 8, 8 Sekunden. Dazu kommt Jitter (zufällige Streuung): Würden 200 Clients alle genau 2 Sekunden warten, kämen sie alle gleichzeitig zurück. Bei Full Jitter wartet jeder eine zufällige Zeit zwischen 0 und der Obergrenze (rng.uniform(0, obergrenze)). Warum das nötig ist, steht ausführlich in konzepte/07a.
Für die Übungen brauchst du zwei Hilfsmittel. Die virtuelle Uhr merkt sich nur, wie viel Zeit vergangen wäre, ohne wirklich zu warten. Das Fake-Modell liefert nacheinander die Schritte eines Plans: eine Exception wird geworfen, ein String zurückgegeben.
Und ein erstes retry_ohne_jitter: feste Versuche, verdoppelte Wartezeit, aber noch ohne Deckel und ohne Zufall. Es gibt nach dem letzten Versuch den Originalfehler weiter. Die Übung 2 baut daraus die vollständige Version.
Ausgabe: Antwort 3 3.0. Zwei Fehlschläge, dann Erfolg: 3 Aufrufe, und die Uhr zeigt 1 plus 2 Sekunden. Beachte, dass nach dem letzten gescheiterten Versuch nicht mehr geschlafen wird: Es gäbe danach keinen Versuch, auf den man warten könnte.
(Die Namen 529 und “Overloaded” für den Überlastfehler stehen so nicht in der Quelle, sie ist nur bei OverloadedError konkret. Ob das SDK eine Klasse mit genau diesem Namen hat oder einen Überlastfehler nur über den Statuscode meldet, und welcher Statuscode das ist: bitte gegen die SDK-Doku prüfen. In der Projektaufgabe übersetzt das Skript deshalb SDK-Fehler in eigene Klassen. Auch ob das SDK von sich aus wiederholt, bevor dein Code den Fehler sieht, bitte prüfen: Das Skript schaltet es mit max_retries=0 ab, damit nur dein retry_call wiederholt.)
Falle
- Retry in der engen Schleife. Ohne Wartezeit schickst du bei Überlast in kürzester Zeit noch mehr Anfragen (Stolperfalle der Quelle).
- Alles wiederholen. Ein ungültiger Schlüssel, ein zu langer Prompt oder ein Programmierfehler werden auch beim zehnten Mal nicht besser. Nur Wiederholbares fangen.
- Kein Deckel. Verdoppeln ohne Obergrenze ergibt bei einer Basis von 1 Sekunde nach 10 Versuchen schon über 8 Minuten Gesamtwartezeit. Der
deckelbegrenzt das. - Nach dem letzten Fehlschlag noch schlafen. Das verschenkt Wartezeit, bevor du aufgibst.
Übungen
Übung 1: Backoff-Plan vorhersagen (leicht, ca. 6 Min.)
Diese Funktion liefert die Obergrenzen der Pausen für einen Lauf, bei dem alle Versuche scheitern. Es gibt eine Pause vor jedem Wiederholen, aber keine nach dem letzten Versuch (Formel aus Schritt 4):
def obergrenzen(max_versuche, basis, deckel):
return [min(deckel, basis * 2 ** (n - 1)) for n in range(1, max_versuche)]Sage voraus, was dieses Tupel mit vier Werten enthält:
obergrenzen(6, 0.5, 5)als Liste,- die Summe dieser Liste (die längste mögliche Gesamtwartezeit),
- die Anzahl der Pausen für
obergrenzen(1, 1.0, 8), - der letzte Wert von
obergrenzen(10, 1.0, 8).
Rechne Pause für Pause: Was liefert basis * 2 ** (n - 1) für n = 1, 2, 3 …, und ab wann greift der Deckel? Wie viele Werte erzeugt range(1, max_versuche), wenn max_versuche gleich 1 ist?
antwort = ([0.5, 1.0, 2.0, 4.0, 5.0], 12.5, 0, 8.0)
antwortÜbung 2: retry_call mit Backoff und Jitter (mittel, ca. 18 Min.)
Schreibe retry_call(fn, uhr, rng, max_versuche=3, basis=1.0, deckel=8.0). Ausgangsbasis ist retry_ohne_jitter aus Schritt 4. Die Klassen aus der Lektion stehen bereit, dazu Endgueltig und RetryErschoepft. Die Regeln:
fn()wird höchstensmax_versucheMal aufgerufen. Bei Erfolg kommt das Ergebnis zurück.- Nur
Wiederholbarwird gefangen. Alles andere (zum BeispielEndgueltigoder einKeyError) kommt sofort und unverändert oben an, ohne Wartezeit. - Vor dem n-ten Wiederholen (n = 1, 2, …) wartest du
uhr.schlafe(rng.uniform(0, obergrenze))mitobergrenze = min(deckel, basis * 2 ** (n - 1)). Benutze genaurng.uniform, nichtrandom.random(), und nichttime.sleep. - Nach dem letzten gescheiterten Versuch wird nicht mehr gewartet. Stattdessen wirfst du
RetryErschoepft(f"nach {max_versuche} Versuchen aufgegeben")mit dem letzten Fehler als Ursache (from).
Zum Nachrechnen: Bei basis=1, deckel=4 und fünf Fehlschlägen warten die vier Pausen nach Obergrenzen 1, 2, 4 und 4. Die fünfte Pause gibt es nicht.
Der Rahmen ist der aus Schritt 4. Drei Fragen: Welche Klasse darf der except-Zweig nennen? Wie nummerierst du die Pausen, wenn die erste Pause nach dem ersten Fehlschlag kommt? Und in welchem Fall darfst du nicht mehr schlafen? Den letzten Fehler musst du dir merken, weil der Name aus except ... as nach der Schleife nicht mehr lebt.
def retry_call(fn, uhr, rng, max_versuche=3, basis=1.0, deckel=8.0):
letzter = None
for versuch in range(1, max_versuche + 1):
try:
return fn()
except Wiederholbar as fehler:
letzter = fehler
if versuch < max_versuche:
obergrenze = min(deckel, basis * 2 ** (versuch - 1))
uhr.schlafe(rng.uniform(0, obergrenze))
raise RetryErschoepft(f"nach {max_versuche} Versuchen aufgegeben") from letzter
retry_callÜbung 3: Welche Retry-Strategie passt? (leicht, ca. 4 Min.)
Ein nächtlicher Job schickt 200 Anfragen gleichzeitig an die API. Kurz nach dem Start melden fast alle einen Rate-Limit-Fehler (429). Der Job soll am Ende möglichst viel erledigt haben, ohne die Lage beim Anbieter zu verschlimmern. Welche Strategie wählst du?
- A: Jeder Aufruf wiederholt sofort in einer Schleife, bis er durchkommt, damit der Job schnell fertig wird.
- B: Jeder Aufruf wartet zufällig lange, mit wachsender Obergrenze, und gibt nach wenigen Versuchen auf.
- C: Alle 200 Aufrufe warten genau 2 Sekunden, starten dann gemeinsam neu und tun das höchstens vier Mal.
- D: Der Job wertet das Rate Limit als endgültigen Fehler und bricht beim ersten 429 komplett ab, ohne Wiederholung.
Stelle dir 200 Clients vor, die denselben Fehler zur selben Zeit sehen. Wann kommt jeder wieder, und wie viele Versuche macht er insgesamt? Prüfe auch, ob 429 zu den Fehlern gehört, bei denen ein zweiter Versuch Sinn hat.
antwort = "B"
antwortProjektaufgabe: extract_rechnung mit Kostenlog und Retry (M1-Abschluss)
Das ist der Abschluss-Check von M1 (Baustein 05 und Abschluss-Check 06). Sie läuft lokal im Lernlabor, nicht im Browser. Für den Trockenlauf brauchst du keine zusätzlichen Pakete. Den API-Schlüssel liest das Skript nur aus der Umgebungsvariable ANTHROPIC_API_KEY, er steht nie in einer Datei. Ohne Schlüssel (oder ohne das Paket anthropic) läuft ein Trockenlauf mit einem Fake-Modell, das auch 429-Fehler simuliert. Dann entstehen keine Kosten.
Aufgabe (Quelle, Abschluss-Check): Eine Funktion extract_rechnung(text: str) -> Rechnung steht, mit Pydantic-Validierung, Kostenlogging pro Aufruf und Retry bei vorübergehenden Fehlern. Das Modell steckt in einer Konstante oder Umgebungsvariable, kein hartkodierter Modellname mitten im Code: Ein Modellwechsel ist eine Zeile.
Das Skript lernlabor/uebung/ki/ki_07_extract_rechnung.py bringt den Rahmen mit (Modell Rechnung, Fake-Modell, Uhr). Drei Teile sind Platzhalter, die du ausfüllst: retry_call, logge_kosten und extract_rechnung. Du kennst alle drei aus den Übungen: logge_kosten aus Teil 1 (Übung 2), retry_call aus diesem Teil (Übung 2). Aufruf:
cd lernlabor && uv run python uebung/ki/ki_07_extract_rechnung.pyDas Skript meldet pro Teil, ob er funktioniert. Mit Schlüssel und installiertem Paket macht es danach einen echten Aufruf (kostet Geld, Preise bitte beim Anbieter prüfen). Das Paket anthropic steht noch nicht in der pyproject.toml des Lernlabors, du installierst es mit uv add anthropic im Ordner lernlabor.
Aufgabenliste M1 (Quelle, ca. 20 bis 26 h gesamt):
- Messages-API-Grundlagen: CLI-Wrapper mit System-Prompt (2 bis 3 h)
- Prompting für Anwendungen: Prompt gegen 10 Edge Cases testen (2 bis 3 h)
- Structured Outputs:
extract_rechnungmit Tool-Schema und Pydantic (5 bis 7 h) - Tool Calling: Wechselkurs-Tool mit Zwei-Schritte-Umlauf (4 bis 6 h)
- Kostenlogging, Streaming und Retry-Hilfsfunktion bauen (4 bis 5 h, Teil 1 und dieser Teil)
- Abschluss-Check: Modell austauschbar machen, End-to-End-Test mit Kostenlog (2 h)
Hinweis zur Vereinfachung: Die Quelle verlangt für extract_rechnung ein Tool-Schema (Baustein 03). Im Hilfsskript bittet der Prompt der Einfachheit halber um JSON im Text und validiert mit Rechnung.model_validate_json. Ersetze das in deinem echten Projekt durch deine Tool-Schema-Version aus Baustein 03, die Validierung danach bleibt gleich.
Stolperfalle (Quelle): Retry ohne Obergrenze und ohne Backoff verschlimmert eine Überlast. Also feste Versuche, steigende Wartezeit, nie eine enge Schleife.
Fertig, wenn:
- Das Hilfsskript meldet im Trockenlauf für
retry_call,logge_kostenundextract_rechnungjeweils “OK” und am Ende “Alle Teile in Ordnung”. extract_rechnunggibt eine validierteRechnungzurück. Bei einer unbrauchbaren Antwort des Modells (kein gültiges JSON oder falsche Felder) wird ein Fehler geworfen, der nicht wiederholt wird.- Pro Aufruf entsteht genau eine Log-Zeile mit Input-Token, Output-Token und Kosten.
- Der Modellname steht genau einmal im Code (Konstante, überschreibbar über die Umgebungsvariable
ANTHROPIC_MODEL). Das Skript prüft das mit. - Ein 429 im Fake-Modell führt zu einem Retry mit virtueller Uhr (keine echte Wartezeit im Trockenlauf).
- Im Schlüssel-Fall: der Schlüssel steht nur in der Umgebung, nicht in der Datei, nicht im Log.
Selbstcheck:
Merksatz
Wiederhole nur vorübergehende Fehler: mit fester Obergrenze der Versuche, wachsender und zufällig gestreuter Wartezeit, nie in einer engen Schleife (Quelle, Stolperfalle).
Prüfstein
Dein Dienst ruft ein Modell auf, das manchmal einen Rate-Limit-Fehler meldet. Welche vier Zahlen oder Einstellungen legst du für retry_call fest (Versuche, Basis, Deckel, was noch?), und welche Fehlerklassen wiederholst du nicht?
Zurück zu Teil 1: Kosten und Streaming.
Quelle: quellen/kursbuch-lerninhalte.md, Modul M1, Baustein “05 Kosten, Streaming & Retries” und “06 Abschluss-Check” mit der Aufgabenliste M1 (Zeilen 437 bis 480). Aus der Quelle stammen: die Aussage zu Retry, RateLimitError und OverloadedError als Retry-Fälle, “nach 3 bis 4 Versuchen aufgeben”, die Stolperfalle, die Übung (retry_call() mit 3 Versuchen und Backoff) und der Abschluss-Check (extract_rechnung, Pydantic, Kostenlog, Retry, Modell in Konstante oder Umgebungsvariable). Über die Quelle hinaus (allgemeines Fachwissen, mit Python 3.13 ausgeführt): Exponential Backoff mit Full Jitter und Deckel, die virtuelle Uhr und das Fake-Modell. Bitte prüfen: der Statuscode für Überlast, ob das SDK von sich aus wiederholt, und ob der Modellname in der Projektdatei zur Quelle passt.