Fehler, Wiederholungen, Rate Limits und Kosten
Die häufigsten API-Fehler nachstellen, sehen, welche Wiederholungen das openai-SDK standardmäßig schon übernimmt, und dann eine Aufruffunktion mit Timeout, exponentiellem Backoff, Begrenzung der Parallelität und Kostenprotokoll schreiben, die man direkt ins Projekt übernehmen kann.
- Etwa 40 Minuten
- Niveau: Fortgeschritten
- Getestet: 2026-09-14 deepseek-flash, openai 3.14
Code und Programmausgaben stehen genau so da, wie sie gelaufen sind – Kommentare und Ausgaben sind daher auf Chinesisch.
Wenn du Beispiele auf deinem eigenen Rechner ausführst, schlagen API-Aufrufe fast nie fehl. Nach dem Livegang sieht das anders aus: Zur Hauptzeit antwortet der Server mit 429, weil du zu viele Anfragen stellst, eine Anfrage hängt eine Minute ohne Reaktion, gelegentlich kommt ein 500er. Ist der Code darauf nicht vorbereitet, sieht ein Nutzer einen langen Fehlertext oder eine Seite, die sich endlos dreht.
Und dann das Geld. Eine falsch geschriebene Schleife, eine Wiederholung ohne Obergrenze kann über Nacht das Budget eines Monats verbrennen.
Diese Lektion stellt zuerst die häufigen Fehler nach und schreibt dann eine Aufruffunktion, die man direkt ins Projekt übernehmen kann.
Häufige Fehler
code/03-llm-apps/errors.py erzeugt absichtlich drei Arten von Fehlern:
print("== 1. 密钥错误")
try:
OpenAI(api_key="sk-wrong", base_url=BASE_URL).chat.completions.create(model=MODEL, messages=HELLO)
except openai.AuthenticationError as e:
print(f"{type(e).__name__},状态码 {e.status_code}")
print("== 2. 模型名写错")
try:
OpenAI(api_key=os.environ["LLM_API_KEY"], base_url=BASE_URL).chat.completions.create(model="deepseek-flsh", messages=HELLO)
except openai.APIStatusError as e:
print(f"{type(e).__name__},状态码 {e.status_code},{e.message[:100]}")
print("== 3. 超时(故意把超时设成 0.5 秒,并关掉 SDK 自带的重试)")
start = time.time()
try:
OpenAI(api_key=os.environ["LLM_API_KEY"], base_url=BASE_URL, timeout=0.5, max_retries=0).chat.completions.create(
model=MODEL, messages=[{"role": "user", "content": "写一篇 800 字的文章"}], extra_body=NO_THINKING)
except openai.APITimeoutError as e:
print(f"{type(e).__name__},用了 {time.time() - start:.1f} 秒")
Ergebnis:
== 1. 密钥错误
AuthenticationError,状态码 401
== 2. 模型名写错
BadRequestError,状态码 400,Error code: 400 - {'error': {'message': 'The supported API model names are deepseek-flash, deepseek-
== 3. 超时(故意把超时设成 0.5 秒,并关掉 SDK 自带的重试)
APITimeoutError,用了 0.7 秒
Das openai-SDK verwandelt verschiedene Fehler in verschiedene Ausnahmeklassen, die du nach Typ getrennt behandeln kannst. Die häufigsten:
| Ausnahme | Statuscode | Ursache | Wiederholen? |
|---|---|---|---|
AuthenticationError |
401 | Schlüssel falsch oder ungültig | Nein, beliebig viele Wiederholungen ändern nichts |
PermissionDeniedError |
403 | keine Berechtigung | Nein |
BadRequestError |
400 | Problem mit der Anfrage selbst: falscher Modellname, ungültiger Parameter, Kontextlänge überschritten | Nein, den Code ändern |
RateLimitError |
429 | zu viele Anfragen, gedrosselt | Ja, nach einer Weile |
InternalServerError |
500 und höher | Problem auf der Serverseite | Ja, meist vorübergehend |
APITimeoutError |
keiner | zu lange keine Antwort | Ja |
APIConnectionError |
keiner | keine Netzwerkverbindung | Ja |
Die Regel ist einfach: Liegt der Fehler bei dir, bringt Wiederholen nichts; liegt er bei der Gegenseite oder im Netz, darf man wiederholen. Die Fehlermeldung bei einem falschen Modellnamen ist freundlich und listet die unterstützten Modellnamen gleich auf; danach richtet man sich einfach.
Außerdem gibt DeepSeek bei zu wenig Guthaben den Statuscode 402 zurück, im SDK ein allgemeiner APIStatusError. Auch diesen Fehler sollte man nicht wiederholen, sondern sich benachrichtigen lassen, um aufzuladen.
Was das SDK schon für dich tut
Viele wissen es nicht: Das openai-SDK wiederholt standardmäßig automatisch. Ich habe mir den Quellcode der aktuellen Version (3.14) angesehen:
- Standard ist
max_retries=2, also höchstens zwei weitere Versuche nach einem Fehlschlag. - Wiederholt wird bei: Timeout, Verbindungsfehler sowie den Statuscodes 408, 409, 429 und allen ab 500. Verlangt der Server im Antwort-Header ausdrücklich eine Wiederholung oder keine, hält es sich daran.
- Zwischen zwei Versuchen wird gewartet, beginnend bei 0,5 Sekunden und jeweils verdoppelt, höchstens 8 Sekunden.
- Das Standard-Timeout beträgt 600 Sekunden, davon höchstens 5 Sekunden für den Verbindungsaufbau.
Selbst wenn du nichts schreibst, werden gelegentliche 429er und 500er also vom SDK stillschweigend wiederholt. Das ist gut, aber zwei Punkte sind zu beachten.
600 Sekunden Timeout sind zu lang. Kein Nutzer wartet 10 Minuten auf einer Webseite. Für normale Gespräche empfehle ich ein Timeout von 30–60 Sekunden; bei komplexen Aufgaben mit Denken darf es länger sein. Beim Erzeugen des Clients einfach timeout=60 übergeben.
Du siehst nicht, dass wiederholt wurde. Die Wiederholungen des SDK laufen still; du weißt nicht, dass ein Aufruf eigentlich dreimal versucht und über zehn Sekunden gewartet hat. Wenn du untersuchst, „warum es so langsam ist“, ist diese Information wichtig.
Eine Aufruffunktion für das Projekt
Deshalb schalte ich die eingebauten Wiederholungen des SDK meist ab und schreibe eine eigene Aufruffunktion, die Wiederholung, Drosselung und Buchführung bündelt:
client = OpenAI(
api_key=os.environ["LLM_API_KEY"],
base_url=BASE_URL,
timeout=60, # 单次请求最多等 60 秒
max_retries=0, # 关掉 SDK 自带的重试,由下面的函数统一处理,方便记录
)
RETRYABLE = (openai.RateLimitError, openai.APITimeoutError, openai.APIConnectionError, openai.InternalServerError)
limiter = threading.Semaphore(5) # 同一时刻最多 5 个请求在路上
log_lock = threading.Lock()
LOG = Path("calls.jsonl")
def call_llm(messages, max_attempts=4, **kwargs):
for attempt in range(1, max_attempts + 1):
start = time.time()
try:
with limiter:
response = client.chat.completions.create(model=MODEL, messages=messages, **kwargs)
except RETRYABLE as e:
if attempt == max_attempts:
raise
# 指数退避:1 秒、2 秒、4 秒……再加一点随机,避免大家同时重试
wait = 2 ** (attempt - 1) + random.random()
print(f" 第 {attempt} 次失败({type(e).__name__}),{wait:.1f} 秒后重试")
time.sleep(wait)
continue
record = {
"time": time.strftime("%Y-%m-%d %H:%M:%S"),
"model": response.model,
"seconds": round(time.time() - start, 2),
"prompt_tokens": response.usage.prompt_tokens,
"completion_tokens": response.usage.completion_tokens,
"cost_usd": round(cost_usd(response.usage, MODEL), 6),
"attempts": attempt,
}
with log_lock:
with LOG.open("a") as f:
f.write(json.dumps(record, ensure_ascii=False) + "\n")
return response
Block für Block erklärt.
Nur wiederholen, was sich zu wiederholen lohnt. RETRYABLE listet die vier wiederholbaren Ausnahmen. Fehler wie 401 oder 400 sind nicht darin und werden direkt geworfen, damit du sie sofort bemerkst.
Exponentielles Backoff mit zufälligem Jitter. Beim ersten Fehlschlag gut 1 Sekunde warten, beim zweiten gut 2, beim dritten gut 4. Die Wartezeit verdoppelt sich, um dem Server Zeit zur Erholung zu geben: Ist die Gegenseite überlastet, verschlimmert sekündliches Wiederholen die Lage nur. Der zufällige Anteil von random.random() verhindert, dass viele Anfragen gleichzeitig scheitern, gleichzeitig wiederholen und so in Wellen anrollen.
Eine Obergrenze für Wiederholungen. Scheitern alle vier Versuche, wird aufgegeben und die Ausnahme an den Aufrufer geworfen. Schreib niemals unendliche Wiederholungen.
Parallelität begrenzen. threading.Semaphore(5) sorgt dafür, dass höchstens 5 Anfragen gleichzeitig laufen. Anbieter begrenzen Parallelität und Anfragen pro Minute (Stand September 2026 nennt die DeepSeek-Dokumentation für deepseek-flash eine Parallelitätsgrenze von 2500, für deepseek-v4-pro 500); selbst vorher zu begrenzen, ist besser, als auf 429 zu warten. Wichtiger noch: Es verhindert, dass ein Bug im Code in einem Augenblick Tausende Anfragen losschickt.
Jeden Aufruf verbuchen. Zeit, Modell, Dauer, Eingabe- und Ausgabe-Tokens, Kosten und Versuche als eine JSON-Zeile an calls.jsonl anhängen. Die Kosten berechnet cost_usd aus Modul 01, Lektion 4. Mit diesem Protokoll lassen sich Fragen beantworten wie „was kostet diese Funktion am Tag“, „welche Anfragen sind besonders langsam“, „wie oft wird wiederholt“. Modul 06, Lektion 3 baut darauf ein vollständiges Logging und Monitoring auf.
Ausprobieren: 20 Anfragen parallel
questions = [f"用一句话解释 HTTP 状态码 {code} 的含义。" for code in [200, 201, 204, 301, 302, 304, 400, 401, 403, 404,
405, 408, 409, 418, 429, 500, 502, 503, 504, 505]]
with ThreadPoolExecutor(20) as pool:
answers = list(pool.map(lambda q: call_llm([{"role": "user", "content": q}], extra_body=NO_THINKING), questions))
20 Threads rufen gleichzeitig auf, aber limiter lässt nur 5 gleichzeitig durch:
== 4. 并发 20 个请求,最多同时 5 个,每次调用记账
20 个请求用了 3.5 秒
第一条回答: HTTP 状态码 200 表示服务器成功处理了请求,并正常返回了所请求的资源。
日志共 20 条,总花费 0.000655 美元,第一条:{'time': '2026-09-14 21:35:48', 'model': 'deepseek-flash', 'seconds': 0.62, 'prompt_tokens': 16, 'completion_tokens': 19, 'cost_usd': 2.8e-05, 'attempts': 1}
20 Anfragen brauchten 3,5 Sekunden; nacheinander hätte es etwa 12 Sekunden gedauert. In diesem Lauf gab es keine Fehler, die eine Wiederholung erforderten, daher steht bei jedem Eintrag attempts auf 1.
Ein paar Sicherungen fürs Geld
Wiederholungen und Parallelitätsbegrenzung schützen vor Pannen, die folgenden vor der Rechnung:
- Für jeden Aufruf
max_tokenssetzen. Verhindert endlose Ausgaben. Mit Denken großzügiger bemessen; Modul 00, Lektion 3 hat gezeigt, dass Denken dieses Kontingent mitverbraucht. - Schleifen eine Obergrenze geben. Tool-Schleifen und Wiederholungsschleifen brauchen eine Höchstzahl. Die Tool-Schleife aus Lektion 3 hatte höchstens 5 Runden, genau deshalb.
- Auf der Plattform eine Guthabenwarnung einrichten. DeepSeek ist Prepaid; ist das Guthaben aufgebraucht, ist Schluss, eine natürliche Obergrenze. Trotzdem nur so viel aufladen, wie für eine Weile nötig ist, und das Guthaben im Blick behalten.
- Das Protokoll lesen. Täglich einen Blick auf die Gesamtkosten in
calls.jsonlwerfen; ungewöhnliches Wachstum fällt sofort auf.
Übungen
- Setz die Parallelität von
limiterauf 1 und auf 20, lass es jeweils einmal laufen und vergleiche die Gesamtdauer der 20 Anfragen. - Schreib eine simulierte Störung: eine Funktion, die bei den ersten beiden Aufrufen
openai.APITimeoutErrorwirft und erst beim dritten wirklich das Modell aufruft. Setz sie incall_llmein und schau dir die Ausgaben zu Wiederholung und Warten sowieattemptsim Protokoll an. (Tipp:openai.APITimeoutError(request=...)braucht einen Parameterrequest; dafür kannst duhttpx.Request("POST", "https://example.com")nehmen.) - Schreib ein kleines Skript, das
calls.jsonlliest und die Gesamtzahl der Aufrufe, die Gesamtkosten, die durchschnittliche Dauer und die drei langsamsten Aufrufe ausgibt.
Selbsttest
1. Soll man bei einem 401-Fehler wiederholen? Und bei 429?
401 bedeutet einen falschen Schlüssel; beliebig viele Wiederholungen ändern nichts, man sollte direkt einen Fehler melden und den Schlüssel prüfen lassen. 429 bedeutet Drosselung wegen zu vieler Anfragen, vorübergehend; man sollte nach einer Weile wiederholen, mit von Mal zu Mal längerer Wartezeit.
2. Warum verdoppelt man die Wartezeit zwischen Wiederholungen und fügt noch etwas Zufall hinzu?
Das Verdoppeln gibt dem Server Zeit zur Erholung; ist die Gegenseite überlastet, verschlimmern häufige Wiederholungen die Lage nur. Der Zufall verteilt die Wiederholungen vieler gleichzeitig gescheiterter Anfragen, damit sie nicht im selben Moment erneut auf den Server einströmen.
3. Wiederholt das openai-SDK automatisch, wenn man nichts einstellt?
Ja. Die aktuelle Version hat standardmäßig max_retries=2 und wiederholt bei Timeout, Verbindungsfehler sowie den Statuscodes 408, 409, 429 und ab 500 nach kurzer Wartezeit automatisch, höchstens zweimal. Das Standard-Timeout von 600 Sekunden ist für die meisten interaktiven Anwendungen zu lang; man sollte timeout selbst setzen.
Fragen und Diskussion
Hängst du in dieser Lektion fest? Frag hier. Und wenn du die Frage von jemandem beantworten kannst, tu es gern.
Eine Frage bringt 3 Punkte, eine Antwort 6. Beiträge erscheinen nach der Prüfung.
Diskussion wird geladen…