T-CREATOR

Dify と OpenAI・Anthropic モデルの切り替え活用術

Dify と OpenAI・Anthropic モデルの切り替え活用術

AI アプリケーション開発の現場で、複数の LLM モデルを効果的に使い分けることは、もはや必須のスキルとなっています。OpenAI の GPT シリーズと Anthropic の Claude シリーズは、それぞれ異なる強みを持っており、適切に切り替えることで、アプリケーションのパフォーマンスとコスト効率を大幅に向上させることができます。

本記事では、Dify プラットフォームを活用して、これらのモデルを戦略的に切り替える実践的な手法をご紹介します。エラーハンドリングからパフォーマンス最適化まで、実際の開発現場で直面する課題と、その解決策を詳しく解説していきます。

背景

近年の AI 技術の急速な発展により、企業や開発者は複数の LLM プロバイダーから最適なモデルを選択できる時代になりました。しかし、この選択肢の豊富さは同時に新たな課題も生み出しています。

AI モデルの多様化

現在主流となっている AI モデルには、以下のような特徴があります。

プロバイダーモデル主な特徴適用場面
OpenAIGPT-4.1最高精度、大容量コンテキスト複雑なタスク、長文分析
OpenAIGPT-4.1-miniバランス型、コスト効率一般的なタスク、チャットボット
OpenAIGPT-4.1-nano超高速、最安コスト軽量タスク、大量処理
OpenAIo3推論特化、ツール統合エージェント、マルチステップタスク
AnthropicClaude-Opus-4最高知能、長文理解複雑な分析、研究支援
AnthropicClaude-Sonnet-4バランス型、コスパ良好文書分析、コンテンツ生成
AnthropicClaude-Haiku-3.5超高速処理、軽量タスクリアルタイム処理、大量処理

Dify プラットフォームの優位性

Dify は、これらの多様なモデルを統一的に管理し、効率的に切り替えるための理想的なプラットフォームです。GUI ベースの直感的な操作性と、API レベルでの柔軟な制御を両立しており、開発者は複雑な統合作業を行うことなく、マルチモデル環境を構築できます。

特に、Dify の以下の機能が重要な役割を果たします:

  • 統一 API エンドポイント: 複数のプロバイダーを単一のインターフェースで管理
  • ワークフロー機能: 条件分岐によるモデル選択の自動化
  • リアルタイム監視: パフォーマンスとコストの可視化
  • フォールバック機能: サービス中断時の自動切り替え

課題

マルチモデル環境を構築する際に、開発者が直面する主要な課題を整理してみましょう。これらの課題を理解することで、効果的な切り替え戦略を立てることができます。

モデル選択の複雑性

どのタスクにどのモデルが最適かを判断することは、想像以上に複雑です。単純にモデルの性能だけでなく、以下の要素を総合的に考慮する必要があります。

typescript// モデル選択時に考慮すべき要素の例
interface ModelSelectionCriteria {
  taskType: 'chat' | 'analysis' | 'generation' | 'coding';
  responseTime: 'realtime' | 'fast' | 'standard';
  accuracy: 'high' | 'medium' | 'standard';
  costBudget: number;
  inputLength: number;
  expectedOutputLength: number;
  languageRequirement: string[];
}

パフォーマンスとコストの最適化

各モデルは異なる料金体系とレスポンス時間を持っているため、トレードオフの関係を理解し、適切にバランスを取る必要があります。

例えば、GPT-4.1 は最高精度を誇りますが処理コストが高く、一方で Claude-Haiku-3.5 は高速で安価ですが、複雑なタスクでは精度が劣る場合があります。

エラーハンドリングの複雑化

複数のプロバイダーを使用することで、以下のような多様なエラーに対処する必要があります:

  • RATE_LIMIT_EXCEEDED: API レート制限に達した場合
  • MODEL_OVERLOADED: モデルが過負荷状態の場合
  • INVALID_API_KEY: 認証エラー
  • CONTEXT_LENGTH_EXCEEDED: 入力テキストが長すぎる場合
  • NETWORK_TIMEOUT: ネットワークタイムアウト

一貫性の担保

異なるモデル間での出力フォーマットや品質の違いを吸収し、ユーザーエクスペリエンスの一貫性を保つことが重要です。モデルによって回答のスタイルや詳細度が異なるため、適切な後処理が必要になります。

監視と運用の課題

マルチモデル環境では、以下の要素を継続的に監視する必要があります:

  • 各モデルのレスポンス時間
  • API 使用量とコスト
  • エラー率とパフォーマンス
  • ユーザー満足度

これらの課題を解決するために、次のセクションでは具体的な解決策をご紹介していきます。

解決策

モデル選択の判断基準と特性理解

効果的なモデル切り替えを実現するには、まず各モデルの特性を正確に理解することが重要です。以下のフレームワークを使って、タスクとモデルのマッチングを体系化しましょう。

モデル特性マトリックス

各モデルの特性を数値化して比較することで、客観的な選択基準を設定できます。

typescript// モデル特性定義
interface ModelCapabilities {
  providerId: string;
  modelName: string;
  speed: number; // 1-10 (10が最高速)
  accuracy: number; // 1-10 (10が最高精度)
  costEfficiency: number; // 1-10 (10が最高効率)
  contextLength: number; // 最大トークン数
  multilingualSupport: number; // 1-10
  codeGeneration: number; // 1-10
  creativeWriting: number; // 1-10
  logicalReasoning: number; // 1-10
}

const modelCapabilities: ModelCapabilities[] = [
  {
    providerId: 'openai',
    modelName: 'gpt-4.1',
    speed: 7,
    accuracy: 10,
    costEfficiency: 5,
    contextLength: 1000000,
    multilingualSupport: 9,
    codeGeneration: 10,
    creativeWriting: 9,
    logicalReasoning: 10,
  },
  {
    providerId: 'openai',
    modelName: 'gpt-4.1-mini',
    speed: 8,
    accuracy: 8,
    costEfficiency: 8,
    contextLength: 1000000,
    multilingualSupport: 8,
    codeGeneration: 8,
    creativeWriting: 8,
    logicalReasoning: 8,
  },
  {
    providerId: 'openai',
    modelName: 'gpt-4.1-nano',
    speed: 10,
    accuracy: 7,
    costEfficiency: 10,
    contextLength: 1000000,
    multilingualSupport: 7,
    codeGeneration: 7,
    creativeWriting: 7,
    logicalReasoning: 7,
  },
  {
    providerId: 'openai',
    modelName: 'o3',
    speed: 6,
    accuracy: 10,
    costEfficiency: 7,
    contextLength: 128000,
    multilingualSupport: 8,
    codeGeneration: 9,
    creativeWriting: 8,
    logicalReasoning: 10,
  },
  {
    providerId: 'anthropic',
    modelName: 'claude-opus-4',
    speed: 6,
    accuracy: 10,
    costEfficiency: 3,
    contextLength: 200000,
    multilingualSupport: 9,
    codeGeneration: 9,
    creativeWriting: 10,
    logicalReasoning: 10,
  },
  {
    providerId: 'anthropic',
    modelName: 'claude-sonnet-4',
    speed: 8,
    accuracy: 9,
    costEfficiency: 7,
    contextLength: 200000,
    multilingualSupport: 9,
    codeGeneration: 9,
    creativeWriting: 9,
    logicalReasoning: 9,
  },
  {
    providerId: 'anthropic',
    modelName: 'claude-haiku-3.5',
    speed: 10,
    accuracy: 7,
    costEfficiency: 9,
    contextLength: 200000,
    multilingualSupport: 8,
    codeGeneration: 7,
    creativeWriting: 8,
    logicalReasoning: 7,
  },
];

動的選択アルゴリズム

タスクの要件に基づいて、最適なモデルを自動選択するアルゴリズムを実装します。

typescript// タスク要件の定義
interface TaskRequirements {
  taskType:
    | 'chat'
    | 'analysis'
    | 'coding'
    | 'creative'
    | 'translation';
  prioritySpeed: number; // 1-10
  priorityAccuracy: number; // 1-10
  priorityCost: number; // 1-10
  inputTokens: number;
  expectedComplexity: 'low' | 'medium' | 'high';
}

// モデル選択ロジック
function selectOptimalModel(
  requirements: TaskRequirements,
  availableModels: ModelCapabilities[]
): ModelCapabilities {
  const scores = availableModels.map((model) => {
    let score = 0;

    // 基本スコア計算
    score += model.speed * requirements.prioritySpeed * 0.3;
    score +=
      model.accuracy * requirements.priorityAccuracy * 0.4;
    score +=
      model.costEfficiency *
      requirements.priorityCost *
      0.3;

    // タスク特化スコア
    switch (requirements.taskType) {
      case 'coding':
        score += model.codeGeneration * 2;
        break;
      case 'creative':
        score += model.creativeWriting * 2;
        break;
      case 'analysis':
        score += model.logicalReasoning * 2;
        break;
      case 'translation':
        score += model.multilingualSupport * 2;
        break;
    }

    // コンテキスト長制約
    if (requirements.inputTokens > model.contextLength) {
      score *= 0.1; // 大幅減点
    }

    return { model, score };
  });

  // 最高スコアのモデルを選択
  const bestMatch = scores.reduce((prev, current) =>
    current.score > prev.score ? current : prev
  );

  return bestMatch.model;
}

動的切り替えシステムの実装

Dify のワークフロー機能を活用して、リアルタイムでモデルを切り替えるシステムを構築します。

Dify ワークフローでの条件分岐設定

Dify のワークフロー機能を使用して、条件に基づく自動切り替えを実装できます。

json{
  "workflow": {
    "name": "smart_model_switcher",
    "nodes": [
      {
        "id": "input_analyzer",
        "type": "code",
        "code": `
// 入力分析ノード
function analyzeInput(input) {
  const tokenCount = input.length / 4; // 簡易トークン計算
  const complexity = detectComplexity(input);
  const taskType = classifyTask(input);

  return {
    tokenCount,
    complexity,
    taskType,
    timestamp: Date.now()
  };
}

function detectComplexity(input) {
  const complexPatterns = [
    /コード.*生成|プログラム.*作成/,
    /詳細.*分析|複雑.*解析/,
    /数学.*計算|統計.*処理/
  ];

  return complexPatterns.some(pattern =>
    pattern.test(input)
  ) ? 'high' : 'medium';
}

function classifyTask(input) {
  if (/コード|プログラム|javascript|typescript|python/.test(input)) {
    return 'coding';
  } else if (/翻訳|translate|英語|中国語|korean/.test(input)) {
    return 'translation';
  } else if (/創作|小説|詩|ストーリー/.test(input)) {
    return 'creative';
  } else if (/分析|解析|調査|research/.test(input)) {
    return 'analysis';
  }
  return 'chat';
}
        `
      },
      {
        "id": "model_selector",
        "type": "code",
        "code": `
// モデル選択ロジック
function selectModel(analysis, systemLoad) {
  const { tokenCount, complexity, taskType } = analysis;

  // システム負荷を考慮
  if (systemLoad.openai > 0.8) {
    return 'anthropic/claude-haiku-3.5';
  }

  // トークン数制限チェック
  if (tokenCount > 100000) {
    return 'anthropic/claude-sonnet-4';
  }

  // タスク別最適化
  switch (taskType) {
    case 'coding':
      return complexity === 'high' ?
        'openai/gpt-4.1' : 'openai/gpt-4.1-mini';
    case 'creative':
      return 'anthropic/claude-sonnet-4';
    case 'translation':
      return 'openai/gpt-4.1';
    case 'analysis':
      return tokenCount > 50000 ?
        'anthropic/claude-sonnet-4' : 'openai/gpt-4.1';
    default:
      return 'openai/gpt-4.1-mini';
  }
}
        `
      },
      {
        "id": "llm_switch",
        "type": "condition",
        "conditions": [
          {
            "variable": "selected_model",
            "comparison": "equals",
            "value": "openai/gpt-4.1",
            "next_node": "openai_gpt41"
          },
          {
            "variable": "selected_model",
            "comparison": "equals",
            "value": "openai/gpt-4.1-mini",
            "next_node": "openai_gpt41mini"
          },
          {
            "variable": "selected_model",
            "comparison": "equals",
            "value": "openai/gpt-4.1-nano",
            "next_node": "openai_gpt41nano"
          },
          {
            "variable": "selected_model",
            "comparison": "equals",
            "value": "openai/o3",
            "next_node": "openai_o3"
          },
          {
            "variable": "selected_model",
            "comparison": "equals",
            "value": "anthropic/claude-opus-4",
            "next_node": "anthropic_opus"
          },
          {
            "variable": "selected_model",
            "comparison": "equals",
            "value": "anthropic/claude-sonnet-4",
            "next_node": "anthropic_sonnet"
          },
          {
            "variable": "selected_model",
            "comparison": "equals",
            "value": "anthropic/claude-haiku-3.5",
            "next_node": "anthropic_haiku"
          }
        ]
      }
    ]
  }
}

API レベルでの切り替え実装

Node.js と Express を使用した API サーバーでの実装例です。

typescript// server.ts
import express from 'express';
import { OpenAI } from 'openai';
import Anthropic from '@anthropic-ai/sdk';

interface ModelConfig {
  provider: 'openai' | 'anthropic';
  model: string;
  maxTokens: number;
  temperature: number;
}

class ModelSwitcher {
  private openai: OpenAI;
  private anthropic: Anthropic;
  private currentLoad: Map<string, number> = new Map();

  constructor() {
    this.openai = new OpenAI({
      apiKey: process.env.OPENAI_API_KEY,
    });

    this.anthropic = new Anthropic({
      apiKey: process.env.ANTHROPIC_API_KEY,
    });

    // 負荷監視の開始
    this.startLoadMonitoring();
  }

  async generateResponse(
    prompt: string,
    config?: Partial<ModelConfig>
  ) {
    try {
      const selectedModel = await this.selectModel(
        prompt,
        config
      );
      console.log(
        `Selected model: ${selectedModel.provider}/${selectedModel.model}`
      );

      if (selectedModel.provider === 'openai') {
        return await this.callOpenAI(prompt, selectedModel);
      } else {
        return await this.callAnthropic(
          prompt,
          selectedModel
        );
      }
    } catch (error) {
      // エラーハンドリングとフォールバック
      return await this.handleErrorAndFallback(
        prompt,
        error,
        config
      );
    }
  }

  private async callOpenAI(
    prompt: string,
    config: ModelConfig
  ) {
    const response =
      await this.openai.chat.completions.create({
        model: config.model,
        messages: [{ role: 'user', content: prompt }],
        max_tokens: config.maxTokens,
        temperature: config.temperature,
      });

    return {
      content: response.choices[0].message.content,
      provider: 'openai',
      model: config.model,
      usage: response.usage,
    };
  }

  private async callAnthropic(
    prompt: string,
    config: ModelConfig
  ) {
    const response = await this.anthropic.messages.create({
      model: config.model,
      max_tokens: config.maxTokens,
      temperature: config.temperature,
      messages: [{ role: 'user', content: prompt }],
    });

    return {
      content: response.content[0].text,
      provider: 'anthropic',
      model: config.model,
      usage: response.usage,
    };
  }

  private async handleErrorAndFallback(
    prompt: string,
    error: any,
    config?: Partial<ModelConfig>
  ) {
    console.error('Model error:', error);

    // エラータイプ別のフォールバック戦略
    if (error.code === 'rate_limit_exceeded') {
      // レート制限の場合は別プロバイダーに切り替え
      const fallbackConfig = this.getFallbackModel(config);
      return await this.generateResponse(
        prompt,
        fallbackConfig
      );
    } else if (error.code === 'model_overloaded') {
      // モデル過負荷の場合は軽量モデルに切り替え
      const lightweightConfig = this.getLightweightModel();
      return await this.generateResponse(
        prompt,
        lightweightConfig
      );
    } else if (error.code === 'context_length_exceeded') {
      // コンテキスト長超過の場合は長文対応モデルに切り替え
      const longContextConfig = this.getLongContextModel();
      return await this.generateResponse(
        prompt,
        longContextConfig
      );
    }

    throw error;
  }

  private startLoadMonitoring() {
    setInterval(async () => {
      // API レスポンス時間による負荷監視
      try {
        const openaiStart = Date.now();
        await fetch('https://api.openai.com/v1/models', {
          headers: {
            Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
          },
        });
        const openaiLatency = Date.now() - openaiStart;

        const anthropicStart = Date.now();
        await fetch(
          'https://api.anthropic.com/v1/messages',
          {
            method: 'POST',
            headers: {
              'x-api-key': process.env.ANTHROPIC_API_KEY,
              'Content-Type': 'application/json',
            },
            body: JSON.stringify({
              model: 'claude-haiku-3.5',
              max_tokens: 1,
              messages: [{ role: 'user', content: 'test' }],
            }),
          }
        );
        const anthropicLatency =
          Date.now() - anthropicStart;

        this.currentLoad.set(
          'openai',
          Math.min(openaiLatency / 1000, 1)
        );
        this.currentLoad.set(
          'anthropic',
          Math.min(anthropicLatency / 1000, 1)
        );
      } catch (error) {
        console.error('Load monitoring error:', error);
      }
    }, 30000); // 30秒間隔
  }
}

const app = express();
const modelSwitcher = new ModelSwitcher();

app.use(express.json());

app.post('/api/chat', async (req, res) => {
  try {
    const { message, preferences } = req.body;
    const response = await modelSwitcher.generateResponse(
      message,
      preferences
    );
    res.json(response);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.listen(3000, () => {
  console.log(
    'Model switching server running on port 3000'
  );
});

パフォーマンス最適化とレスポンス時間管理

レスポンス時間を最適化するために、複数の戦略を組み合わせて実装します。

キャッシュシステムの実装

同様のクエリに対する応答を高速化するため、Redis を使用したキャッシュシステムを構築しましょう。

typescript// cache-service.ts
import Redis from 'ioredis';
import crypto from 'crypto';

class ResponseCache {
  private redis: Redis;
  private defaultTTL: number = 3600; // 1時間

  constructor() {
    this.redis = new Redis({
      host: process.env.REDIS_HOST || 'localhost',
      port: parseInt(process.env.REDIS_PORT || '6379'),
      retryDelayOnFailover: 100,
      maxRetriesPerRequest: 3,
    });
  }

  // クエリのハッシュ生成
  private generateCacheKey(
    prompt: string,
    modelConfig: any
  ): string {
    const payload = JSON.stringify({ prompt, modelConfig });
    return `llm_cache:${crypto
      .createHash('sha256')
      .update(payload)
      .digest('hex')}`;
  }

  // キャッシュから応答を取得
  async getCachedResponse(
    prompt: string,
    modelConfig: any
  ): Promise<any | null> {
    try {
      const cacheKey = this.generateCacheKey(
        prompt,
        modelConfig
      );
      const cached = await this.redis.get(cacheKey);

      if (cached) {
        const response = JSON.parse(cached);
        console.log(`Cache hit for key: ${cacheKey}`);
        return response;
      }

      return null;
    } catch (error) {
      console.error('Cache retrieval error:', error);
      return null;
    }
  }

  // 応答をキャッシュに保存
  async setCachedResponse(
    prompt: string,
    modelConfig: any,
    response: any,
    ttl?: number
  ): Promise<void> {
    try {
      const cacheKey = this.generateCacheKey(
        prompt,
        modelConfig
      );
      const cacheData = {
        ...response,
        cached_at: Date.now(),
        ttl: ttl || this.defaultTTL,
      };

      await this.redis.setex(
        cacheKey,
        ttl || this.defaultTTL,
        JSON.stringify(cacheData)
      );

      console.log(`Response cached with key: ${cacheKey}`);
    } catch (error) {
      console.error('Cache storage error:', error);
    }
  }

  // 類似クエリの検索
  async findSimilarCachedResponses(
    prompt: string,
    threshold: number = 0.8
  ): Promise<any[]> {
    try {
      const keys = await this.redis.keys('llm_cache:*');
      const similarResponses = [];

      for (const key of keys.slice(0, 100)) {
        // 最大100件まで検索
        const cached = await this.redis.get(key);
        if (cached) {
          const response = JSON.parse(cached);
          const similarity = this.calculateSimilarity(
            prompt,
            response.prompt || ''
          );

          if (similarity > threshold) {
            similarResponses.push({
              ...response,
              similarity_score: similarity,
            });
          }
        }
      }

      return similarResponses.sort(
        (a, b) => b.similarity_score - a.similarity_score
      );
    } catch (error) {
      console.error('Similar cache search error:', error);
      return [];
    }
  }

  private calculateSimilarity(
    text1: string,
    text2: string
  ): number {
    // 簡易的な類似度計算(実際の実装では、より高度な手法を使用)
    const words1 = text1.toLowerCase().split(/\s+/);
    const words2 = text2.toLowerCase().split(/\s+/);
    const intersection = words1.filter((word) =>
      words2.includes(word)
    );
    const union = [...new Set([...words1, ...words2])];

    return intersection.length / union.length;
  }
}

並列処理による高速化

複数のモデルに同時にリクエストを送信し、最初に返ってきた応答を使用する手法です。

typescript// parallel-processing.ts
class ParallelModelProcessor {
  private modelSwitcher: ModelSwitcher;
  private cache: ResponseCache;

  constructor(
    modelSwitcher: ModelSwitcher,
    cache: ResponseCache
  ) {
    this.modelSwitcher = modelSwitcher;
    this.cache = cache;
  }

  async getRaceResponse(
    prompt: string,
    modelConfigs: ModelConfig[]
  ): Promise<any> {
    // 並列でリクエストを送信
    const promises = modelConfigs.map(
      async (config, index) => {
        try {
          const response =
            await this.modelSwitcher.generateResponse(
              prompt,
              config
            );
          return {
            ...response,
            response_order: index,
            completed_at: Date.now(),
          };
        } catch (error) {
          throw {
            error,
            config,
            response_order: index,
          };
        }
      }
    );

    try {
      // 最初に完了した応答を返す
      const fastestResponse = await Promise.race(promises);

      // バックグラウンドで残りの応答もキャッシュに保存
      this.cacheRemainingResponses(
        promises,
        prompt,
        modelConfigs
      );

      return fastestResponse;
    } catch (error) {
      // 全てのリクエストが失敗した場合の処理
      const results = await Promise.allSettled(promises);
      const errors = results
        .filter((result) => result.status === 'rejected')
        .map((result) => result.reason);

      throw new Error(
        `All models failed: ${JSON.stringify(errors)}`
      );
    }
  }

  private async cacheRemainingResponses(
    promises: Promise<any>[],
    prompt: string,
    modelConfigs: ModelConfig[]
  ): Promise<void> {
    try {
      const results = await Promise.allSettled(promises);

      results.forEach((result, index) => {
        if (result.status === 'fulfilled') {
          this.cache.setCachedResponse(
            prompt,
            modelConfigs[index],
            result.value,
            7200 // 2時間キャッシュ
          );
        }
      });
    } catch (error) {
      console.error('Background caching error:', error);
    }
  }

  // 応答品質による選択
  async getBestQualityResponse(
    prompt: string,
    modelConfigs: ModelConfig[]
  ): Promise<any> {
    const responses = await Promise.allSettled(
      modelConfigs.map((config) =>
        this.modelSwitcher.generateResponse(prompt, config)
      )
    );

    const successfulResponses = responses
      .filter((result) => result.status === 'fulfilled')
      .map((result) => result.value);

    if (successfulResponses.length === 0) {
      throw new Error('No successful responses received');
    }

    // 応答品質スコアを計算
    const scoredResponses = successfulResponses.map(
      (response) => ({
        ...response,
        quality_score: this.calculateQualityScore(
          response,
          prompt
        ),
      })
    );

    // 最高品質の応答を返す
    return scoredResponses.reduce((best, current) =>
      current.quality_score > best.quality_score
        ? current
        : best
    );
  }

  private calculateQualityScore(
    response: any,
    prompt: string
  ): number {
    let score = 0;
    const content = response.content || '';

    // 応答長による評価
    const contentLength = content.length;
    if (contentLength > 50 && contentLength < 5000) {
      score += 20;
    }

    // コードブロックの存在
    if (
      prompt.includes('コード') &&
      content.includes('```')
    ) {
      score += 30;
    }

    // 日本語の自然さ
    const japaneseRatio =
      (
        content.match(
          /[\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF]/g
        ) || []
      ).length / content.length;
    score += japaneseRatio * 20;

    // 構造化された回答
    if (
      content.includes('##') ||
      content.includes('・') ||
      content.includes('1.')
    ) {
      score += 15;
    }

    return score;
  }
}

コスト効率化とリソース管理

AI モデルの使用コストを最適化し、予算内で最大の価値を得るための戦略を実装します。

コスト追跡システム

typescript// cost-tracker.ts
interface UsageMetrics {
  providerId: string;
  modelName: string;
  inputTokens: number;
  outputTokens: number;
  requestCount: number;
  totalCost: number;
  timestamp: Date;
}

class CostTracker {
  private costRates: Map<string, any> = new Map();
  private dailyUsage: Map<string, UsageMetrics> = new Map();
  private monthlyBudget: number;
  private alertThreshold: number;

  constructor(
    monthlyBudget: number = 1000,
    alertThreshold: number = 0.8
  ) {
    this.monthlyBudget = monthlyBudget;
    this.alertThreshold = alertThreshold;
    this.initializeCostRates();
    this.startDailyReset();
  }

  private initializeCostRates(): void {
    // 2025年4月時点の最新料金(1000トークンあたり)
    this.costRates.set('openai/gpt-4.1', {
      input: 0.002, // $2.00/1M tokens
      output: 0.008, // $8.00/1M tokens
    });

    this.costRates.set('openai/gpt-4.1-mini', {
      input: 0.0004, // $0.40/1M tokens
      output: 0.0016, // $1.60/1M tokens
    });

    this.costRates.set('openai/gpt-4.1-nano', {
      input: 0.0001, // $0.10/1M tokens
      output: 0.0004, // $0.40/1M tokens
    });

    this.costRates.set('openai/o3', {
      input: 0.001, // $1.00/1M tokens
      output: 0.004, // $4.00/1M tokens
    });

    this.costRates.set('anthropic/claude-opus-4', {
      input: 0.015, // $15.00/1M tokens
      output: 0.075, // $75.00/1M tokens
    });

    this.costRates.set('anthropic/claude-sonnet-4', {
      input: 0.003, // $3.00/1M tokens
      output: 0.015, // $15.00/1M tokens
    });

    this.costRates.set('anthropic/claude-haiku-3.5', {
      input: 0.0008, // $0.80/1M tokens
      output: 0.004, // $4.00/1M tokens
    });
  }

  // 使用量とコストを記録
  trackUsage(
    providerId: string,
    modelName: string,
    inputTokens: number,
    outputTokens: number
  ): number {
    const modelKey = `${providerId}/${modelName}`;
    const rates = this.costRates.get(modelKey);

    if (!rates) {
      console.warn(
        `No cost rates found for model: ${modelKey}`
      );
      return 0;
    }

    const cost =
      (inputTokens / 1000) * rates.input +
      (outputTokens / 1000) * rates.output;

    // 日次使用量を更新
    const today = new Date().toISOString().split('T')[0];
    const dailyKey = `${today}:${modelKey}`;
    const existing = this.dailyUsage.get(dailyKey) || {
      providerId,
      modelName,
      inputTokens: 0,
      outputTokens: 0,
      requestCount: 0,
      totalCost: 0,
      timestamp: new Date(),
    };

    existing.inputTokens += inputTokens;
    existing.outputTokens += outputTokens;
    existing.requestCount += 1;
    existing.totalCost += cost;

    this.dailyUsage.set(dailyKey, existing);

    // 予算アラートチェック
    this.checkBudgetAlert();

    return cost;
  }

  // 予算に基づくモデル推奨
  recommendCostEffectiveModel(
    taskRequirements: TaskRequirements,
    availableModels: ModelCapabilities[]
  ): ModelCapabilities {
    const currentSpend = this.getCurrentMonthlySpend();
    const remainingBudget =
      this.monthlyBudget - currentSpend;
    const daysRemaining = this.getDaysRemainingInMonth();
    const dailyBudget = remainingBudget / daysRemaining;

    // 今日の使用量チェック
    const todaySpend = this.getTodaySpend();

    if (todaySpend > dailyBudget * 0.8) {
      // 予算逼迫時は最安モデルを推奨
      return availableModels.reduce((cheapest, current) =>
        current.costEfficiency > cheapest.costEfficiency
          ? current
          : cheapest
      );
    }

    // 通常時はバランス考慮
    return availableModels.reduce((best, current) => {
      const currentScore =
        this.calculateCostEfficiencyScore(
          current,
          taskRequirements
        );
      const bestScore = this.calculateCostEfficiencyScore(
        best,
        taskRequirements
      );
      return currentScore > bestScore ? current : best;
    });
  }

  private calculateCostEfficiencyScore(
    model: ModelCapabilities,
    requirements: TaskRequirements
  ): number {
    // パフォーマンス vs コストのバランススコア
    const performanceScore = this.getPerformanceScore(
      model,
      requirements
    );
    const costMultiplier = model.costEfficiency / 10; // 1-10スケールを0.1-1.0に正規化

    return performanceScore * costMultiplier;
  }

  private getPerformanceScore(
    model: ModelCapabilities,
    requirements: TaskRequirements
  ): number {
    switch (requirements.taskType) {
      case 'coding':
        return model.codeGeneration;
      case 'creative':
        return model.creativeWriting;
      case 'analysis':
        return model.logicalReasoning;
      case 'translation':
        return model.multilingualSupport;
      default:
        return (model.speed + model.accuracy) / 2;
    }
  }

  // 予算アラート
  private checkBudgetAlert(): void {
    const currentSpend = this.getCurrentMonthlySpend();
    const budgetRatio = currentSpend / this.monthlyBudget;

    if (budgetRatio > this.alertThreshold) {
      console.warn(
        `Budget alert: ${(budgetRatio * 100).toFixed(
          1
        )}% of monthly budget used`
      );

      // Slackやメール通知の実装例
      this.sendBudgetAlert(budgetRatio, currentSpend);
    }
  }

  private async sendBudgetAlert(
    ratio: number,
    currentSpend: number
  ): Promise<void> {
    const alertData = {
      budget_ratio: ratio,
      current_spend: currentSpend,
      monthly_budget: this.monthlyBudget,
      alert_time: new Date().toISOString(),
    };

    // Webhook送信例
    try {
      await fetch(process.env.ALERT_WEBHOOK_URL || '', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          text: `AI予算アラート: 月次予算の${(
            ratio * 100
          ).toFixed(1)}%を使用しました`,
          data: alertData,
        }),
      });
    } catch (error) {
      console.error('Alert sending failed:', error);
    }
  }

  // ユーティリティメソッド
  private getCurrentMonthlySpend(): number {
    const now = new Date();
    const monthKey = `${now.getFullYear()}-${String(
      now.getMonth() + 1
    ).padStart(2, '0')}`;

    let totalSpend = 0;
    for (const [key, usage] of this.dailyUsage) {
      if (key.startsWith(monthKey)) {
        totalSpend += usage.totalCost;
      }
    }

    return totalSpend;
  }

  private getTodaySpend(): number {
    const today = new Date().toISOString().split('T')[0];

    let todaySpend = 0;
    for (const [key, usage] of this.dailyUsage) {
      if (key.startsWith(today)) {
        todaySpend += usage.totalCost;
      }
    }

    return todaySpend;
  }

  private getDaysRemainingInMonth(): number {
    const now = new Date();
    const lastDay = new Date(
      now.getFullYear(),
      now.getMonth() + 1,
      0
    ).getDate();
    return lastDay - now.getDate() + 1;
  }

  private startDailyReset(): void {
    // 毎日午前0時にデータをリセット(古いデータは保持)
    setInterval(() => {
      const cutoffDate = new Date();
      cutoffDate.setDate(cutoffDate.getDate() - 30); // 30日より古いデータを削除

      for (const [key, usage] of this.dailyUsage) {
        if (usage.timestamp < cutoffDate) {
          this.dailyUsage.delete(key);
        }
      }
    }, 24 * 60 * 60 * 1000); // 24時間間隔
  }
}

エラーハンドリングとフォールバック機能

複数のプロバイダーを使用する際の包括的なエラーハンドリング戦略を実装します。

高度なエラーハンドリングシステム

typescript// error-handler.ts
interface ErrorContext {
  originalPrompt: string;
  modelConfig: ModelConfig;
  attemptNumber: number;
  timestamp: Date;
  errorCode?: string;
  errorMessage?: string;
  fallbackStrategy?: string;
}

class AdvancedErrorHandler {
  private fallbackChain: ModelConfig[];
  private retrySettings: Map<string, number> = new Map();
  private circuitBreakers: Map<string, CircuitBreaker> =
    new Map();

  constructor() {
    this.initializeFallbackChain();
    this.initializeRetrySettings();
    this.initializeCircuitBreakers();
  }

  private initializeFallbackChain(): void {
    // フォールバック優先順位の設定
    this.fallbackChain = [
      {
        provider: 'openai',
        model: 'gpt-4.1',
        maxTokens: 32000,
        temperature: 0.7,
      },
      {
        provider: 'anthropic',
        model: 'claude-sonnet-4',
        maxTokens: 200000,
        temperature: 0.7,
      },
      {
        provider: 'openai',
        model: 'gpt-4.1-mini',
        maxTokens: 32000,
        temperature: 0.7,
      },
      {
        provider: 'anthropic',
        model: 'claude-haiku-3.5',
        maxTokens: 200000,
        temperature: 0.5,
      },
    ];
  }

  private initializeRetrySettings(): void {
    // エラータイプ別のリトライ回数
    this.retrySettings.set('RATE_LIMIT_EXCEEDED', 3);
    this.retrySettings.set('MODEL_OVERLOADED', 2);
    this.retrySettings.set('NETWORK_TIMEOUT', 3);
    this.retrySettings.set('TEMPORARY_UNAVAILABLE', 2);
    this.retrySettings.set('CONTEXT_LENGTH_EXCEEDED', 1);
    this.retrySettings.set('INVALID_REQUEST_ERROR', 0); // リトライしない
  }

  private initializeCircuitBreakers(): void {
    // プロバイダー別のサーキットブレーカー
    ['openai', 'anthropic'].forEach((provider) => {
      this.circuitBreakers.set(
        provider,
        new CircuitBreaker({
          failureThreshold: 5,
          resetTimeout: 60000, // 1分
          monitoringPeriod: 300000, // 5分
        })
      );
    });
  }

  async handleWithFallback(
    prompt: string,
    config: ModelConfig,
    modelSwitcher: ModelSwitcher,
    context: ErrorContext = {
      originalPrompt: prompt,
      modelConfig: config,
      attemptNumber: 1,
      timestamp: new Date(),
    }
  ): Promise<any> {
    const circuitBreaker = this.circuitBreakers.get(
      config.provider
    );

    // サーキットブレーカーのチェック
    if (circuitBreaker && circuitBreaker.isOpen()) {
      console.log(
        `Circuit breaker open for ${config.provider}, switching to fallback`
      );
      return await this.tryFallback(
        prompt,
        context,
        modelSwitcher
      );
    }

    try {
      const response = await modelSwitcher.generateResponse(
        prompt,
        config
      );

      // 成功時はサーキットブレーカーをリセット
      if (circuitBreaker) {
        circuitBreaker.recordSuccess();
      }

      return response;
    } catch (error) {
      console.error(
        `Error with ${config.provider}/${config.model}:`,
        error
      );

      // サーキットブレーカーに失敗を記録
      if (circuitBreaker) {
        circuitBreaker.recordFailure();
      }

      const errorCode = this.classifyError(error);
      const enhancedContext: ErrorContext = {
        ...context,
        errorCode,
        errorMessage: error.message,
        attemptNumber: context.attemptNumber + 1,
      };

      return await this.handleSpecificError(
        prompt,
        error,
        enhancedContext,
        modelSwitcher
      );
    }
  }

  private classifyError(error: any): string {
    const message = error.message?.toLowerCase() || '';
    const status = error.status || error.statusCode;

    // OpenAI specific errors
    if (
      message.includes('rate limit exceeded') ||
      status === 429
    ) {
      return 'RATE_LIMIT_EXCEEDED';
    }
    if (
      message.includes('model is currently overloaded') ||
      status === 503
    ) {
      return 'MODEL_OVERLOADED';
    }
    if (
      message.includes('maximum context length exceeded')
    ) {
      return 'CONTEXT_LENGTH_EXCEEDED';
    }
    if (
      message.includes('invalid api key') ||
      status === 401
    ) {
      return 'INVALID_API_KEY';
    }
    if (message.includes('insufficient quota')) {
      return 'QUOTA_EXCEEDED';
    }

    // Anthropic specific errors
    if (error.error?.type === 'rate_limit_error') {
      return 'RATE_LIMIT_EXCEEDED';
    }
    if (error.error?.type === 'overloaded_error') {
      return 'MODEL_OVERLOADED';
    }

    // Network errors
    if (
      message.includes('timeout') ||
      message.includes('ECONNRESET')
    ) {
      return 'NETWORK_TIMEOUT';
    }
    if (
      message.includes('network error') ||
      status >= 500
    ) {
      return 'NETWORK_ERROR';
    }

    return 'UNKNOWN_ERROR';
  }

  private async handleSpecificError(
    prompt: string,
    error: any,
    context: ErrorContext,
    modelSwitcher: ModelSwitcher
  ): Promise<any> {
    const { errorCode, attemptNumber } = context;
    const maxRetries =
      this.retrySettings.get(errorCode) || 0;

    switch (errorCode) {
      case 'RATE_LIMIT_EXCEEDED':
        return await this.handleRateLimit(
          prompt,
          context,
          modelSwitcher
        );

      case 'MODEL_OVERLOADED':
        return await this.handleOverload(
          prompt,
          context,
          modelSwitcher
        );

      case 'CONTEXT_LENGTH_EXCEEDED':
        return await this.handleContextLength(
          prompt,
          context,
          modelSwitcher
        );

      case 'NETWORK_TIMEOUT':
        if (attemptNumber <= maxRetries) {
          await this.exponentialBackoff(attemptNumber);
          return await this.handleWithFallback(
            prompt,
            context.modelConfig,
            modelSwitcher,
            context
          );
        }
        return await this.tryFallback(
          prompt,
          context,
          modelSwitcher
        );

      case 'INVALID_API_KEY':
      case 'QUOTA_EXCEEDED':
        // 即座に別プロバイダーに切り替え
        return await this.tryFallback(
          prompt,
          context,
          modelSwitcher
        );

      default:
        if (attemptNumber <= 2) {
          await this.exponentialBackoff(attemptNumber);
          return await this.handleWithFallback(
            prompt,
            context.modelConfig,
            modelSwitcher,
            context
          );
        }
        return await this.tryFallback(
          prompt,
          context,
          modelSwitcher
        );
    }
  }

  private async handleRateLimit(
    prompt: string,
    context: ErrorContext,
    modelSwitcher: ModelSwitcher
  ): Promise<any> {
    // レート制限の場合は別プロバイダーに即座に切り替え
    const currentProvider = context.modelConfig.provider;
    const alternativeConfig = this.fallbackChain.find(
      (config) => config.provider !== currentProvider
    );

    if (alternativeConfig) {
      console.log(
        `Rate limited on ${currentProvider}, switching to ${alternativeConfig.provider}`
      );
      context.fallbackStrategy = 'provider_switch';
      return await this.handleWithFallback(
        prompt,
        alternativeConfig,
        modelSwitcher,
        context
      );
    }

    // 全プロバイダーで制限されている場合は待機
    await this.exponentialBackoff(3);
    return await this.tryFallback(
      prompt,
      context,
      modelSwitcher
    );
  }

  private async handleOverload(
    prompt: string,
    context: ErrorContext,
    modelSwitcher: ModelSwitcher
  ): Promise<any> {
    // 同プロバイダーの軽量モデルに切り替え
    const { provider } = context.modelConfig;
    const lightweightModel = this.fallbackChain.find(
      (config) =>
        config.provider === provider &&
        config.model !== context.modelConfig.model
    );

    if (lightweightModel) {
      console.log(
        `Model overloaded, switching to lightweight model: ${lightweightModel.model}`
      );
      context.fallbackStrategy = 'lightweight_model';
      return await this.handleWithFallback(
        prompt,
        lightweightModel,
        modelSwitcher,
        context
      );
    }

    return await this.tryFallback(
      prompt,
      context,
      modelSwitcher
    );
  }

  private async handleContextLength(
    prompt: string,
    context: ErrorContext,
    modelSwitcher: ModelSwitcher
  ): Promise<any> {
    // 長いコンテキストに対応したモデルに切り替え
    const longContextModel = this.fallbackChain.find(
      (config) => config.model.includes('claude') // Claudeは長いコンテキストに対応
    );

    if (
      longContextModel &&
      longContextModel.model !== context.modelConfig.model
    ) {
      console.log(
        `Context too long, switching to: ${longContextModel.model}`
      );
      context.fallbackStrategy = 'long_context_model';
      return await this.handleWithFallback(
        prompt,
        longContextModel,
        modelSwitcher,
        context
      );
    }

    // プロンプトの分割処理
    return await this.handleLongPrompt(
      prompt,
      context,
      modelSwitcher
    );
  }

  private async handleLongPrompt(
    prompt: string,
    context: ErrorContext,
    modelSwitcher: ModelSwitcher
  ): Promise<any> {
    // プロンプトを複数のチャンクに分割
    const chunks = this.splitPrompt(prompt, 4000); // 4000文字ずつ分割
    const responses = [];

    for (const chunk of chunks) {
      try {
        const response =
          await modelSwitcher.generateResponse(chunk, {
            ...context.modelConfig,
            maxTokens: 1000,
          });
        responses.push(response.content);
      } catch (error) {
        console.error('Chunk processing error:', error);
        responses.push(
          `[エラー: チャンク処理に失敗しました]`
        );
      }
    }

    // 分割した応答を統合
    return {
      content: responses.join('\n\n'),
      provider: context.modelConfig.provider,
      model: context.modelConfig.model,
      processing_strategy: 'chunked',
      chunk_count: chunks.length,
    };
  }

  private splitPrompt(
    prompt: string,
    maxLength: number
  ): string[] {
    const chunks = [];
    let currentChunk = '';

    const sentences = prompt.split(/[。!?\n]/);

    for (const sentence of sentences) {
      if ((currentChunk + sentence).length <= maxLength) {
        currentChunk += sentence + '。';
      } else {
        if (currentChunk) {
          chunks.push(currentChunk.trim());
        }
        currentChunk = sentence + '。';
      }
    }

    if (currentChunk) {
      chunks.push(currentChunk.trim());
    }

    return chunks;
  }

  private async tryFallback(
    prompt: string,
    context: ErrorContext,
    modelSwitcher: ModelSwitcher
  ): Promise<any> {
    // フォールバックチェーンを順次試行
    for (const fallbackConfig of this.fallbackChain) {
      if (
        fallbackConfig.provider ===
          context.modelConfig.provider &&
        fallbackConfig.model === context.modelConfig.model
      ) {
        continue; // 同じモデルはスキップ
      }

      const circuitBreaker = this.circuitBreakers.get(
        fallbackConfig.provider
      );
      if (circuitBreaker && circuitBreaker.isOpen()) {
        continue; // サーキットブレーカーが開いているプロバイダーはスキップ
      }

      try {
        console.log(
          `Trying fallback: ${fallbackConfig.provider}/${fallbackConfig.model}`
        );
        return await modelSwitcher.generateResponse(
          prompt,
          fallbackConfig
        );
      } catch (error) {
        console.error(
          `Fallback failed for ${fallbackConfig.provider}/${fallbackConfig.model}:`,
          error
        );
        continue;
      }
    }

    // 全てのフォールバックが失敗した場合
    throw new Error(
      `All fallback options exhausted. Original error: ${context.errorMessage}`
    );
  }

  private async exponentialBackoff(
    attempt: number
  ): Promise<void> {
    const delay = Math.min(
      1000 * Math.pow(2, attempt - 1),
      30000
    ); // 最大30秒
    console.log(
      `Waiting ${delay}ms before retry (attempt ${attempt})`
    );
    await new Promise((resolve) =>
      setTimeout(resolve, delay)
    );
  }
}

// サーキットブレーカーの実装
class CircuitBreaker {
  private failureCount: number = 0;
  private lastFailureTime: number = 0;
  private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';

  constructor(
    private options: {
      failureThreshold: number;
      resetTimeout: number;
      monitoringPeriod: number;
    }
  ) {}

  isOpen(): boolean {
    if (this.state === 'OPEN') {
      if (
        Date.now() - this.lastFailureTime >
        this.options.resetTimeout
      ) {
        this.state = 'HALF_OPEN';
        return false;
      }
      return true;
    }
    return false;
  }

  recordSuccess(): void {
    this.failureCount = 0;
    this.state = 'CLOSED';
  }

  recordFailure(): void {
    this.failureCount++;
    this.lastFailureTime = Date.now();

    if (
      this.failureCount >= this.options.failureThreshold
    ) {
      this.state = 'OPEN';
    }
  }
}

具体例

ここからは、実際の業務で活用できる具体的な実装例をご紹介します。これらの例を参考に、自社の AI アプリケーションに最適なモデル切り替え戦略を構築してください。

実践例 1:カスタマーサポートチャットボット

カスタマーサポートでは、問い合わせの複雑さに応じてモデルを動的に切り替える必要があります。

typescript// customer-support-bot.ts
class CustomerSupportBot {
  private modelSwitcher: ModelSwitcher;
  private costTracker: CostTracker;
  private errorHandler: AdvancedErrorHandler;

  constructor() {
    this.modelSwitcher = new ModelSwitcher();
    this.costTracker = new CostTracker(500); // 月予算$500
    this.errorHandler = new AdvancedErrorHandler();
  }

  async handleCustomerQuery(
    query: string,
    customerTier: string
  ): Promise<any> {
    // 顧客ランクと問い合わせ内容を分析
    const analysisResult = this.analyzeQuery(
      query,
      customerTier
    );

    try {
      // 最適なモデルを選択
      const modelConfig =
        this.selectModelForSupport(analysisResult);

      return await this.errorHandler.handleWithFallback(
        this.buildSupportPrompt(query, analysisResult),
        modelConfig,
        this.modelSwitcher
      );
    } catch (error) {
      // エラー時は最低限の応答を提供
      return {
        content:
          '申し訳ございません。技術的な問題が発生しました。しばらくお待ちいただくか、別の方法でお問い合わせください。',
        fallback: true,
        error_code: error.message,
      };
    }
  }

  private analyzeQuery(
    query: string,
    customerTier: string
  ): any {
    const urgencyKeywords = [
      '緊急',
      '至急',
      '今すぐ',
      'urgent',
      'asap',
    ];
    const complexityKeywords = [
      '設定',
      'インテグレーション',
      'API',
      'エラーコード',
      'database',
    ];
    const simpleKeywords = [
      'パスワード',
      'ログイン',
      '料金',
      'プラン変更',
    ];

    return {
      isUrgent: urgencyKeywords.some((keyword) =>
        query.includes(keyword)
      ),
      complexity: this.determineComplexity(
        query,
        complexityKeywords,
        simpleKeywords
      ),
      customerTier: customerTier,
      queryLength: query.length,
      language: this.detectLanguage(query),
    };
  }

  private selectModelForSupport(
    analysis: any
  ): ModelConfig {
    // 顧客ランクと緊急度に基づく選択
    if (
      analysis.customerTier === 'enterprise' ||
      analysis.isUrgent
    ) {
      if (analysis.complexity === 'high') {
        return {
          provider: 'openai',
          model: 'gpt-4.1',
          maxTokens: 32000,
          temperature: 0.3,
        };
      } else {
        return {
          provider: 'openai',
          model: 'gpt-4.1-mini',
          maxTokens: 32000,
          temperature: 0.3,
        };
      }
    }

    // 一般顧客向けの最適化
    if (analysis.complexity === 'low') {
      return {
        provider: 'anthropic',
        model: 'claude-haiku-3.5',
        maxTokens: 200000,
        temperature: 0.5,
      };
    } else {
      return {
        provider: 'anthropic',
        model: 'claude-sonnet-4',
        maxTokens: 200000,
        temperature: 0.4,
      };
    }
  }

  private buildSupportPrompt(
    query: string,
    analysis: any
  ): string {
    return `
あなたは親切で専門的なカスタマーサポート担当者です。
以下の問い合わせに適切に回答してください。

顧客情報:
- ランク: ${analysis.customerTier}
- 緊急度: ${analysis.isUrgent ? '高' : '通常'}
- 複雑度: ${analysis.complexity}

問い合わせ内容:
${query}

回答要件:
- 丁寧で分かりやすい日本語で回答
- 具体的な解決手順を提示
- 必要に応じて関連ドキュメントへのリンクを含める
- エスカレーションが必要な場合は明記
    `;
  }

  private determineComplexity(
    query: string,
    complexKeywords: string[],
    simpleKeywords: string[]
  ): string {
    if (
      complexKeywords.some((keyword) =>
        query.includes(keyword)
      )
    ) {
      return 'high';
    } else if (
      simpleKeywords.some((keyword) =>
        query.includes(keyword)
      )
    ) {
      return 'low';
    }
    return 'medium';
  }

  private detectLanguage(text: string): string {
    const japaneseRatio =
      (
        text.match(
          /[\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF]/g
        ) || []
      ).length / text.length;
    return japaneseRatio > 0.3 ? 'japanese' : 'english';
  }
}

実践例 2:コンテンツ生成プラットフォーム

マーケティングコンテンツ生成では、創造性と効率性のバランスが重要です。

typescript// content-generator.ts
class ContentGenerator {
  private modelSwitcher: ModelSwitcher;
  private qualityAnalyzer: ContentQualityAnalyzer;

  async generateMarketingContent(
    contentType: 'blog' | 'social' | 'email' | 'ad-copy',
    brief: string,
    targetAudience: string,
    budget: 'low' | 'medium' | 'high'
  ): Promise<any> {
    const contentStrategy = this.planContentStrategy(
      contentType,
      brief,
      budget
    );

    try {
      // 初期ドラフト生成
      const draft = await this.generateInitialDraft(
        brief,
        contentStrategy
      );

      // 品質評価
      const qualityScore =
        await this.qualityAnalyzer.evaluateContent(
          draft.content
        );

      // 品質が低い場合は高性能モデルで再生成
      if (qualityScore < 0.7 && budget !== 'low') {
        console.log(
          'Quality below threshold, regenerating with premium model'
        );
        return await this.regenerateWithPremiumModel(
          brief,
          contentStrategy,
          draft
        );
      }

      return {
        ...draft,
        quality_score: qualityScore,
        generation_strategy: contentStrategy.strategy,
      };
    } catch (error) {
      console.error('Content generation error:', error);
      throw error;
    }
  }

  private planContentStrategy(
    contentType: string,
    brief: string,
    budget: string
  ): any {
    // コンテンツタイプ別の戦略
    const strategies = {
      blog: {
        primaryModel:
          budget === 'high'
            ? 'anthropic/claude-sonnet-4'
            : 'openai/gpt-4.1-mini',
        creativity: 0.8,
        maxTokens: 32000,
        structureRequired: true,
      },
      social: {
        primaryModel: 'anthropic/claude-haiku-3.5',
        creativity: 0.9,
        maxTokens: 500,
        structureRequired: false,
      },
      email: {
        primaryModel: 'openai/gpt-4.1-mini',
        creativity: 0.6,
        maxTokens: 32000,
        structureRequired: true,
      },
      'ad-copy': {
        primaryModel:
          budget === 'high'
            ? 'openai/gpt-4.1'
            : 'anthropic/claude-haiku-3.5',
        creativity: 0.9,
        maxTokens: 200,
        structureRequired: false,
      },
    };

    return {
      ...strategies[contentType],
      strategy: `${contentType}_${budget}_budget`,
      requiresReview: budget === 'high',
    };
  }

  private async generateInitialDraft(
    brief: string,
    strategy: any
  ): Promise<any> {
    const prompt = this.buildContentPrompt(brief, strategy);

    const modelConfig = {
      provider: strategy.primaryModel.split('/')[0],
      model: strategy.primaryModel.split('/')[1],
      maxTokens: strategy.maxTokens,
      temperature: strategy.creativity,
    };

    return await this.modelSwitcher.generateResponse(
      prompt,
      modelConfig
    );
  }

  private buildContentPrompt(
    brief: string,
    strategy: any
  ): string {
    let prompt = `
あなたは経験豊富なマーケティングライターです。
以下の要件に基づいて、魅力的なコンテンツを作成してください。

コンテンツ要件:
${brief}

作成ガイドライン:
- ターゲットオーディエンスを意識した文体
- SEOを考慮したキーワード配置
- エンゲージメントを高める文章構成
    `;

    if (strategy.structureRequired) {
      prompt += `
- 明確な見出し構造(H2, H3の適切な使用)
- 読みやすい段落分け
- 箇条書きや番号リストの活用
      `;
    }

    return prompt;
  }
}

// コンテンツ品質分析クラス
class ContentQualityAnalyzer {
  async evaluateContent(content: string): Promise<number> {
    let score = 0;

    // 基本的な品質指標
    score += this.evaluateLength(content) * 0.2;
    score += this.evaluateStructure(content) * 0.3;
    score += this.evaluateReadability(content) * 0.3;
    score += this.evaluateEngagement(content) * 0.2;

    return Math.min(score, 1.0);
  }

  private evaluateLength(content: string): number {
    const length = content.length;
    if (length < 100) return 0.3;
    if (length < 500) return 0.7;
    if (length < 2000) return 1.0;
    if (length < 5000) return 0.8;
    return 0.6; // 長すぎる場合
  }

  private evaluateStructure(content: string): number {
    let score = 0;

    // 見出しの存在
    if (content.includes('##') || content.includes('#'))
      score += 0.3;

    // 段落分け
    const paragraphs = content.split('\n\n').length;
    if (paragraphs > 2) score += 0.4;

    // リスト構造
    if (
      content.includes('・') ||
      content.includes('1.') ||
      content.includes('-')
    ) {
      score += 0.3;
    }

    return score;
  }

  private evaluateReadability(content: string): number {
    let score = 0;

    // 平均文長
    const sentences = content
      .split(/[。!?]/)
      .filter((s) => s.trim().length > 0);
    const avgLength = content.length / sentences.length;

    if (avgLength > 20 && avgLength < 60) score += 0.5;

    // 漢字の適切な使用
    const kanjiRatio =
      (content.match(/[\u4E00-\u9FAF]/g) || []).length /
      content.length;
    if (kanjiRatio > 0.1 && kanjiRatio < 0.4) score += 0.5;

    return score;
  }

  private evaluateEngagement(content: string): number {
    let score = 0;

    // 感情表現
    const emotionalWords = [
      '素晴らしい',
      '驚くべき',
      '重要',
      '必見',
      '注目',
    ];
    if (
      emotionalWords.some((word) => content.includes(word))
    )
      score += 0.3;

    // 疑問文の使用
    if (
      content.includes('?') ||
      content.includes('でしょうか')
    )
      score += 0.3;

    // 読者への語りかけ
    if (
      content.includes('あなた') ||
      content.includes('皆さん')
    )
      score += 0.4;

    return score;
  }
}

実践例 3:多言語対応 AI アシスタント

グローバルサービスでは、言語ごとに最適なモデルを選択する必要があります。

typescript// multilingual-assistant.ts
class MultilingualAssistant {
  private languageDetector: LanguageDetector;
  private modelSwitcher: ModelSwitcher;

  async processMultilingualQuery(
    input: string,
    userContext?: any
  ): Promise<any> {
    // 言語検出
    const detectedLanguage =
      await this.languageDetector.detect(input);

    // 言語別最適モデル選択
    const modelConfig = this.selectLanguageOptimizedModel(
      detectedLanguage,
      input
    );

    // コンテキストを考慮したプロンプト構築
    const prompt = this.buildMultilingualPrompt(
      input,
      detectedLanguage,
      userContext
    );

    try {
      const response =
        await this.modelSwitcher.generateResponse(
          prompt,
          modelConfig
        );

      return {
        ...response,
        detected_language: detectedLanguage,
        model_used: `${modelConfig.provider}/${modelConfig.model}`,
        confidence: detectedLanguage.confidence,
      };
    } catch (error) {
      // 言語特有のエラーハンドリング
      return await this.handleLanguageSpecificError(
        input,
        detectedLanguage,
        error
      );
    }
  }

  private selectLanguageOptimizedModel(
    language: any,
    input: string
  ): ModelConfig {
    const { code, confidence } = language;

    // 言語別最適モデルマッピング
    const languageModelMap = {
      ja:
        confidence > 0.8
          ? 'openai/gpt-4.1'
          : 'anthropic/claude-sonnet-4',
      en: 'anthropic/claude-sonnet-4',
      zh: 'openai/gpt-4.1',
      ko: 'openai/gpt-4.1-mini',
      es: 'anthropic/claude-haiku-3.5',
      fr: 'anthropic/claude-haiku-3.5',
      de: 'openai/gpt-4.1-mini',
    };

    const modelName =
      languageModelMap[code] || 'openai/gpt-4.1-mini';
    const [provider, model] = modelName.split('/');

    return {
      provider: provider as 'openai' | 'anthropic',
      model: model,
      maxTokens: this.getTokensForLanguage(code),
      temperature: this.getTemperatureForLanguage(code),
    };
  }

  private getTokensForLanguage(
    languageCode: string
  ): number {
    // 言語特性を考慮したトークン数設定
    const tokenMap = {
      ja: 2000, // 日本語は情報密度が高い
      zh: 1800, // 中国語も同様
      en: 2500, // 英語は冗長になりがち
      ko: 2200,
      es: 2400,
      fr: 2400,
      de: 2300, // ドイツ語は複合語が多い
    };

    return tokenMap[languageCode] || 2000;
  }

  private getTemperatureForLanguage(
    languageCode: string
  ): number {
    // 言語の特性に応じた創造性レベル
    const tempMap = {
      ja: 0.6, // 日本語は適度な創造性
      en: 0.7, // 英語は表現豊か
      zh: 0.5, // 中国語は正確性重視
      ko: 0.6,
      es: 0.8, // ラテン系言語は表現豊か
      fr: 0.8,
      de: 0.5, // ドイツ語は正確性重視
    };

    return tempMap[languageCode] || 0.7;
  }

  private buildMultilingualPrompt(
    input: string,
    language: any,
    context?: any
  ): string {
    const languageInstructions = {
      ja: 'あなたは親切で知識豊富な日本語アシスタントです。丁寧語で回答してください。',
      en: 'You are a helpful and knowledgeable assistant. Please respond in clear, professional English.',
      zh: '你是一个乐于助人且知识丰富的助手。请用简洁专业的中文回答。',
      ko: '당신은 도움이 되고 지식이 풍부한 어시스턴트입니다. 정중한 한국어로 답변해 주세요.',
      es: 'Eres un asistente útil y conocedor. Por favor responde en español claro y profesional.',
      fr: 'Vous êtes un assistant utile et compétent. Veuillez répondre en français clair et professionnel.',
      de: 'Sie sind ein hilfreicher und sachkundiger Assistent. Bitte antworten Sie in klarem, professionellem Deutsch.',
    };

    const instruction =
      languageInstructions[language.code] ||
      languageInstructions['en'];

    let prompt = `${instruction}\n\n`;

    if (context) {
      prompt += `Context: ${JSON.stringify(context)}\n\n`;
    }

    prompt += `User Query: ${input}`;

    return prompt;
  }
}

// 言語検出クラス
class LanguageDetector {
  async detect(
    text: string
  ): Promise<{ code: string; confidence: number }> {
    // 簡易的な言語検出ロジック
    const patterns = {
      ja: /[\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF]/,
      zh: /[\u4E00-\u9FFF]/,
      ko: /[\uAC00-\uD7AF]/,
      en: /^[a-zA-Z\s\.,!?'"]+$/,
      es: /[ñáéíóúü]/i,
      fr: /[àâäéèêëïîôöùûüÿç]/i,
      de: /[äöüß]/i,
    };

    let bestMatch = { code: 'en', confidence: 0 };

    for (const [lang, pattern] of Object.entries(
      patterns
    )) {
      const matches = text.match(pattern);
      const confidence = matches
        ? matches.length / text.length
        : 0;

      if (confidence > bestMatch.confidence) {
        bestMatch = { code: lang, confidence };
      }
    }

    return bestMatch;
  }
}

まとめ

本記事では、Dify プラットフォームで OpenAI と Anthropic のモデルを効果的に切り替える実践的な手法をご紹介しました。重要なポイントを振り返ってみましょう。

主要な学習ポイント

モデル選択の体系化が何より重要です。各モデルの特性を数値化し、タスクの要件と照らし合わせることで、客観的で再現可能な選択基準を確立できます。感覚に頼るのではなく、データドリブンなアプローチが成功の鍵となります。

動的切り替えシステムの実装により、リアルタイムでの最適化が可能になります。Dify のワークフロー機能と API 統合を組み合わせることで、ユーザーが意識することなく、最適なモデルが自動選択される仕組みを構築できるでしょう。

パフォーマンスとコストの最適化は、持続可能な AI サービス運営の基盤です。キャッシュシステムや並列処理の活用により、応答速度を向上させつつ、コスト追跡により予算内での最大価値を実現することができます。

包括的なエラーハンドリングにより、サービスの信頼性が大幅に向上します。サーキットブレーカーパターンやフォールバック機能の実装で、単一障害点を排除し、常に安定したサービス提供が可能になります。

実装時の注意点

実際の導入では、以下の点にご注意ください。まず、段階的な導入を心がけることが重要です。いきなり全ての機能を実装するのではなく、基本的な切り替え機能から始めて、徐々に高度な機能を追加していくアプローチが成功確率を高めます。

監視体制の構築も欠かせません。各モデルのパフォーマンス、コスト、エラー率を継続的に監視し、定期的に切り替えロジックを見直すことで、最適化を継続できるでしょう。

セキュリティ面への配慮も忘れてはいけません。複数の API キーの管理、ログの適切な処理、機密情報の保護など、企業レベルでの運用では特に重要となります。

今後の展望

AI 技術の進歩は目覚ましく、新しいモデルが次々とリリースされています。本記事でご紹介した切り替えシステムは、このような変化にも柔軟に対応できる設計となっています。新しいプロバイダーやモデルが登場した際も、設定の追加や調整により、簡単に対応することができるでしょう。

また、マルチモーダル AI の普及に伴い、テキストだけでなく画像や音声を含む切り替えロジックの需要も高まると予想されます。本記事の基盤を活用して、そうした次世代の要求にも対応していくことが可能です。

継続的な改善を心がけ、ユーザーフィードバックとパフォーマンスデータに基づいて、切り替えロジックを進化させていくことが、長期的な成功につながります。AI の活用は一度の実装で完結するものではなく、継続的な最適化が必要な分野だということを忘れずに、取り組んでいただければと思います。

本記事が、皆さんの AI プロジェクトの成功に少しでもお役に立てれば幸いです。

関連リンク

公式ドキュメント

技術リソース

参考記事・リサーチ