deepseek.ac日本語で学ぶ、実践する

troubleshooting

DeepSeek APIのエラーを「起こり得るエラー」と「致命的エラー」に分類して扱う

· 約10分

リード

DeepSeek API のエラーを「全部リトライ」で処理すると、課金エラーを握りつぶしたり、レート制限を悪化させたりする。 この記事ではエラーを 再試行可否 × 想定内/想定外 の2軸で4分類し、コード上の扱いを分ける方法を示す。

前提

注記: 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で重複検知

まとめ

チェックリスト