LLM APIが止まった時のフォールバック設計:代替モデル・縮退表示・再送の決め方
AIを使いこなすClaudeで執筆し、GPTで自動校閲・修正しています。

先に結論です。LLM APIの障害対応は「①同じモデルへ再送するか、②代替モデルへ切り替えるか、③AIなしの縮退表示にするか」を、エラーの種類と連続失敗の度合いで段階的に決めます。再送は、一時的な混雑・サーバーエラー・タイムアウトなどからの回復を狙う手段です。課金上限・認証・権限の問題は、同じリクエストの再送や、同じ制限が適用される代替モデルへの切り替えでは解消しません。この場合は運営者への通知と縮退表示に進みます。 この記事では、障害レベル別の対応表、エラー別の再送判断表、利用者向けメッセージ例、短い実装例を示します。エラーの意味は、各社の公式ドキュメントを2026年10月28日に確認した範囲で書いています。しきい値や文言は、公式仕様ではなく編集上の出発点です。自社のサービスに合わせて調整してください。
まず「再送してよいエラー」を切り分ける
最初にエラーを「待てば直るもの」と「直らないもの」に分けます。公式資料から読み取れる範囲では、次のように整理できます。
- Claude API:429は組織のレート制限、または利用ティアの月次上限などに到達した場合です。上限到達の429にはretry-afterヘッダーがなく、アクセスが再開されるまで失敗し続けると説明されています。500は内部エラーで、指数バックオフでの再試行が案内されています。529は全利用者のトラフィック集中などによる一時的な過負荷、504はタイムアウトです。自分で設定した組織・ワークスペースの支出上限に達した場合は400が返ります。(Claude API errors)
- OpenAI API:429には、クレジット枯渇、レート制限、急な増加への制限、支出上限、利用上限超過といった別々の原因があります。503はモデルの容量不足で、Retry-Afterがあれば従います。課金・支出・利用上限のエラーは再試行では解消しないため、error.codeで原因を判別します。レート制限は、Retry-Afterがなければ回数を制限したジッター付き指数バックオフで再試行します。(OpenAI API error codes)
- Gemini API:再試行するのは429、408、5xxなどの一時的なエラーだけです。400、402、403などのクライアント側エラーは再試行しません。(Gemini API troubleshooting)
三社に共通するのは、同じ429でも原因が複数あることです。そのため「429なら一律に待って再送」とは書かないほうが安全です。次の表を、再送判断の出発点にしてください。
再送判断表(編集上の提案)
- 500/502/503/529(過負荷・内部エラー)、再送:する(最大2回程度、ジッター付き)、次の動き:失敗が続けば代替モデルへ
- 504/408(タイムアウト)、再送:1回だけ、次の動き:長い生成はストリーミングに切り替えを検討
- 429で一時的なレート制限と確認でき、retry-afterあり、再送:指定時間以上待って1回、次の動き:待てない画面なら、同じ制限が適用されない代替モデルへ
- 429で一時的なレート制限と確認でき、retry-afterなし、再送:回数を制限したジッター付き指数バックオフ、次の動き:失敗が続けば、同じ制限が適用されない代替モデルへ
- 429でクレジット枯渇・支出上限・利用上限、または原因不明、再送:しない、次の動き:運営者へ通知し、縮退表示。ヘッダーの有無だけでは原因を判定しない
- 400/401/402/403、再送:しない、次の動き:設定・課金・権限・リクエスト形式の確認。縮退表示にして通知
- ストリーミング途中のエラー、再送:自動で上書き再送しない、次の動き:途中までの表示を残し、利用者に再生成を選ばせる
最後の行について補足します。Claudeのドキュメントによると、SSEでは200を返したあとにエラーが起きることがあり、通常のエラー処理とは異なります。途中まで表示した文章を黙って別の回答で置き換えると、利用者が混乱します。
SDKの自動再試行にも注意が必要です。Claudeの公式SDKは、接続エラー・レート制限・5xxを指数バックオフで既定2回まで自動再試行します。自作の再送ロジックと重ねると、回数が掛け算になります。どちらかの回数を明示的に決めてください。
障害を4段階に分けて機能を落とす
再送と切り替えの判断を、その場の実装者任せにすると、画面ごとに挙動がばらつきます。次のように障害レベルを先に決め、レベルごとに「何を続け、何を止めるか」を表にしておきます。
障害レベル別の対応表(説明用の例)
- L0 正常、状態の目安(例):失敗がまれ、システムの動き:通常のモデルで応答、利用者に見せるもの:なし
- L1 一時的な遅延、状態の目安(例):単発の5xx・529・タイムアウト、システムの動き:同じモデルへ短い再送。20秒程度で打ち切り、利用者に見せるもの:「処理に時間がかかっています」
- L2 継続的な混雑、状態の目安(例):直近数分で失敗が目立つ、システムの動き:代替モデルへ切り替え。出力形式を検証、利用者に見せるもの:「簡易モードで回答しています」
- L3 AI機能の停止、状態の目安(例):本命も代替も失敗、システムの動き:AIなしの縮退。入力の保存と後処理キュー、利用者に見せるもの:「AI機能を一時停止中です」と代わりの操作
- L4 自社設定が原因、状態の目安(例):401・402・403、上限到達の429、システムの動き:再送せず運営者へ即時通知。縮退表示、利用者に見せるもの:原因は出さず「一時的に利用できません」
L2・L3に上げる条件(たとえば「直近1分の失敗率が半分を超えたら」)は、自社のトラフィックで決めてください。ここでは数値を示しません。根拠のある推奨値を、公式資料から確認できていないためです。
縮退の中身は機能ごとに決める
縮退表示で「何を出すか」は、機能の性質で変わります。
- 要約・下書き生成:入力を保存し、「あとで通知」にする。または手入力のフォームに切り替える。
- チャット窓口:定型のFAQ検索、問い合わせフォームへの誘導。
- 分類・タグ付け:ルールベースの暫定分類を付け、あとでAIで再処理する。
- 副作用のある処理(メール送信や注文など):AIが判断できない間は自動実行しない。確認画面を人が操作する形に戻す。
副作用のある処理は、再送で二重実行しないことが前提です。ツール呼び出しを伴う場合は、自社側で冪等キーを持たせる設計にしてください。
代替モデルに切り替える時の落とし穴
代替モデルは「同じプロンプトを別の宛先に送る」だけでは動かないことがあります。Claudeのドキュメントでは、モデルごとに受け付けないパラメータがあり、たとえば思考設定や強制ツール呼び出しの指定がモデルによって400エラーになる例が挙がっています。他社に切り替えるなら、なおさら形式の差があります。事前に次の点を確認してください。
- 本命と代替で、プロンプト・出力スキーマ・パラメータを別々に用意する(共通化しすぎない)。
- 代替の出力も、本命と同じJSONスキーマ検証にかける。通らなければ縮退へ進む。
- 他社へ送ることになる場合は、個人情報や機密の扱いが利用規約・契約・プライバシーポリシーと矛盾しないかを、切り替え設計の前に確認する。
- 代替の品質が低い機能は、無理に切り替えず縮退を選ぶ。
切り替え先は、同じ提供元の別モデルか、別の提供元かで考え方が変わります。同じ提供元では、全体的な過負荷だと両方が影響を受けることがあります。ただし、モデルごとの容量差は公式資料では確認できていないため、本文では断定しません。重要な機能は、別の提供元も含めて検討してください。
実装例:再送→代替→縮退の流れ
次のコードは考え方を示す説明用の例で、実行して検証したものではありません。primary・backup・degradedは、各自のSDK呼び出しを包んだ関数だと想定しています。呼び出しを包む側でエラー本文を調べ、一時的なレート制限と確認できた429だけretryable_429=Trueに変換します。OpenAIではerror.codeなどで原因を判別し、Retry-Afterの有無だけでは判定しません。
import random, time
RETRYABLE = {408, 500, 502, 503, 504, 529, 429}
CONFIG_ERRORS = {400, 401, 402, 403}
class LLMError(Exception):
def __init__(self, status, retry_after=None, retryable_429=False):
self.status, self.retry_after = status, retry_after
self.retryable_429 = retryable_429
def call_with_fallback(req, primary, backup, degraded,
max_retry=2, deadline=20, alert=print):
start = time.monotonic()
for name, provider in (("primary", primary), ("backup", backup)):
for attempt in range(max_retry + 1):
try:
return provider(req), name
except LLMError as e:
if e.status in CONFIG_ERRORS:
alert(f"{name}: config error {e.status}")
return degraded(req), "degraded"
if e.status not in RETRYABLE:
break
if e.status == 429 and not e.retryable_429:
alert(f"{name}: billing, quota, or unclassified 429")
return degraded(req), "degraded"
wait = e.retry_after or min(2 ** attempt, 8) + random.random()
if time.monotonic() - start + wait > deadline:
break # 待てる時間を超えたら次へ
time.sleep(wait)
return degraded(req), "degraded"返り値の第2要素(primary/backup/degraded)は、画面のメッセージ切り替えと、障害レベルの集計に使います。max_retryとdeadlineは、SDK側の自動再試行の設定と合わせて決めてください。この実装では、400・401・402・403と、一時的なレート制限だと確認できない429は縮退にしています。リクエスト形式の不備も含むため、本番ではログにrequest-id(Claudeはエラー本文とヘッダーで返します)を残し、原因を切り分けられるようにしてください。
利用者向けメッセージ例
文言は、原因の推測を避け、「今できること」を必ず添えます。次は説明用の例です。
- L1(遅延):「回答の準備に時間がかかっています。このままお待ちいただくか、しばらくしてからもう一度お試しください。」
- L2(簡易モード):「現在、混み合っているため簡易モードで回答しています。内容が普段と異なる場合があります。重要な内容は確認のうえご利用ください。」
- L3(停止):「AI機能を一時的にご利用いただけません。入力した内容は保存しています。復旧後に通知することもできます。それまでは【手入力フォーム/FAQ検索】をご利用ください。」
- L4(自社要因):「現在この機能をご利用いただけません。時間をおいて再度お試しください。」(利用者には、課金や設定の詳細を出さない。運営者側にだけ通知する)
- ストリーミング中断:「回答の途中で通信が中断されました。表示済みの部分は残しています。続きが必要な場合は『再生成』を押してください。」
メッセージを実装する時は、「再試行してください」ボタンを連打できないようにします。送信の無効化と、再送の最小間隔を設けないと、混雑時に自社のリクエストが増え、かえって復旧を遅らせます。
運用前に確認する4項目
- 本命・代替・縮退の3経路を、エラーを人工的に返すモックで通してみたか。
- 429の原因(レート制限か、支出上限か)を、運営者向けのログで見分けられるか。
- SDKの自動再試行回数と自作ロジックの合計を把握しているか。
- 縮退中に保存した入力を、復旧後に処理する手順があるか。
最初は、障害レベル表のL1とL3だけを実装しても構いません。再送の上限と、AIなしで続けられる最小機能が決まっているだけで、障害時の混乱はかなり減ります。

