AI API を活用したアプリケーションにおいて、ネットワーク不安定やサーバー側の、一時的なレート制限(429 Too Many Requests)は避けられない課題です。特に、大規模言語モデルを活用したシステムでは、API 呼び出しの失敗がユーザー体験を著しく低下させる要因となります。

本稿では、TypeScript の Decorator(デコレータ)パターンを活用した、再利用可能な自動リトライロジックの実装方法を解説します。私が実際の EC サイトの AI カスタマーサービスシステムで直面した課題と、その解決策を元に、実践的なコード例を紹介します。

なぜ自動リトライが必要なのか

私が担当した EC サイトの AI チャットボットでは、夜間のアクセス集中時に API 呼び出しが頻繫に失敗していました。最初の実装では単純な try-catch で処理していましたが、以下のような問題が発生しました:

HolySheep AI は <50ms の超低レイテンシを提供していますが、それでもネットワーク経路やサーバー負荷により一時的な失敗は発生します。Decorators を活用した自動リトライ機構を実装することで、これらの問題を透過的に解決できました。

環境構築と前提条件

本記事で使用する環境は以下を想定しています:

まず、プロジェクトディレクトリの初期化と必要なパッケージのインストールを行います:

mkdir ai-retry-decorator
cd ai-retry-decorator
npm init -y
npm install typescript ts-node axios
npm install -D @types/node

次に、tsconfig.json で Decorators を有効化します:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "lib": ["ES2022"],
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "outDir": "./dist"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

Decorators による自動リトライ機構の実装

私が実装した自動リトライ Decorator は、以下の要件を満たす必要があります:

Decorators の基礎:メタプログラミングとは

Decorators は TypeScript のメタプログラミング機能の一つで、関数やクラスの動作を宣言的に拡張できます。自動リトライのように横断的関心事(Cross-Cutting Concern)を分離巻くのに最適です。

// src/retry.decorator.ts

import { AsyncRetryOptions, RetryableError, defaultRetryableErrors } from './types';

/**
 * 自動リトライデコレータの工厂関数
 * 私はこのパターンを「設定オブジェクト指向 Decorator」と呼んでいます
 */
export function AsyncRetry(options: Partial) {
  return function  Promise>(
    target: T,
    context: ClassMethodDecoratorContext
  ): T {
    const {
      maxAttempts = 3,
      initialDelay = 1000,
      maxDelay = 10000,
      backoffMultiplier = 2,
      retryableErrors = defaultRetryableErrors,
      onRetry = null,
    } = options;

    return function (this: any, ...args: any[]): Promise {
      let currentAttempt = 0;
      let currentDelay = initialDelay;

      const executeWithRetry = async (): Promise => {
        currentAttempt++;

        try {
          return await target.apply(this, args);
        } catch (error) {
          // 最大試行回数に達した場合
          if (currentAttempt >= maxAttempts) {
            console.error(
              [Retry] 最大リトライ回数(${maxAttempts})に達しました。
            );
            throw error;
          }

          // リトライ対象エラーかどうか判定
          const isRetryable = shouldRetry(error, retryableErrors);

          if (!isRetryable) {
            console.error(
              [Retry] リトライ対象外のエラー: ${error instanceof Error ? error.message : String(error)}
            );
            throw error;
          }

          // 指数バックオフで待機
          const delay = Math.min(currentDelay, maxDelay);
          console.warn(
            [Retry] ${currentAttempt}回目失敗。${delay}ms後にリトライします...
          );

          if (onRetry) {
            await onRetry(error, currentAttempt, delay);
          }

          await sleep(delay);
          currentDelay *= backoffMultiplier;

          // 再帰的にリトライ
          return executeWithRetry();
        }
      };

      return executeWithRetry();
    } as T;
  };
}

/**
 * エラーがリトライ対象かどうか判定
 */
function shouldRetry(
  error: any,
  retryableErrors: RetryableError[]
): boolean {
  if (!error) return false;

  const errorMessage = error instanceof Error ? error.message : String(error);
  const statusCode = error.response?.status;

  return retryableErrors.some((retryable) => {
    // ステータスコードで判定
    if (retryable.statusCodes && statusCode) {
      return retryable.statusCodes.includes(statusCode);
    }
    // エラーメッセージで判定
    if (retryable.messagePattern && errorMessage) {
      return errorMessage.includes(retryable.messagePattern);
    }
    // エラータイプで判定
    if (retryable.errorType) {
      return error instanceof retryable.errorType;
    }
    return false;
  });
}

/**
 * 待機用ユーティリティ
 */
function sleep(ms: number): Promise {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

型定義ファイル

// src/types.ts

export interface RetryableError {
  /** ステータスコードで判定 */
  statusCodes?: number[];
  /** エラーメッセージのパターン(部分一致) */
  messagePattern?: string;
  /** エラータイプのクラス */
  errorType?: new (...args: any[]) => Error;
}

export interface AsyncRetryOptions {
  /** 最大試行回数 */
  maxAttempts: number;
  /** 初期待機時間(ミリ秒) */
  initialDelay: number;
  /** 最大待機時間(ミリ秒) */
  maxDelay: number;
  /** バックオフの乗数 */
  backoffMultiplier: number;
  /** リトライ対象のエラー定義 */
  retryableErrors: RetryableError[];
  /** リトライ時のコールバック */
  onRetry?: (error: any, attempt: number, delay: number) => Promise | void;
}

/**
 * デフォルトでリトライ対象とするエラー
 * 私は HolySheep API の特性を考慮してこれらを定義しました
 */
export const defaultRetryableErrors: RetryableError[] = [
  // レート制限(429 Too Many Requests)
  { statusCodes: [429] },
  // サーバーエラー(5xx系)
  { statusCodes: [500, 502, 503, 504] },
  // ゲートウェイタイムアウト
  { statusCodes: [504] },
  // ネットワーク関連エラー
  { messagePattern: 'ECONNREFUSED' },
  { messagePattern: 'ETIMEDOUT' },
  { messagePattern: 'ENOTFOUND' },
  { messagePattern: 'socket hang up' },
  { messagePattern: 'timeout' },
  { messagePattern: 'network error' },
];

export interface HolySheepAPIError extends Error {
  response?: {
    status: number;
    data?: {
      error?: {
        message?: string;
        type?: string;
        code?: string;
      };
    };
  };
  isRetryable?: boolean;
}

HolySheep AI API クライアントへの適用

ここからは、HolySheep AI の API を呼び出す具体的な例を示します。HolySheep AI は ¥1=$1 という業界最安水準の料金体系を提供しており、2026 年の価格表は以下の通りです:

// src/holysheep-client.ts

import axios, { AxiosInstance, AxiosError } from 'axios';
import { AsyncRetry } from './retry.decorator';

/**
 * HolySheep AI API クライアント
 * 私はこのクライアントを複数のプロジェクトで再利用しています
 */
export class HolySheepAIClient {
  private client: AxiosInstance;

  constructor(apiKey: string = process.env.HOLYSHEEP_API_KEY || 'YOUR_HOLYSHEEP_API_KEY') {
    this.client = axios.create({
      baseURL: 'https://api.holysheep.ai/v1',
      headers: {
        'Authorization': Bearer ${apiKey},
        'Content-Type': 'application/json',
      },
      timeout: 30000, // 30秒タイムアウト
    });

    // レスポンスエラーのフォーマット
    this.client.interceptors.response.use(
      (response) => response,
      (error: AxiosError) => {
        const message = error.response?.data
          ? JSON.stringify(error.response.data)
          : error.message;
        console.error([HolySheep API Error] ${error.response?.status}: ${message});
        return Promise.reject(error);
      }
    );
  }

  /**
   * チャットCompletions API(再試行Decorator付き)
   */
  @AsyncRetry({
    maxAttempts: 5,
    initialDelay: 1000,
    maxDelay: 16000,
    backoffMultiplier: 2,
    onRetry: async (error, attempt, delay) => {
      console.log([リトライログ] Attempt ${attempt}, 待機時間: ${delay}ms);
    },
  })
  async createChatCompletion(params: {
    model: string;
    messages: Array<{ role: string; content: string }>;
    temperature?: number;
    max_tokens?: number;
  }): Promise {
    const response = await this.client.post('/chat/completions', params);
    return response.data;
  }

  /**
   * Embeddings API(再試行Decorator付き)
   */
  @AsyncRetry({
    maxAttempts: 3,
    initialDelay: 500,
    maxDelay: 8000,
  })
  async createEmbedding(params: {
    model: string;
    input: string | string[];
  }): Promise {
    const response = await this.client.post('/embeddings', params);
    return response.data;
  }

  /**
   * ストリーミング対応chat completion(手動リトライ)
   * ストリーミングでは Decorator が使いにくいので、手動実装
   */
  async createStreamingChatCompletion(
    params: {
      model: string;
      messages: Array<{ role: string; content: string }>;
    },
    onChunk: (chunk: string) => void,
    maxRetries: number = 3
  ): Promise {
    let retries = 0;

    while (retries < maxRetries) {
      try {
        const response = await this.client.post(
          '/chat/completions',
          { ...params, stream: true },
          { responseType: 'stream' }
        );

        const stream = response.data;
        stream.on('data', (chunk: Buffer) => {
          onChunk(chunk.toString());
        });

        await new Promise((resolve, reject) => {
          stream.on('end', resolve);
          stream.on('error', reject);
        });
        return;
      } catch (error) {
        retries++;
        if (retries >= maxRetries) throw error;

        const delay = Math.min(1000 * Math.pow(2, retries - 1), 8000);
        console.warn([Streaming] ${retries}回目失敗、${delay}ms後に再接続...);
        await new Promise((r) => setTimeout(r, delay));
      }
    }
  }
}

// 使用例
const client = new HolySheepAIClient();

async function main() {
  try {
    // DeepSeek V3.2 での質問(最安コスト)
    const response = await client.createChatCompletion({
      model: 'deepseek-chat',
      messages: [
        { role: 'system', content: 'あなたは有用なアシスタントです。' },
        { role: 'user', content: 'TypeScriptのDecoratorについて教えてください。' },
      ],
      temperature: 0.7,
      max_tokens: 500,
    });

    console.log('応答:', response.choices[0]?.message?.content);
  } catch (error) {
    console.error('API呼び出し最終エラー:', error);
  }
}

main();

実戦例:EC サイトの AI カスタマーサービス

私が実際に開発した EC サイトの AI チャットボットでは、以下の構成で自動リトライ機構を採用しました:

// src/services/chat-service.ts

import { HolySheepAIClient } from '../holysheep-client';
import { AsyncRetry } from '../retry.decorator';

/**
 * EC サイトの AI カスタマーサービス
 * 商品の在庫確認、配送状況、AI応答生成を統合
 */
export class EcommerceChatService {
  private aiClient: HolySheepAIClient;
  private context: Map = new Map();

  constructor() {
    this.aiClient = new HolySheepAIClient(process.env.HOLYSHEEP_API_KEY);
  }

  /**
   * AI 応答生成(高優先度:高レート制限遭遇率)
   * 私は RAG システムと連携して精度を高めています
   */
  @AsyncRetry({
    maxAttempts: 5,
    initialDelay: 1000,
    backoffMultiplier: 2.0,
    onRetry: async (error, attempt) => {
      // Slack通知を挿入可能
      console.log([Ecommerce AI] リトライ回 数: ${attempt});
    },
  })
  async generateAIResponse(
    sessionId: string,
    userMessage: string,
    useRAG: boolean = true
  ): Promise {
    const context = this.context.get(sessionId) || { history: [] };

    // RAG 模式下では Embeddings API も呼び出し
    let systemPrompt = 'あなたはECサイトの丁寧なカスタマーサポート担当者です。';
    
    if (useRAG) {
      // 商品データベースから関連情報を取得
      const productInfo = await this.searchProductKnowledge(userMessage);
      if (productInfo) {
        systemPrompt += \n\n関連商品情報:\n${productInfo};
      }
    }

    const response = await this.aiClient.createChatCompletion({
      model: 'deepseek-chat', // DeepSeek V3.2 でコスト効率最大化
      messages: [
        { role: 'system', content: systemPrompt },
        ...context.history.slice(-6), // 直近3回の会話をコンテキストに
        { role: 'user', content: userMessage },
      ],
      temperature: 0.8,
      max_tokens: 800,
    });

    const aiMessage = response.choices[0]?.message?.content || '';

    // 会話履歴を更新
    context.history.push(
      { role: 'user', content: userMessage },
      { role: 'assistant', content: aiMessage }
    );
    if (context.history.length > 10) {
      context.history = context.history.slice(-10);
    }
    this.context.set(sessionId, context);

    return aiMessage;
  }

  /**
   * 商品知識のベクトル検索(RAG用)
   */
  @AsyncRetry({ maxAttempts: 3, initialDelay: 500 })
  private async searchProductKnowledge(query: string): Promise {
    try {
      const embedding = await this.aiClient.createEmbedding({
        model: 'text-embedding-3-small',
        input: query,
      });

      // ベクトル検索の実装(省略)
      // const results = await vectorStore.similaritySearch(embedding.data[0].embedding);
      // return results.map(r => r.pageContent).join('\n');

      return null; // 簡略化のため null を返す
    } catch (error) {
      console.error('[RAG] Embedding 生成失敗:', error);
      return null; // RAG 失敗は致命적이지 않게 handle
    }
  }
}

// 使用テスト
async function testEcommerceChat() {
  const service = new EcommerceChatService();

  const responses = await Promise.allSettled([
    service.generateAIResponse('session-1', '商品の在庫状況は?'),
    service.generateAIResponse('session-2', '配送多久ですか?'),
    service.generateAIResponse('session-3', 'キャンセル方法を教えてください'),
  ]);

  responses.forEach((result, index) => {
    if (result.status === 'fulfilled') {
      console.log(セッション${index + 1} 応答:, result.value);
    } else {
      console.error(セッション${index + 1} 失敗:, result.reason);
    }
  });
}

testEcommerceChat();

指数バックオフの動作を確認する

指数バックオフが正しく動作していることを確認するためのテストスクリプトを作成しました:

// src/test-retry-logic.ts

import { HolySheepAIClient } from './holysheep-client';

/**
 * 手動リトライロジックのテスト
 * Decorator を使わずに直接実装し、バックオフの動きを確認
 */
async function testExponentialBackoff() {
  const HOLYSHEEP_API_KEY = process.env.HOLYSHEEP_API_KEY || 'YOUR_HOLYSHEEP_API_KEY';
  const client = new HolySheepAIClient(HOLYSHEEP_API_KEY);

  const maxAttempts = 4;
  const initialDelay = 500;
  const backoffMultiplier = 2;
  let currentDelay = initialDelay;

  console.log('=== 指数バックオフ テスト ===');
  console.log(最大試行回数: ${maxAttempts});
  console.log(初期待機時間: ${initialDelay}ms);
  console.log(バックオフ乗数: ${backoffMultiplier});
  console.log('---');

  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    const delay = Math.min(currentDelay, 8000); // 最大8秒
    console.log(Attempt ${attempt}: ${delay}ms 待機);

    // 実際の API 呼び出し
    const startTime = Date.now();
    try {
      const response = await client.createChatCompletion({
        model: 'deepseek-chat',
        messages: [{ role: 'user', content: 'Hello' }],
        max_tokens: 10,
      });

      const latency = Date.now() - startTime;
      console.log(✓ 成功! レイテンシ: ${latency}ms);
      break;
    } catch (error: any) {
      const latency = Date.now() - startTime;
      const status = error.response?.status || 'Network Error';
      console.log(✗ 失敗 (${status}) レイテンシ: ${latency}ms);

      if (attempt < maxAttempts) {
        console.log(  → 次回待機: ${delay}ms × ${backoffMultiplier} = ${delay * backoffMultiplier}ms);
        await new Promise((r) => setTimeout(r, delay));
        currentDelay *= backoffMultiplier;
      } else {
        console.log(! 最大試行回数に達しました);
      }
    }
  }
}

/**
 * 同時リクエストのテスト(レート制限の確認)
 */
async function testConcurrentRequests() {
  const HOLYSHEEP_API_KEY = process.env.HOLYSHEEP_API_KEY || 'YOUR_HOLYSHEEP_API_KEY';
  const client = new HolySheepAIClient(HOLYSHEEP_API_KEY);

  console.log('\n=== 同時リクエスト テスト ===');

  const promises = Array.from({ length: 10 }, (_, i) => {
    return client.createChatCompletion({
      model: 'gpt-4o-mini', // 安価なモデルでテスト
      messages: [{ role: 'user', content: テスト ${i + 1} }],
      max_tokens: 5,
    }).then((r) => ({ success: true, index: i }))
      .catch((e) => ({ success: false, index: i, error: e.response?.status }));
  });

  const results = await Promise.allSettled(promises);

  const successCount = results.filter((r) => r.status === 'fulfilled').length;
  const failCount = results.filter((r) => r.status === 'rejected').length;

  console.log(成功: ${successCount}, 失敗: ${failCount});
}

// 環境変数確認
console.log('HolySheep API Key設定:', 
  process.env.HOLYSHEEP_API_KEY ? '済み' : '未設定(デフォルト値使用)'
);

testExponentialBackoff()
  .then(() => testConcurrentRequests())
  .catch(console.error);

