LESSON 40分

ストーリー

佐藤CTO
リトライとサーキットブレーカーは実装した。でも、リトライしても全部失敗したらどうなる?
佐藤CTO
APIが完全に落ちている場合……サービスが止まりますね
佐藤CTO
それが問題だ。フォールバック戦略が必要だ。プライマリが落ちたらセカンダリに切り替え、それも駄目ならキャッシュから返す。最終手段は定型文だ
あなた
Graceful Degradation、段階的な品質低下を許容するということですか
佐藤CTO
そうだ。AIシステムは”最高品質 or 全停止”ではいけない。品質を落としてでもサービスを継続する設計が必要だ

LLM特有のエラー分類

エラーカテゴリ

カテゴリエラー例リトライフォールバック
レート制限429 Too Many Requestsあり(バックオフ)別プロバイダー
サーバーエラー500/502/503あり別プロバイダー
タイムアウト30秒超過あり(短縮)軽量モデル
コンテンツフィルター出力がフィルタリングされたなしプロンプト修正
トークン超過コンテキスト長の制限なしコンテキスト削減
認証エラー401 Unauthorizedなし即座にアラート
不正なレスポンスJSONパース失敗ありリトライ + 修正指示

エラー識別と分類

type LLMErrorType =
  | 'rate_limit'
  | 'server_error'
  | 'timeout'
  | 'content_filter'
  | 'token_exceeded'
  | 'auth_error'
  | 'invalid_response'
  | 'unknown';

class LLMErrorClassifier {
  classify(error: Error): {
    type: LLMErrorType;
    retryable: boolean;
    fallbackable: boolean;
    severity: 'low' | 'medium' | 'high' | 'critical';
  } {
    const message = error.message.toLowerCase();

    if (message.includes('rate_limit') || message.includes('429')) {
      return {
        type: 'rate_limit',
        retryable: true,
        fallbackable: true,
        severity: 'medium',
      };
    }

    if (message.includes('500') || message.includes('502') || message.includes('503')) {
      return {
        type: 'server_error',
        retryable: true,
        fallbackable: true,
        severity: 'high',
      };
    }

    if (message.includes('timeout') || message.includes('ETIMEDOUT')) {
      return {
        type: 'timeout',
        retryable: true,
        fallbackable: true,
        severity: 'medium',
      };
    }

    if (message.includes('content_filter') || message.includes('content_policy')) {
      return {
        type: 'content_filter',
        retryable: false,
        fallbackable: false,
        severity: 'low',
      };
    }

    if (message.includes('context_length') || message.includes('token')) {
      return {
        type: 'token_exceeded',
        retryable: false,
        fallbackable: true,
        severity: 'medium',
      };
    }

    if (message.includes('401') || message.includes('auth') || message.includes('api_key')) {
      return {
        type: 'auth_error',
        retryable: false,
        fallbackable: false,
        severity: 'critical',
      };
    }

    return {
      type: 'unknown',
      retryable: true,
      fallbackable: true,
      severity: 'high',
    };
  }
}

モデルフォールバックチェーン

フォールバック戦略

graph TD
    P["プライマリ:
GPT-4o
Azure Japan East"] S["セカンダリ:
Claude 3.5 Sonnet
AWS Bedrock Tokyo"] T["ターシャリ:
GPT-4o-mini
Azure Japan East"] C["キャッシュ:
セマンティックキャッシュから類似回答"] D["定型回答:
一時的にサービスが利用できません"] P -->|"失敗"| S S -->|"失敗"| T T -->|"失敗"| C C -->|"該当なし"| D style P fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e40af style S fill:#d1fae5,stroke:#059669,color:#065f46 style T fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#92400e style C fill:#f3e8ff,stroke:#7c3aed,color:#5b21b6 style D fill:#fee2e2,stroke:#dc2626,color:#991b1b

フォールバックチェーンの実装

interface FallbackChainConfig {
  providers: {
    provider: LLMProvider;
    priority: number;
    maxLatencyMs: number;
  }[];
  cache?: SemanticCache;
  defaultResponse: string;
}

class FallbackChain {
  private readonly sortedProviders: FallbackChainConfig['providers'];

  constructor(
    private readonly config: FallbackChainConfig,
    private readonly errorClassifier: LLMErrorClassifier,
    private readonly logger: Logger,
    private readonly metrics: MetricsCollector,
  ) {
    this.sortedProviders = [...config.providers].sort(
      (a, b) => a.priority - b.priority,
    );
  }

  async complete(
    messages: ChatMessage[],
    options?: CompletionOptions,
  ): Promise<CompletionResult & { provider: string; fallbackLevel: number }> {
    const errors: Array<{ provider: string; error: Error }> = [];

    // 各プロバイダーを順に試行
    for (let i = 0; i < this.sortedProviders.length; i++) {
      const { provider, maxLatencyMs } = this.sortedProviders[i];

      try {
        const result = await provider.complete(messages, {
          ...options,
          timeout: maxLatencyMs,
        });

        // 成功時のメトリクス記録
        this.metrics.record({
          event: 'llm_completion',
          provider: provider.name,
          model: provider.modelId,
          fallbackLevel: i,
          latencyMs: result.latencyMs,
          success: true,
        });

        return { ...result, provider: provider.name, fallbackLevel: i };
      } catch (error) {
        const classified = this.errorClassifier.classify(error as Error);
        errors.push({ provider: provider.name, error: error as Error });

        this.logger.warn(`Provider ${provider.name} failed`, {
          errorType: classified.type,
          fallbackable: classified.fallbackable,
          attemptIndex: i,
        });

        // フォールバック不可のエラーは即座にスロー
        if (!classified.fallbackable) {
          throw error;
        }
      }
    }

    // 全プロバイダー失敗: キャッシュを試行
    if (this.config.cache) {
      const query = messages.find(m => m.role === 'user')?.content ?? '';
      const cached = await this.config.cache.get(query);

      if (cached) {
        this.logger.info('Serving from cache after all providers failed');
        this.metrics.record({
          event: 'llm_completion',
          provider: 'cache',
          fallbackLevel: this.sortedProviders.length,
          success: true,
        });

        return {
          content: cached.answer,
          usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
          model: 'cache',
          finishReason: 'stop',
          latencyMs: 0,
          provider: 'cache',
          fallbackLevel: this.sortedProviders.length,
        };
      }
    }

    // 最終手段: 定型回答
    this.logger.error('All fallback options exhausted', {
      errors: errors.map(e => ({
        provider: e.provider,
        message: e.error.message,
      })),
    });

    this.metrics.record({
      event: 'llm_completion',
      provider: 'default',
      fallbackLevel: this.sortedProviders.length + 1,
      success: false,
    });

    return {
      content: this.config.defaultResponse,
      usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
      model: 'default',
      finishReason: 'stop',
      latencyMs: 0,
      provider: 'default',
      fallbackLevel: this.sortedProviders.length + 1,
    };
  }
}

Graceful Degradation

段階的な品質低下

interface DegradationLevel {
  level: number;
  description: string;
  strategy: string;
}

const DEGRADATION_LEVELS: DegradationLevel[] = [
  {
    level: 0,
    description: '通常運用',
    strategy: 'フルRAGパイプライン + GPT-4o',
  },
  {
    level: 1,
    description: '軽量モード',
    strategy: 'RAG + GPT-4o-mini(精度やや低下)',
  },
  {
    level: 2,
    description: 'キャッシュ優先モード',
    strategy: 'セマンティックキャッシュ優先 + LLMは新規クエリのみ',
  },
  {
    level: 3,
    description: '検索のみモード',
    strategy: 'ベクトル検索結果を直接表示(LLM不使用)',
  },
  {
    level: 4,
    description: '最小限モード',
    strategy: '定型文 + サポート問い合わせリンク',
  },
];

class GracefulDegradationManager {
  private currentLevel = 0;
  private readonly errorWindow: number[] = [];
  private readonly windowSize = 60000; // 1分間

  constructor(
    private readonly thresholds: {
      level1ErrorRate: number; // 例: 0.1 (10%)
      level2ErrorRate: number; // 例: 0.3 (30%)
      level3ErrorRate: number; // 例: 0.5 (50%)
      level4ErrorRate: number; // 例: 0.8 (80%)
    },
    private readonly logger: Logger,
  ) {}

  recordResult(success: boolean): void {
    const now = Date.now();
    this.errorWindow.push(success ? 0 : 1);

    // 古いエントリを削除
    while (
      this.errorWindow.length > 0
      && this.errorWindow[0] < now - this.windowSize
    ) {
      this.errorWindow.shift();
    }

    this.updateLevel();
  }

  getCurrentLevel(): DegradationLevel {
    return DEGRADATION_LEVELS[this.currentLevel];
  }

  private updateLevel(): void {
    if (this.errorWindow.length < 10) return;

    const errorRate = this.errorWindow.reduce((sum, v) => sum + v, 0)
      / this.errorWindow.length;

    let newLevel = 0;
    if (errorRate >= this.thresholds.level4ErrorRate) newLevel = 4;
    else if (errorRate >= this.thresholds.level3ErrorRate) newLevel = 3;
    else if (errorRate >= this.thresholds.level2ErrorRate) newLevel = 2;
    else if (errorRate >= this.thresholds.level1ErrorRate) newLevel = 1;

    if (newLevel !== this.currentLevel) {
      this.logger.warn(
        `Degradation level changed: ${this.currentLevel} → ${newLevel}`,
        { errorRate, level: DEGRADATION_LEVELS[newLevel] },
      );
      this.currentLevel = newLevel;
    }
  }
}

タイムアウト戦略

レイヤー別タイムアウト

レイヤータイムアウト理由
HTTP接続5秒APIへの接続確立
LLM生成(通常)30秒一般的なリクエスト
LLM生成(複雑)60秒長文生成や複雑な推論
ストリーミング初回チャンク10秒TTFB(Time to First Byte)
パイプライン全体90秒検索 + 生成 + 後処理の合計
class TimeoutManager {
  private readonly timeouts: Map<string, number> = new Map([
    ['connection', 5000],
    ['completion_simple', 30000],
    ['completion_complex', 60000],
    ['stream_first_chunk', 10000],
    ['pipeline_total', 90000],
  ]);

  getTimeout(operation: string, complexity: 'simple' | 'complex' = 'simple'): number {
    if (operation === 'completion') {
      return this.timeouts.get(`completion_${complexity}`) ?? 30000;
    }
    return this.timeouts.get(operation) ?? 30000;
  }

  // アダプティブタイムアウト: 過去のレイテンシから動的に調整
  adaptiveTimeout(
    recentLatencies: number[],
    percentile: number = 95,
    multiplier: number = 1.5,
  ): number {
    if (recentLatencies.length === 0) return 30000;

    const sorted = [...recentLatencies].sort((a, b) => a - b);
    const index = Math.ceil(sorted.length * (percentile / 100)) - 1;
    const p95 = sorted[index];

    return Math.min(p95 * multiplier, 90000);
  }
}

まとめ

ポイント内容
エラー分類LLM特有のエラーを分類し、リトライ可否とフォールバック可否を判定
フォールバックチェーンプライマリ → セカンダリ → キャッシュ → 定型文の段階的な切り替え
Graceful Degradationエラー率に応じて運用レベルを動的に調整
タイムアウトレイヤー別の適切な設定とアダプティブな動的調整

チェックリスト

  • LLM特有のエラーカテゴリと対応策を理解した
  • モデルフォールバックチェーンの設計と実装を理解した
  • Graceful Degradationの段階的な品質低下戦略を理解した
  • レイヤー別タイムアウト戦略を理解した

次のステップへ

エラーハンドリングとフォールバック戦略を学びました。次のセクションでは、ストリーミングレスポンスとUXの最適化について学びます。

“最高品質 or 全停止”ではなく、“品質を落としてでもサービスを継続する”。それがプロダクション品質のAIシステムだ。


推定読了時間: 40分