「開発中は完璧だったのに、本番でトラフィックが乗った瞬間に落ちた」——LLMアプリで頻発する事故です。理由はシンプルで、LLMアプリは、非決定的な外部APIに依存する分散システムだからです。プロバイダ側の一時障害、レート制限(429)、レイテンシスパイク、そしてコスト暴走。これらは「起きるか」ではなく「いつ起きるか」の問題です。
結論から言えば、本番LLMアプリの回復性(resilience)は、モデルではなくアーキテクチャで作ります。柱は6つ。
- タイムアウト — 返らない呼び出しで止まらない。
- リトライ(一時障害のみ・指数バックオフ+ジッター) — 一過性の失敗を吸収する。
- サーキットブレーカ — 落ちているプロバイダへの攻撃を止め、カスケード障害を防ぐ。
- 多プロバイダ・フォールバック — 1社の障害でサービスを止めない。
- 冪等性 — リトライや二重呼び出しで副作用を二重に起こさない。
- レート制限・コスト上限 — 濫用と暴走を封じる。
この記事は、決済基盤で本番二重課金0件を実現してきた信頼性・冪等性の設計の知見を、生成AIに持ち込みます。AI SDK / Vercel AI Gateway の現行APIと、AWS・Google SRE の一次情報に準拠し、実コードで示します。これは、セキュリティ・MCP・評価・プライバシーに続く「信頼できる本番AI」の第5の柱です。
LLM呼び出しは「失敗する前提」で設計する
まず、どんな失敗が起きるかを分類します。対応が変わるからです。
| 失敗 | 例 | 対応 |
|---|---|---|
| 一時障害 | 429(レート制限)、500/502/503/504、接続断、タイムアウト | リトライ(バックオフ+ジッター)→ ダメならフォールバック |
| 恒久エラー | 400(不正)、401/403(認可)、422 | リトライしない(成功しない・コストの無駄) |
| 部分的劣化 | 応答は返るが遅い/品質が低い | タイムアウト+評価で検知 |
HTTPの意味論は明確です。429(Too Many Requests)は RFC 6585、Retry-After ヘッダと 503 は RFC 9110 が定義します。Retry-Afterが返ってきたら、独自のバックオフよりそれを優先します。
タイムアウトとリトライ:一時障害だけを、ジッター付きで
リトライの鉄則は2つ。「一時障害だけ」「冪等な呼び出しだけ」。Google SREも「恒久エラーや不正なリクエストはリトライしない——決して成功しないから」と述べています。
そしてジッター(ゆらぎ)。全クライアントが同じ間隔で再試行すると、再試行の山が同期して群集雪崩(thundering herd)を起こし、復旧しかけたプロバイダを再び潰します。AWSの推奨は、指数バックオフにフルジッター(0〜backoffの一様乱数)を加えて山を平準化することです。
まず良い知らせ:Vercel AI SDK は一時障害を既定でリトライします(maxRetries、既定=2)。多くのケースはこれで足ります。
import { generateText } from "ai";
// AI SDK は一時障害を既定で2回リトライ(maxRetries=2)。timeout は AbortSignal で。
// Vercel AI Gateway なら「モデル・フォールバック」を宣言的に書ける:
// primary が失敗したら順にバックアップを試し、最初に成功した応答を返す。
const { text } = await generateText({
model: "anthropic/claude-opus-4-8",
prompt,
maxRetries: 2, // 既定値。多層でリトライを重ねない
abortSignal: AbortSignal.timeout(15_000), // 15秒で打ち切り
providerOptions: {
gateway: {
models: ["anthropic/claude-opus-4-8", "openai/gpt-5"], // 順にフォールバック
},
},
});
自前でリトライを制御したい場合(ゲートウェイを使わない、独自のバックオフが要る)は、一時障害だけを、指数バックオフ+フルジッターで再試行します。
// 自前リトライ:一時障害(429/5xx/timeout)だけを、指数バックオフ+フルジッターで。
const RETRYABLE = new Set([429, 500, 502, 503, 504]);
async function withRetry<T>(fn: () => Promise<T>, retries = 2, baseMs = 500): Promise<T> {
for (let attempt = 0; ; attempt++) {
try {
return await fn();
} catch (err) {
const status = (err as { statusCode?: number }).statusCode;
// 恒久エラー(4xx)はリトライしない。上限に達したら諦める(無限リトライ=コスト暴走)。
if (attempt >= retries || status === undefined || !RETRYABLE.has(status)) throw err;
const backoff = Math.min(baseMs * 2 ** attempt, 20_000);
const wait = retryAfterMs(err) ?? Math.random() * backoff; // Retry-After優先、無ければfull jitter
await new Promise((resolve) => setTimeout(resolve, wait));
}
}
}
サーキットブレーカ:落ちた相手を叩き続けない
一時障害が続くとき、リトライは逆効果になります。落ちているプロバイダを叩き続ければ、カスケード障害とコスト浪費を招くだけ。そこでサーキットブレーカ——連続失敗が閾値を超えたら回路を open(即失敗)にし、cooldown後に half-open で1回だけ試して復旧を確認します(Martin Fowler)。
テスト容易性のため、時刻を引数で注入します(Date.now()に依存させない=決定的に単体テストできる)。
// サーキットブレーカ:連続失敗が閾値超で一定時間 open(即失敗)。
// now を注入してテスト可能に(closed → open → half-open)。
class CircuitBreaker {
private failures = 0;
private openedAt: number | null = null;
constructor(
private readonly threshold = 5,
private readonly cooldownMs = 30_000,
) {}
allow(now: number): boolean {
if (this.openedAt === null) return true; // closed:通常通り
if (now - this.openedAt >= this.cooldownMs) return true; // half-open:試行を1回許可
return false; // open:即失敗(叩かない)
}
onSuccess(): void {
this.failures = 0;
this.openedAt = null;
}
onFailure(now: number): void {
if (++this.failures >= this.threshold) this.openedAt = now;
}
}
多プロバイダ・フォールバック:1社の障害で止めない
回復性の本丸です。primaryプロバイダが落ちたら、secondaryに切り替える。Vercel AI Gateway なら前述の providerOptions.gateway.models(モデル順)や order(プロバイダ順)で宣言的に書けます。自前で管理するなら、サーキットブレーカとリトライを組み合わせたフォールバック連鎖にします。
// 多プロバイダ・フォールバック:primary が落ちたら次へ。open 中のプロバイダはスキップ。
const CHAIN = ["anthropic/claude-opus-4-8", "openai/gpt-5"] as const;
const breakers = new Map(CHAIN.map((model) => [model, new CircuitBreaker()]));
async function generateResilient(prompt: string, now: () => number): Promise<string> {
let lastErr: unknown;
for (const model of CHAIN) {
const breaker = breakers.get(model)!;
if (!breaker.allow(now())) continue; // サーキットが open ならこのプロバイダは飛ばす
try {
// withRetry が再試行を担うので、SDK側は maxRetries:0(多層リトライを避ける)
const { text } = await withRetry(() =>
generateText({ model, prompt, maxRetries: 0, abortSignal: AbortSignal.timeout(15_000) }),
);
breaker.onSuccess();
return text;
} catch (err) {
lastErr = err;
breaker.onFailure(now());
}
}
throw new Error(`all providers failed: ${String(lastErr)}`);
}
注意点は品質の一貫性です。フォールバック先はモデルが変わるため、出力の質・形式がぶれます。切替時も品質を保つには、structured output の信頼性設計でスキーマ検証し、評価(eval)で継続的に品質を測ります。
retry amplification に注意。 SDK・自前リトライ・ゲートウェイ・上流サービス——各層がリトライを持つと、再試行回数が掛け算になり、障害時に負荷を爆発させます。Google SREの原則は明快で、「リクエストあたりのリトライを制限する」。リトライは1つの層に集約し、他は0にします。
冪等性:リトライで副作用を二重に起こさない
リトライとフォールバックは、副作用のある操作では諸刃の剣です。1回目が実は成功していた(応答だけ失われた)場合、リトライは二重実行になります。LLMエージェントは同じ意図でツールを複数回呼ぶこともあり、リスクはさらに高い。
対策は冪等キー。同じキーの操作は1回だけ実行されることを、下流で保証します(MCPのツール実行設計や決済基盤と同じ)。
// 副作用のある操作は冪等キーで保護。リトライ/二重呼び出しでも一度きり。
async function chargeIdempotent(orderId: string, amountJpy: number): Promise<Receipt> {
const key = `charge:${orderId}:${amountJpy}`; // 意図が同じなら同じキー
const existing = await receipts.get(key);
if (existing) return existing; // 既に処理済みなら再実行しない
const receipt = await paymentApi.charge(orderId, amountJpy, { idempotencyKey: key });
await receipts.put(key, receipt);
return receipt;
}
レート制限とコスト回復性
信頼性はコストの問題でもあります。リトライやループが暴走すれば、可用性ではなく請求書が壊れます。対策はセキュリティ記事のOWASP LLM10「Unbounded Consumption」と同じ——出力トークン上限(maxOutputTokens)、per-userレート制限、タイムアウト、そしてリトライ回数の上限。Google SREの「過負荷時は劣化した応答を返す(load shedding / graceful degradation)」も有効な選択肢です。
可観測性で回復性を運用する
これらの仕組みは、測って初めて機能します。失敗率・フォールバック率・サーキットのopen時間・p95レイテンシ・リトライ回数・コストを記録し、異常を検知します。「フォールバックが常時発火している=primaryが慢性的に不調」のように、指標が設計の穴を教えてくれます。土台は OpenTelemetryによる本番可観測性 を参照してください。
導入チェックリスト(本番投入前)
- タイムアウト — すべてのLLM呼び出しに
abortSignal(AbortSignal.timeout)を設定したか。 - リトライ範囲 — 429/5xx/タイムアウトの一時障害だけ、かつ冪等な呼び出しだけをリトライしているか。
- バックオフ+ジッター — 指数バックオフにフルジッター、Retry-After優先になっているか。
- retry amplification — リトライを1層に集約し、多層で重ねていないか(上限も設定)。
- サーキットブレーカ — 連続失敗で落ちたプロバイダを一時遮断しているか。
- 多プロバイダ・フォールバック — 1社障害で止まらないか。フォールバック時の品質を評価で担保しているか。
- 冪等性 — 副作用のある操作を冪等キーで保護し、二重実行を防いでいるか。
- コスト回復性 — トークン上限・レート制限・リトライ上限で暴走を封じているか。
- 可観測性 — 失敗率・フォールバック率・レイテンシ・コストを監視し、劣化を検知できるか。
まとめ
本番LLMアプリの信頼性は、特別な魔法ではなく、分散システムの回復性設計をLLMに適用することです。「LLMは失敗する外部依存」と割り切り、タイムアウト・(一時障害限定の)リトライ・サーキットブレーカ・多プロバイダ・フォールバック・冪等性で被害を封じ込める。
- リトライは一時障害だけ・冪等だけ・ジッター付き。 多層で重ねない(retry amplification)。
- サーキットブレーカで落ちた相手を叩かない。 多プロバイダ・フォールバックで1社障害に耐える。
- 冪等キーで二重実行を防ぎ、レート制限とコスト上限で暴走を封じる。
セキュリティ・MCP・評価・プライバシー・信頼性——この5本柱が揃って、生成AIは「速く作れて、安全で、壊れず、信頼でき、そして止まらない」本番システムになります。作る速さと、止めない堅さは、正しく設計すれば両立します。