よくあるエラーと対処法

1. Decorator が「Experimental」警告で動作しない

エラー内容:

Experimental support for decorators is a feature that is subject to change and might be broken
at any time. They are called "experimental" because the behavior might change or be removed in
future TypeScript releases.

解決方法: tsconfig.json で experimentalDecorators を明示的に有効化し、emitDecoratorMetadata は用途に応じて設定してください:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,  // Dependency Injection 使用時のみ必要
    "strict": true
  }
}

2. リトライ無限ループによるアプリケーションフリーズ

エラー内容: API が永久に 200 OK を返さないため、リトライが止まらない。

解決方法: maxAttempts の上限を必ず設定し、maxDelay で最大待機時間を制限します。また、onRetry コールバックでログを出力して監視します:

@AsyncRetry({
  maxAttempts: 3,        // 必ず有限値を設定
  maxDelay: 10000,       // 最大10秒で打ち切り
  initialDelay: 1000,
  onRetry: (error, attempt, delay) => {
    console.warn([監視] リトライ ${attempt}回目、${delay}ms待機);
    // 集中監視システムへの通知も可能
    // metrics.increment('api_retry_total');
  },
})

3. Axios エラーの型情報が正しく取得できない

エラー内容:

TypeError: Cannot read properties of undefined (reading 'status')
   at shouldRetry (retry.decorator.ts:45:24)

解決方法: Optional Chaining と Nullish Coalescing を使用して、安全にプロパティアクセスします:

// 修正前(危険なアクセス)
const statusCode = error.response.status;

// 修正後(安全なアクセス)
const statusCode = error.response?.status;
const errorMessage = error instanceof Error ? error.message : String(error);

// defaultRetryableErrors の判定も強化
{ statusCodes: [429] },  // undefined 場合は無視される

4. ストリーミング API での Decorator 動作不良

エラー内容: stream: true を設定した際、Decorator が Response オブジェクトを正常に処理できない。

解決方法: ストリーミングは Decorator でラップせず、手動でリトライロジックを実装します。Decorator は Promise を返す必要があるため、Stream とは相性が悪いです:

// Decorator ではなく通常メソッドとして実装
async createStreamingChatCompletion(params, onChunk, maxRetries = 3) {
  let retries = 0;
  
  while (retries < maxRetries) {
    try {
      const response = await this.client.post('/chat/completions', {
        ...params,
        stream: true,
      }, { responseType: 'stream' });
      
      // ストリーム処理...
      return;
    } catch (error) {
      retries++;
      if (retries >= maxRetries) throw error;
      
      const delay = Math.min(1000 * Math.pow(2, retries - 1), 8000);
      await new Promise(r => setTimeout(r, delay));
    }
  }
}

パフォーマンス最適化のポイント

私自身の経験則として、自动リトライ機構導入時には以下の点に注意しています:

// Jitter 付きバックオフの例
const jitter = delay * (0.8 + Math.random() * 0.4); // ±20% 揺らぎ
await sleep(jitter);

まとめ

TypeScript の Decorator パターンを活用した自動リトライ機構は、以下のメリットをもたらします:

HolySheep AI のような高パフォーマンス API でも、ネットワーク経路や一時的な高負荷は発生します。Decorators を活用した堅牢なリトライ機構を実装することで、ユーザー体験を向上させ、システムの信頼性を高めることができます。

特に、DeepSeek V3.2 の $0.42/MTok という破格の料金体系中では、API 呼び出しの最適化によるコスト削減も重要です。自動リトライ機構を組み合わせることで、コスト効率と信頼性の両方を達成できます。

👉 HolySheep AI に登録して無料クレジットを獲得