ANTIGRAVITY LABEN
記事一覧/Agents & Manager
Agents & Manager/2026-04-13上級

Antigravity AIエージェント耐障害性設計 — リトライ・サーキットブレーカー・フォールバックで本番を守る

AIエージェントを本番で止めないための耐障害性設計をまとめました。リトライ・サーキットブレーカー・モデルフォールバック・チェックポイントの実装に加え、指数バックオフだけではリトライの集中が解消しないことを200エージェント規模で数え直した記録を添えています。

antigravity449agents94error-handling2production59resilience9circuit-breaker3retry8

プレミアム記事

AIエージェントはなぜ本番で壊れるのか

Antigravity で動くAIエージェントを開発環境でテストしているときは、すべてが順調に見えます。しかし本番にデプロイした瞬間、想像もしなかった壊れ方をします。LLM APIが429を返す。レスポンスが途中で途切れます。トークン上限に達してエージェントが中途半端な状態で停止します。

私がこの問題に本格的に向き合ったのは、マルチエージェントパイプラインを本番投入して3日目の深夜2時でした。Gemini APIのレートリミットに引っかかり、5つのエージェントが連鎖的に停止。復旧までに4時間かかっています。

このガイドでは、その経験から構築した耐障害性パターンをすべて公開します。サーキットブレーカー、リトライ戦略、モデルフォールバック、チェックポイント設計——本番環境でAIエージェントを安定稼働させるために必要な実装パターンを、動作するコードとともに解説します。

リトライ戦略の設計 — Exponential Backoff と Jitter の正しい実装

AIエージェントのAPI呼び出しは、従来のWebサービスとは異なるリトライ戦略が必要です。LLM APIは応答に数秒〜数十秒かかるため、単純なリトライでは待ち時間が爆発的に増加します。また、複数エージェントが同時にリトライすると「リトライストーム」を引き起こし、API側の負荷をさらに悪化させます。

なぜ固定間隔リトライではダメなのか

固定間隔(例: 毎回3秒待機)でリトライすると、障害発生時に全エージェントが同じタイミングで再リクエストを投げます。これがいわゆる「Thundering Herd(群集雪崩)」問題で、APIの復旧をかえって遅らせてしまいます。

Exponential Backoff with Jitter(指数バックオフ+ジッター)が標準解です。待ち時間を指数的に増やしつつ、ランダムなゆらぎを加えることで、リトライのタイミングを分散させます。

// Antigravity プロジェクトで使えるリトライユーティリティ
// エージェントのAPI呼び出しをラップして自動リトライする
 
interface RetryConfig {
  maxRetries: number;       // 最大リトライ回数
  baseDelayMs: number;      // 初回待機時間(ミリ秒)
  maxDelayMs: number;       // 待機時間の上限
  jitterFactor: number;     // ジッター係数(0〜1)
  retryableErrors: number[]; // リトライ対象のHTTPステータス
}
 
const DEFAULT_CONFIG: RetryConfig = {
  maxRetries: 5,
  baseDelayMs: 1000,
  maxDelayMs: 60000,
  jitterFactor: 0.5,
  retryableErrors: [429, 500, 502, 503, 504],
};
 
async function withRetry<T>(
  operation: () => Promise<T>,
  config: Partial<RetryConfig> = {}
): Promise<T> {
  const cfg = { ...DEFAULT_CONFIG, ...config };
  let lastError: Error | null = null;
 
  for (let attempt = 0; attempt <= cfg.maxRetries; attempt++) {
    try {
      return await operation();
    } catch (error: unknown) {
      lastError = error instanceof Error ? error : new Error(String(error));
 
      // リトライ対象でないエラーは即座にスロー
      const status = (error as { status?: number }).status;
      if (status && !cfg.retryableErrors.includes(status)) {
        throw error;
      }
 
      if (attempt === cfg.maxRetries) {
        break; // 最大リトライ回数に達した
      }
 
      // Exponential Backoff + Full Jitter
      const exponentialDelay = cfg.baseDelayMs * Math.pow(2, attempt);
      const cappedDelay = Math.min(exponentialDelay, cfg.maxDelayMs);
      const jitter = cappedDelay * cfg.jitterFactor * Math.random();
      const finalDelay = cappedDelay - (cappedDelay * cfg.jitterFactor / 2) + jitter;
 
      console.warn(
        `[Retry ${attempt + 1}/${cfg.maxRetries}] ` +
        `Waiting ${Math.round(finalDelay)}ms before retry. ` +
        `Error: ${lastError.message}`
      );
 
      await new Promise(resolve => setTimeout(resolve, finalDelay));
    }
  }
 
  throw new Error(
    `Operation failed after ${cfg.maxRetries} retries. Last error: ${lastError?.message}`
  );
}
 
// 使用例: Gemini API呼び出しにリトライを適用
const response = await withRetry(
  () => fetch('https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-goog-api-key': process.env.GEMINI_API_KEY ?? '',
    },
    body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }] }),
  }),
  { maxRetries: 3, baseDelayMs: 2000 }
);
// 期待出力: 429エラー時に2s→4s→8sの間隔でリトライし、成功すればレスポンスを返す

指数バックオフだけでは、集中は消えませんでした

上のコードには jitter フラグがあります。実装した当初、私はここを飾りだと思っていました。待ち時間が 2s → 4s → 8s と広がるのだから、それだけで十分に散るはずだ、と考えていたためです。

その思い込みを確かめたくて、手元で数えてみました。200 エージェントが同時に失敗し、それぞれ最大5回リトライする状況を置いて、リトライの到着時刻を 250ms 単位で数えるだけの短いスクリプトです。

// retry-arrival.mjs — リトライ到着時刻の集中度を数える
// 実行: node retry-arrival.mjs
const AGENTS = 200, MAX_RETRY = 5, BASE = 1000, CAP = 30000, BUCKET = 250;
 
function arrivals(kind) {
  const out = [];
  for (let a = 0; a < AGENTS; a++) {
    let t = 0, prev = BASE;
    for (let n = 0; n < MAX_RETRY; n++) {
      let d;
      if (kind === 'fixed') {
        d = 3000;                                            // 固定間隔
      } else if (kind === 'exp') {
        d = Math.min(CAP, BASE * 2 ** n);                    // 指数のみ(ジッターなし)
      } else if (kind === 'full') {
        d = Math.random() * Math.min(CAP, BASE * 2 ** n);    // Full Jitter
      } else {
        d = Math.min(CAP, BASE + Math.random() * (prev * 3 - BASE)); // Decorrelated Jitter
        prev = d;
      }
      t += d;
      out.push(t);
    }
  }
  return out;
}
 
function peak(list) {
  const bucket = {};
  for (const t of list) {
    const k = Math.floor(t / BUCKET);
    bucket[k] = (bucket[k] || 0) + 1;
  }
  return Math.max(...Object.values(bucket));
}
 
for (const kind of ['fixed', 'exp', 'full', 'decorr']) {
  let sum = 0;
  for (let i = 0; i < 20; i++) sum += peak(arrivals(kind));
  console.log(kind.padEnd(7), '250msあたりの最大同時到着:', (sum / 20).toFixed(1));
}

20 回試行の平均です。

fixed   250msあたりの最大同時到着: 200.0
exp     250msあたりの最大同時到着: 200.0
full    250msあたりの最大同時到着: 77.0
decorr  250msあたりの最大同時到着: 36.0

ジッターを使う2行は乱数に依存するため、実行のたびに数個ぶん上下します(手元では full が 74〜78、decorr が 35〜38 の範囲でした)。fixedexp は乱数を含まないため、何度走らせても同じ値になります。

exp の行が、私の思い込みでした。指数バックオフを入れても、ピークは固定間隔とまったく同じ 200 のままです。待ち時間が伸びても、全エージェントが同じ式から同じ値を出すのですから、揃って戻ってくるのは当然でした。

さらに、平均到着密度に対するピークの比を取ると、指数のみの構成は固定間隔より悪くなっていました。到着が数回の鋭いスパイクに圧縮されるためです。

戦略250msあたりの最大同時到着平均密度に対するピーク比
固定間隔(3秒)20012.0
指数バックオフのみ20024.8
Full Jitter778.5
Decorrelated Jitter3612.9

Decorrelated Jitter は絶対量のピークが最も低い一方で、比の列では Full Jitter に劣ります。到着が長い時間へ広がるぶん平均密度が下がり、比の分母が小さくなるためです。API に届く瞬間最大負荷を下げたいのであれば絶対量の列を、監視のしきい値を設計したいのであれば比の列を見る、という使い分けになります。

散らしているのはジッターであって、指数の部分ではありませんでした。指数バックオフは「1エージェントが投げる総回数を抑える」ために効き、ジッターは「複数エージェントの到着を重ねない」ために効きます。効く相手が違う2つを、私は1つだと思っていたわけです。

jitter: false のまま動いているコードが手元にあるなら、そこだけは今日のうちに true にしておく価値があります。1行の変更で、障害時に API へ届く瞬間最大負荷が半分以下になります。

リトライと冪等性

ここで見落としがちな問題があります。リトライするということは、同じ操作を複数回実行する可能性があるということです。LLMへの問い合わせ自体は冪等ですが、エージェントが「ファイルに書き込む」「APIを呼ぶ」といった副作用を伴うアクションを実行する場合、リトライが重複実行を引き起こします。

対策は、アクション実行前にチェックポイントを記録し、リトライ時に「すでに実行済みか」を確認するパターンです。これについては後のセクションで詳しく解説します。

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
AIエージェントが本番で突然止まる問題を、サーキットブレーカーとリトライ戦略で根本解決できる
モデルフォールバック・チェックポイント・グレースフルデグラデーションの実装パターンを習得し、自分のプロダクトに即適用できる
エージェントの障害を検知・復旧・予防する監視基盤を構築し、深夜のインシデント対応から解放される
Stripe による安全な決済 · いつでもキャンセル可能

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Antigravity Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥2,480 の永久アクセス
メンバーシップを見る →

関連記事

Agents & Manager2026-05-22
Antigravity Agent のフォールバック階層設計 — モデル劣化・API障害・コスト超過を 4 段で受け止める Resilience アーキテクチャ
モデル応答の劣化・API 障害・コスト超過を4段階のフォールバック階層で受け止める Antigravity Agent の設計を解説します。Gemini 3 Pro から Gemma 4 ローカル推論、キャッシュ応答までの階層遷移を、6アプリ並行運用の実装ログに基づき整理しました。
アプリ開発2026-04-23
Antigravity Python API を本番で落とさない — リトライ・タイムアウト・サーキットブレーカー実装ガイド
Antigravity で開発する Google Gen AI SDK アプリケーションを本番環境で安定稼働させるための、リトライ・タイムアウト・サーキットブレーカー・バックプレッシャー設計を、動作する Python コードと運用経験から体系的に解説します。
Agents & Manager2026-07-05
エージェント開発スタックの『既知の正常』を1枚のロックファイルで守る — 可動部が同時に動く時代の変更予算設計
IDEビルド・CLI・モデル・依存が同時に動くと、回帰の原因が特定できなくなります。既知の正常を1枚のロックファイルに固定し、一度に動かす軸を絞る変更予算の設計を、実装コードと運用ログで整理しました。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →