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

AIエージェントのエラーリカバリ設計 ― 止まらないパイプラインを構築する実践パターン

AIエージェントが本番で遭遇する典型的な障害パターンと、パイプラインを止めずに自動復旧させるリトライ・フォールバック・サーキットブレーカーの実装を解説します。

agents90error-recoverypipeline4resilience7production59

「エージェントが動かなくなりました」という報告を受けて調べてみると、外部APIの一時的なレート制限に引っかかって、リトライもせずにそのまま停止していた――個人的にこの経験をしてから、エージェントのエラーリカバリ設計を根本から見直すことにしました。

人間が操作するアプリケーションなら「エラーが出たのでもう一度お試しください」で済む。しかしAIエージェントは自律的に動き続けるものです。エラーが発生したとき、自分で判断して復旧できなければ、エージェントを使う意味が半減します。

ここではAIエージェントパイプラインで発生する典型的な障害パターンを整理し、それぞれに対応するリカバリ戦略を実装コード付きで解説します。

エージェント特有の3つの障害パターン

Webアプリケーションでの一般的なエラーハンドリングとは異なり、AIエージェントには固有の障害パターンがあります。

パターン1:幻覚ループ。エージェントがツール呼び出しの結果を誤解釈し、存在しないリソースに何度もアクセスを試みる。たとえばファイル一覧の取得結果を「ファイルが1000個ある」と誤認し、存在しないファイルパスへのアクセスを繰り返すようなケースです。

パターン2:ツール連鎖失敗。ステップAの出力をステップBの入力にする多段パイプラインで、途中のステップが失敗するとそれ以降の全ステップが連鎖的に崩れます。単純なリトライでは解決しません。

パターン3:コンテキスト溢れ。長時間動作するエージェントがコンテキストウィンドウの上限に達し、初期の指示や重要な中間結果を「忘れる」。タスクの方向性がずれたまま動作し続ける。

それぞれに対する防御策を見ていこう。

指数バックオフ付きスマートリトライ

単純なリトライでは不十分です。APIのレート制限に対しては指数バックオフが必要ですし、幻覚ループに対してはリトライ自体を止める判断が必要になります。

interface RetryConfig {
  maxRetries: number;
  baseDelayMs: number;
  maxDelayMs: number;
  retryableErrors: Set<string>;
  // 同じエラーが連続した場合に早期打ち切りする閾値
  sameErrorThreshold: number;
}
 
const DEFAULT_RETRY_CONFIG: RetryConfig = {
  maxRetries: 5,
  baseDelayMs: 1000,
  maxDelayMs: 30000,
  retryableErrors: new Set([
    "rate_limit_exceeded",
    "server_error",
    "timeout",
    "connection_reset",
  ]),
  sameErrorThreshold: 3,
};
 
async function withSmartRetry<T>(
  fn: () => Promise<T>,
  config: RetryConfig = DEFAULT_RETRY_CONFIG
): Promise<T> {
  let lastErrorCode = "";
  let sameErrorCount = 0;
 
  for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error: unknown) {
      const errorCode = (error as { code?: string }).code ?? "unknown";
 
      // リトライ不可能なエラーは即座に投げる
      if (\!config.retryableErrors.has(errorCode)) {
        throw error;
      }
 
      // 同じエラーの連続検知(幻覚ループの兆候)
      if (errorCode === lastErrorCode) {
        sameErrorCount++;
        if (sameErrorCount >= config.sameErrorThreshold) {
          throw new Error(
            `同一エラーが${sameErrorCount}回連続: ${errorCode}(幻覚ループの可能性)`
          );
        }
      } else {
        sameErrorCount = 1;
        lastErrorCode = errorCode;
      }
 
      if (attempt === config.maxRetries) throw error;
 
      // 指数バックオフ + ジッター
      const delay = Math.min(
        config.baseDelayMs * Math.pow(2, attempt) + Math.random() * 500,
        config.maxDelayMs
      );
      await new Promise((r) => setTimeout(r, delay));
    }
  }
  throw new Error("unreachable");
}

ポイントは sameErrorThreshold です。同じエラーコードが3回連続した場合、それはリトライで解決する問題ではなく、エージェントが同じ誤った判断を繰り返している兆候と判断して早期に打ち切る。これが幻覚ループへの第一の防御線になります。

フォールバックチェーンで連鎖失敗を防ぐ

多段パイプラインでは、各ステップに代替手段(フォールバック)を用意しておく点が肝心です。

