troubleshooting
DeepSeek APIのエラーを「起こり得るエラー」と「致命的エラー」に分類して扱う
リード
DeepSeek API のエラーを「全部リトライ」で処理すると、課金エラーを握りつぶしたり、レート制限を悪化させたりする。 この記事ではエラーを 再試行可否 × 想定内/想定外 の2軸で4分類し、コード上の扱いを分ける方法を示す。
前提
- 想定読者: DeepSeek API をアプリに組み込み始めたエンジニア
- 必要な環境: Python 3.10 以上、
openaiSDK もしくは HTTP クライアント - 前提知識: HTTP ステータスコード、指数バックオフの基礎
注記: DeepSeek API は OpenAI 互換の形式でエラーを返すことが多いですが、ステータスコードの体系・レート制限の仕様は変更され得ます。実装前に公式ドキュメントで最新のエラー一覧を確認してください。
なぜ「分類」から始めるのか
エラー処理が破綻する典型パターンは、原因の異なるエラーを1つの except Exception で扱うことです。分類を先に決めると、次の3つが機械的に決まります。
- リトライするか(待てば直るのか、待っても直らないのか)
- 誰に知らせるか(ユーザーに返す / ログのみ / 即アラート)
- どこで捕まえるか(呼び出し側 / 共通ラッパー / 上位のジョブ監視)
2つの軸で4分類する
| 想定内(正常運用でも起きる) | 想定外(実装ミス・障害) | |
|---|---|---|
| 再試行で解決し得る | レート制限・一時的なタイムアウト | サーバ側 5xx、ネットワーク断 |
| 再試行しても解決しない | 入力バリデーション違反、コンテキスト長超過、残高不足 | APIキー不正、エンドポイント誤り、SDKの誤用 |
この4象限を、そのまま例外クラスにします。
class DeepSeekError(Exception):
"""DeepSeek 関連エラーの基底"""
class RetryableError(DeepSeekError):
"""待てば直る可能性がある。指数バックオフで再試行する"""
class BusinessError(DeepSeekError):
"""入力起因。ユーザーに理由を返して終了する"""
class FatalError(DeepSeekError):
"""設定・実装ミス。再試行せず即アラートし、処理を止める"""
分類1: 一時的エラー(再試行する)
レート制限(429)やゲートウェイ系の 5xx、タイムアウトが該当します。指数バックオフ+ジッターを入れ、試行回数に上限を設けます。
import random
import time
RETRYABLE_STATUS = {408, 409, 429, 500, 502, 503, 504}
def with_retry(fn, max_attempts: int = 5):
for attempt in range(max_attempts):
try:
return fn()
except RetryableError:
if attempt == max_attempts - 1:
raise
# 1, 2, 4, 8... 秒 + ジッター。同時刻の再試行集中を避ける
wait = (2 ** attempt) + random.uniform(0, 0.5)
time.sleep(wait)
分類2: 恒久的エラー(再試行しない)
401(認証)、403、404(パス・モデル名の誤り)、402(残高不足)など。リトライしても成功率は上がらず、課金やレート制限の枠を無駄に消費するだけです。即座に失敗させ、アラート対象にします。
分類3: 想定内のビジネスエラー(ユーザーに返す)
400 / 422 系の入力不正、メッセージ長・コンテキスト長の超過、コンテンツポリシーによる拒否など。これは「異常」ではなく正常な分岐です。例外として投げっぱなしにせず、呼び出し元でユーザー向けメッセージに変換します。
分類4: 実装ミス由来のエラー(テストで潰す)
引数の型違い、messages の構造誤り、ストリーミング時の扱い誤りなど。本番で起きたら設計不良のサインなので、分類3と混ぜずにログへ詳細を残し、テストで再現させる対象です。
ステータスコード別の一次対応表
代表例です。実際のコード体系は公式ドキュメントで確認してください。
| ステータス | 分類 | 再試行 | 一次対応 |
|---|---|---|---|
| 400 | 入力エラー | しない | リクエストボディを検証 |
| 401 | 設定ミス | しない | APIキーの有効性・環境変数を確認 |
| 402 | 課金・残高 | しない(チャージ後) | 残高確認、アラート通知 |
| 404 | 設定ミス | しない | エンドポイント・モデル名を確認 |
| 422 | 入力エラー | しない | パラメータの型・値を検証 |
| 429 | 一時的 | する | バックオフ、並列度を下げる |
| 500 / 502 / 503 / 504 | 一時的 | する | リトライ後も続けばアラート |
| タイムアウト | 一時的 | 冪等ならする | タイムアウト値の見直し |
分類を判定するヘルパー
SDK の例外をステータスで振り分ける薄いラッパーを1枚かませると、呼び出し側に if status == ... が散らばりません。
def classify(exc: Exception) -> DeepSeekError:
status = getattr(exc, "status_code", None)
if status in RETRYABLE_STATUS:
return RetryableError(str(exc))
if status in (401, 403, 404, 402):
return FatalError(str(exc))
if status in (400, 422):
return BusinessError(str(exc))
# 判定不能は「想定外」として扱う。握りつぶさない
return FatalError(f"unclassified: {exc!r}")
なぜ最後を FatalError にするか: 未知のエラーを Retryable に倒すと無限リトライで障害が長引くためです。未知は「人間が見る」に寄せます。
つまずきポイント
| 症状 | 対処 |
|---|---|
| 429 でリトライが雪崩を起こす | ジッター付きバックオフ+同時実行数の制限 |
| 401 をリトライし続けて遅くなる | 認証系は再試行対象から除外 |
| 課金エラーが握りつぶされ、静かに失敗する | 402 はアラート必須。ログに残す |
except Exception で全部同じ処理 |
上記4分類に振り分ける |
| タイムアウト時に二重課金が心配 | 非冪等な処理はリトライせず、IDで重複検知 |
まとめ
- エラーは「再試行可否」と「想定内/想定外」の2軸で4分類すると、扱いが機械的に決まる
- 一時的エラーのみ指数バックオフ+ジッターで再試行し、上限を必ず設ける
- 未知のエラーは再試行に倒さず、アラート対象にして人間が確認する
チェックリスト
- リトライ対象のステータスを明示的に列挙した
- バックオフにジッターを入れ、最大試行回数を設定した
- 認証・課金エラーを再試行対象から除外した
- 入力エラーをユーザー向けメッセージに変換している
- 未知のエラーを握りつぶさず、ログとアラートに回している
- エラー分類ごとにテストを書いた