type StepFn<TIn, TOut> = (input: TIn) => Promise<TOut>;
 
interface PipelineStep<TIn, TOut> {
  name: string;
  primary: StepFn<TIn, TOut>;
  fallbacks: StepFn<TIn, TOut>[];
  validate: (output: TOut) => boolean;
}
 
async function executeStep<TIn, TOut>(
  step: PipelineStep<TIn, TOut>,
  input: TIn
): Promise<{ output: TOut; usedFallback: boolean; method: string }> {
  // プライマリを試行
  try {
    const output = await withSmartRetry(() => step.primary(input));
    if (step.validate(output)) {
      return { output, usedFallback: false, method: "primary" };
    }
  } catch {
    // フォールバックへ
  }
 
  // フォールバックを順に試行
  for (let i = 0; i < step.fallbacks.length; i++) {
    try {
      const output = await withSmartRetry(() => step.fallbacks[i](input));
      if (step.validate(output)) {
        return { output, usedFallback: true, method: `fallback_${i}` };
      }
    } catch {
      continue;
    }
  }
 
  throw new Error(`ステップ "${step.name}" の全手段が失敗`);
}
 
// 使用例:Webページの内容取得ステップ
const fetchContentStep: PipelineStep<string, string> = {
  name: "fetch_content",
  primary: async (url) => {
    // 直接フェッチ
    const res = await fetch(url);
    return res.text();
  },
  fallbacks: [
    async (url) => {
      // キャッシュから取得
      const cached = await cacheStore.get(url);
      if (\!cached) throw new Error("キャッシュなし");
      return cached;
    },
    async (url) => {
      // 簡易版のコンテンツで代替
      return `[コンテンツ取得失敗] URL: ${url} — 要約なしで処理を続行`;
    },
  ],
  validate: (content) => content.length > 0,
};

3番目のフォールバックに注目してほしい。コンテンツ取得が完全に失敗しても、「取得失敗」というメタ情報を渡してパイプラインを止めありません。エージェントはこのメタ情報を受け取って、取得できた他のデータだけで処理を続行します。完璧な結果でなくても、止まるよりはましだという割り切りが、エージェントパイプラインの設計では重要になります。

サーキットブレーカーで障害の波及を止める

外部サービスが長時間ダウンしているとき、リトライを続けることはリソースの無駄遣いであり、タイムアウトの連鎖でパイプライン全体が遅延する原因になります。

enum CircuitState {
  CLOSED = "closed",     // 正常(リクエスト通過)
  OPEN = "open",         // 遮断(即座にエラー返却)
  HALF_OPEN = "half_open" // 試行(1リクエストだけ通す)
}
 
class CircuitBreaker {
  private state: CircuitState = CircuitState.CLOSED;
  private failureCount = 0;
  private lastFailureTime = 0;
  private successCount = 0;
 
  constructor(
    private readonly name: string,
    private readonly failureThreshold: number = 5,
    private readonly recoveryTimeMs: number = 60_000,
    private readonly halfOpenSuccessThreshold: number = 2
  ) {}
 
  async execute<T>(fn: () => Promise<T>): Promise<T> {
    if (this.state === CircuitState.OPEN) {
      if (Date.now() - this.lastFailureTime > this.recoveryTimeMs) {
        this.state = CircuitState.HALF_OPEN;
        this.successCount = 0;
      } else {
        throw new Error(`CircuitBreaker [${this.name}]: OPEN — リクエスト遮断中`);
      }
    }
 
    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }
 
  private onSuccess(): void {
    if (this.state === CircuitState.HALF_OPEN) {
      this.successCount++;
      if (this.successCount >= this.halfOpenSuccessThreshold) {
        this.state = CircuitState.CLOSED;
        this.failureCount = 0;
      }
    } else {
      this.failureCount = 0;
    }
  }
 
  private onFailure(): void {
    this.failureCount++;
    this.lastFailureTime = Date.now();
    if (this.failureCount >= this.failureThreshold) {
      this.state = CircuitState.OPEN;
    }
  }
}
 
// 外部サービスごとにサーキットブレーカーを作成
const openaiBreaker = new CircuitBreaker("openai_api", 3, 30_000);
const searchBreaker = new CircuitBreaker("search_api", 5, 60_000);

サーキットブレーカーは3つの状態を持ちます。正常時は CLOSED(リクエストが通過する)、失敗が閾値を超えると OPEN(即座にエラーを返す)、一定時間後に HALF_OPEN(テスト的に1〜2リクエストを通す)に遷移します。

エージェントパイプラインでは、外部サービスごとに個別のサーキットブレーカーを作成するのがベストプラクティスです。検索APIがダウンしても、それ以外のツール(ファイル操作やコード実行)は正常に使い続けられます。

障害注入テストで耐障害性を検証する

リカバリ機構を実装したら、それが実際に機能するかを検証する必要があります。障害注入(Chaos Engineering)の考え方をエージェントテストに応用します。

class FaultInjector {
  private rules: Map<string, { probability: number; errorType: string }> = new Map();
 
  addRule(toolName: string, probability: number, errorType: string): void {
    this.rules.set(toolName, { probability, errorType });
  }
 
  shouldInjectFault(toolName: string): { inject: boolean; errorType?: string } {
    const rule = this.rules.get(toolName);
    if (\!rule) return { inject: false };
    return Math.random() < rule.probability
      ? { inject: true, errorType: rule.errorType }
      : { inject: false };
  }
}
 
// テスト例
async function testPipelineResilience(): Promise<void> {
  const injector = new FaultInjector();
 
  // 検索APIが50%の確率でタイムアウトする状況をシミュレート
  injector.addRule("web_search", 0.5, "timeout");
  // ファイル書き込みが20%の確率で失敗する状況
  injector.addRule("file_write", 0.2, "disk_full");
 
  const results = { succeeded: 0, failed: 0, usedFallback: 0 };
 
  for (let i = 0; i < 100; i++) {
    try {
      const result = await runPipelineWithInjector(injector);
      results.succeeded++;
      if (result.usedFallback) results.usedFallback++;
    } catch {
      results.failed++;
    }
  }
 
  console.log(`成功: ${results.succeeded}/100`);
  console.log(`フォールバック使用: ${results.usedFallback}`);
  console.log(`完全失敗: ${results.failed}`);
 
  // 成功率90%以上を要求
  if (results.succeeded < 90) {
    throw new Error("耐障害性テスト失敗: 成功率が90%を下回った");
  }
}

このテストでは、100回のパイプライン実行のうち90%以上が成功することを確認しています。検索APIが半分の確率で落ちる過酷な条件でも、フォールバックとリトライが正しく機能すれば、パイプライン全体としての成功率は90%を超える。

実際にこのテストを導入したことで、「フォールバックの validate 関数が常に false を返すバグ」や「サーキットブレーカーの recoveryTime が短すぎてAPIが復旧する前に HALF_OPEN になる」といった問題を事前に発見できました。

エージェントが本番で止まるのは、エラーが発生したときではありません。エラーからの復旧に失敗したときです。この記事で紹介した3つのパターン――スマートリトライ、フォールバックチェーン、サーキットブレーカー――を組み合わせることで、個々の障害がパイプライン全体の停止に波及しない設計が実現できます。次のステップとして、自分のエージェントパイプラインで最も頻繁に発生しているエラーを洗い出し、そこにフォールバックを1つ追加することから始めてみてほしい。


この記事はプレミアムコンテンツの見本として全文を無料公開しています。Antigravity Lab のプレミアム記事では、このような本番運用に直結する実装パターンを、動作するコード付きで詳しく解説しています。すべてのプレミアム記事を読むには、メンバーシップへの登録をご検討ください。

シェア

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

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

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

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

Agents & Manager2026-05-22
Antigravity Agent のフォールバック階層設計 — モデル劣化・API障害・コスト超過を 4 段で受け止める Resilience アーキテクチャ
AI エージェントの本番運用で起きる『モデル応答劣化・API障害・コスト超過』を 4 段階のフォールバック階層で受け止める設計を、6 アプリ並行運用の実装ログとともに整理しました。
Agents & Manager2026-04-13
Antigravity AIエージェント耐障害性設計 — リトライ・サーキットブレーカー・フォールバックで本番を守る
Antigravityで構築するAIエージェントに耐障害性を組み込む実践ガイド。リトライ戦略、サーキットブレーカー、モデルフォールバック、チェックポイント設計まで本番運用に必要な全パターンを網羅する。
Agents & Manager2026-07-05
エージェント開発スタックの『既知の正常』を1枚のロックファイルで守る — 可動部が同時に動く時代の変更予算設計
IDEビルド・CLI・モデル・依存が同時に動くと、回帰の原因が特定できなくなります。既知の正常を1枚のロックファイルに固定し、一度に動かす軸を絞る変更予算の設計を、実装コードと運用ログで整理しました。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →