開発ノート

モデル選定の実際:タスク別にどのモデルを使い分けるか

管理者2026.06.11 公開 ・ 24 min read
モデル選定の実際:タスク別にどのモデルを使い分けるか

01TL;DR

  • タスクを「必要な能力」で分類し、高性能クラスと軽量クラスを使い分けることが費用対効果の改善に直結します。
  • コスト・速度・品質の3軸は常にトレードオフの関係にあり、どの軸を優先するかをタスクごとに決めるのが選定の出発点です。
  • モデルの優劣は汎用ベンチマークよりも自分のタスクでの実測を基準にします。
  • ルーティングロジックをコードで実装すると、選定基準の変更が容易になります。
  • サンプルコードはTypeScript(Node.js v22.x / Anthropic SDK v0.26.x / OpenAI SDK v4.x)で記述しています。

02はじめに

この記事の対象読者

  • LLMを使ったシステムを開発・運用しており、「全部おまかせでいちばん賢いモデル」から脱却したいエンジニア
  • モデル選定の判断基準を言語化できておらず、なんとなく選んでいると感じている人
  • チームでモデル選定方針を共有したく、選定ロジックをコード化したい人

TypeScriptとNode.jsの基礎的な扱いを前提にしています。 特定モデルの詳細な性能評価は本記事の目的ではありません。 個々のモデルの能力や価格は世代交代が速く、本記事に掲載する数値は「一例」「目安」「執筆時点(2026年6月)の公表情報に基づく」ものであり、実際の運用では各プロバイダの公式ドキュメントを確認してください。

この記事で得られること

  • タスク特性ごとの要求水準の整理方法
  • コスト・速度・品質の3軸での比較フレームワーク
  • 自分のタスクで比較検証するベンチマークスクリプトの実装
  • モデルルーティングの実装パターン
  • 選定に迷ったときの早見表

03モデルクラスの整理から始める

「高性能」と「軽量」という分類

LLM APIを提供する各社のラインナップはおおむね次の2クラスに整理できます。

クラス 傾向 代表的な位置づけ
高性能クラス 複雑な推論・長文理解・多段階の思考に強い。入出力コストが高め。レイテンシも大きくなる傾向 フラグシップモデル、o系推論モデルなど
軽量クラス 定型処理・短い入出力・高スループットに向く。コストが低く、レイテンシも小さい ミニ系、ハイク系、フラッシュ系など

2026年6月時点では、高性能クラスと軽量クラスの入力コスト差はおおむね10〜30倍程度になっていることが多いです(プロバイダ・モデルによって大きく異なります)。

ただしこの分類は目安に過ぎず、同じ「高性能クラス」でも推論特化型のモデルと汎用モデルでは向き不向きが異なります。まずは「自分が使っているモデルをどのクラスと見なすか」を決めてから設計を始めるのが実用的です。

3軸のトレードオフ

モデル選定で常に意識すべき軸は3つです。

クラス コスト 速度 品質(別途評価)
高性能クラス 低(レイテンシ大) タスク固有の指標で測定
軽量クラス 高(レイテンシ小) タスク固有の指標で測定

コスト・速度・品質の3軸は同時にすべて最大化できません。タスクごとに優先する軸を決めることが選定の出発点です。

  • コスト: トークン単価 × 想定消費量で決まります。(関連記事: トークンコスト管理:消費を見積もり、最適化する)
  • 速度: Time to First Token (TTFT) と生成速度 (tokens/s) の2つで評価します。
  • 品質: タスク固有の評価指標(正確性・フォーマット遵守率・ハルシネーション率など)で測ります。

この3軸は同時にすべて最大化できません。「このタスクは品質を最優先、コストは二の次」「このタスクは速度が命でコストも許容範囲ならよい」という優先順位を決めることが、選定の実質的な作業です。


04タスク特性ごとの要求水準を整理する

タスクを分類する

まず自分のシステムに存在するタスクを以下の観点で棚卸しします。

推論の深さ

レベル 説明
単純抽出 入力から情報を取り出す。判断を必要としない 名前・日付の抽出、JSONのパース
判定・分類 選択肢から選ぶ。ロジックは単純 感情分析、カテゴリ分類、Yes/No判定
要約・変換 意味を保ちながら別形式に変換する 長文要約、翻訳、箇条書き化
複雑な推論 複数情報を統合し、多段階で結論を出す コード生成、根拠付き分析、設計レビュー
創造的生成 制約の中で新規性のある内容を生成する コピーライティング、アイデア出し

文脈の長さ

入力が長くなるほどモデルに求められる文脈理解力が上がります。 数百トークン以内の定型入力なら軽量クラスで十分なことが多いですが、長いドキュメントやチャット履歴を扱う場合は、長文の理解精度も選定基準に加わります。

フォーマット遵守の厳密さ

JSONや特定の構造を確実に返してほしい場合は、インストラクション追従能力が高いモデルを選ぶ理由になります。軽量クラスでも関数呼び出し(Function Calling)を使えばフォーマット問題はある程度解消できますが、複雑なスキーマになるほど高性能クラスの方が安定することがあります。

エラーコストの非対称性

タスクの種類によって、失敗したときの影響が大きく異なります。

  • 社内向けの補助的な要約処理であれば、精度が95%でも許容できるかもしれません。
  • 契約書の条項を分析して判断を補助するタスクなら、ハルシネーション1件が大きなリスクになります。

エラーコストが高い箇所に高性能クラスを使い、そうでない箇所に軽量クラスを使うという方針が、現場では自然に採用されることが多いです。

タスク種別ごとの目安

下記はあくまで参考です。実際の選定は後述のベンチマークで自分のタスクに合わせて検証してください。

タスク種別 推奨クラスの目安 判断の理由
単純な情報抽出(固定フォーマット) 軽量クラス 推論を必要としない。コストと速度を優先
感情分析・ラベル分類 軽量クラス 選択肢が明確。ファインチューニング済みモデルも候補
長文要約(概要レベル) 軽量クラス ある程度の品質低下を許容できることが多い
長文要約(精密・専門文書) 高性能クラス 微細なニュアンスの把握が必要
コード生成(定型・スニペット) 軽量クラス パターン化されたコードは軽量クラスで十分なことが多い
コード生成(複雑なロジック・設計) 高性能クラス 多段階の推論が必要
翻訳(汎用言語) 軽量クラス 一般的な翻訳は精度が安定している
翻訳(専門用語・ニュアンス重視) 高性能クラス ドメイン固有の判断が入る
多段階推論・根拠付き分析 高性能クラス 推論能力が直接品質に影響
ユーザー向けの対話(チャット) タスク依存 内容の複雑さによって判断

05ベンチマークスクリプトを実装する

なぜ汎用ベンチマークに頼らないのか

公開されているモデル評価ベンチマーク(MMLUやHumanEvalなど)は参考になりますが、自分のユースケースとは乖離があります。 筆者たちが経験した例を挙げると、汎用ベンチマークで同スコアの2モデルを比較したとき、自社の専門領域の文書要約では片方が明確に優れていたということがありました。

「自分のタスクで実測する」ことが、正確な選定の唯一の方法です。

評価パイプラインの設計

flowchart TD
    A["テストケース集\n(input + expected_output)"]
    B["各モデルへ並列リクエスト"]
    C["レイテンシ・コスト・出力を記録"]
    D["自動評価(スコアリング)"]
    E["結果を集計・比較レポート出力"]

    A --> B --> C --> D --> E

テストケースの準備

まずテストケースを定義します。 本番データに近い入力と、期待する出力(または評価基準)をセットで用意します。

// benchmark/types.ts

export interface TestCase {
  id: string;
  input: string;
  systemPrompt?: string;
  /** 期待する出力(文字列一致、キーワード含有、または評価関数で使用) */
  expected?: string;
  /** タスク種別(レポート集計用) */
  taskType: "extraction" | "classification" | "summarization" | "code-gen" | "reasoning";
}

export interface BenchmarkResult {
  caseId: string;
  modelId: string;
  modelClass: "high-performance" | "lightweight";
  output: string;
  /** Time to First Token (ms) */
  ttftMs: number;
  /** 全レスポンス受信までの時間 (ms) */
  totalMs: number;
  inputTokens: number;
  outputTokens: number;
  /** スコア: 0.0〜1.0 */
  qualityScore: number;
  error?: string;
}

テストケースのサンプルです。

// benchmark/test-cases.ts
import { TestCase } from "./types.js";

export const testCases: TestCase[] = [
  {
    id: "extract-001",
    taskType: "extraction",
    systemPrompt: "以下のテキストから日付と金額を抽出し、JSON形式で返してください。",
    input: "2026年5月15日に請求書を受領しました。金額は税込110,000円です。",
    expected: JSON.stringify({ date: "2026-05-15", amount: 110000 }),
  },
  {
    id: "classify-001",
    taskType: "classification",
    systemPrompt:
      "以下のサポートメッセージを「技術的問題」「請求・支払い」「機能要望」「その他」のいずれかに分類し、分類名だけを返してください。",
    input: "ログインボタンを押してもエラーが出て進めません。昨日から続いています。",
    expected: "技術的問題",
  },
  {
    id: "summarize-001",
    taskType: "summarization",
    systemPrompt: "以下の議事録を3行以内で要約してください。",
    input: `
      参加者: 山田、田中、鈴木
      議題: 来月のリリース計画
      内容:
      - 山田より、UIの実装が80%完了、残り20%は来週中に完了予定と報告があった。
      - 田中より、バックエンドAPIのテストカバレッジが70%に達したと報告。90%を目標に今週中に追加テストを書く。
      - 鈴木より、リリース日は月末に設定し、前日にステージング環境でのテストを実施する提案があり全員合意。
      - 次回MTGは来週水曜日14時に設定。
    `,
    expected: "UI実装は来週完了予定、APIテストは今週中に90%へ。リリースは月末、前日にステージング確認。次回MTGは来週水曜14時。",
  },
  {
    id: "codegen-001",
    taskType: "code-gen",
    systemPrompt: "TypeScriptで、配列を受け取り重複を除いた配列を返す関数を実装してください。コードのみを返してください。",
    input: "入力: number[]、出力: number[]",
    expected: "Set",
  },
  {
    id: "reasoning-001",
    taskType: "reasoning",
    systemPrompt:
      "以下の状況を分析し、根拠を示した上で推奨アクションを提案してください。",
    input: `
      ECサイトの直近データ:
      - 先月比でカート追加数は+20%だが、購入完了率は-15%
      - モバイルの離脱率が先月比で+30%増加
      - ページ読み込み速度は先月比で+2秒(モバイルのみ)
    `,
  },
];

リクエスト実行とメトリクス収集

// benchmark/runner.ts
import Anthropic from "@anthropic-ai/sdk";
import OpenAI from "openai";
import { TestCase, BenchmarkResult } from "./types.js";

// Node.js v22.x / Anthropic SDK v0.26.x / OpenAI SDK v4.x
const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

export interface ModelConfig {
  modelId: string;
  provider: "anthropic" | "openai";
  modelClass: "high-performance" | "lightweight";
  /** 入力1Mトークンあたりのコスト(USD)。目安として設定 */
  inputCostPer1M: number;
  /** 出力1Mトークンあたりのコスト(USD)。目安として設定 */
  outputCostPer1M: number;
}

/**
 * 単一ケースを1モデルで実行し、メトリクスを返します。
 * エラー時はqualityScore: 0でエラー内容を記録します。
 */
export async function runSingleCase(
  testCase: TestCase,
  config: ModelConfig
): Promise<BenchmarkResult> {
  const startTime = Date.now();
  let ttftMs = 0;
  let output = "";
  let inputTokens = 0;
  let outputTokens = 0;
  let error: string | undefined;

  const messages: Array<{ role: "user" | "assistant"; content: string }> = [
    { role: "user", content: testCase.input },
  ];

  try {
    if (config.provider === "anthropic") {
      // Anthropic SDK: streamを使ってTTFTを計測します
      const stream = await anthropic.messages.stream({
        model: config.modelId,
        max_tokens: 1024,
        system: testCase.systemPrompt,
        messages,
      });

      let firstChunk = true;
      for await (const chunk of stream) {
        if (firstChunk) {
          ttftMs = Date.now() - startTime;
          firstChunk = false;
        }
        if (
          chunk.type === "content_block_delta" &&
          chunk.delta.type === "text_delta"
        ) {
          output += chunk.delta.text;
        }
      }

      const finalMessage = await stream.finalMessage();
      inputTokens = finalMessage.usage.input_tokens;
      outputTokens = finalMessage.usage.output_tokens;
    } else {
      // OpenAI SDK: streamを使ってTTFTを計測します
      const stream = await openai.chat.completions.create({
        model: config.modelId,
        messages: [
          ...(testCase.systemPrompt
            ? [{ role: "system" as const, content: testCase.systemPrompt }]
            : []),
          ...messages,
        ],
        stream: true,
        stream_options: { include_usage: true },
      });

      let firstChunk = true;
      for await (const chunk of stream) {
        if (firstChunk && chunk.choices.length > 0) {
          ttftMs = Date.now() - startTime;
          firstChunk = false;
        }
        const delta = chunk.choices[0]?.delta?.content;
        if (delta) output += delta;

        if (chunk.usage) {
          inputTokens = chunk.usage.prompt_tokens;
          outputTokens = chunk.usage.completion_tokens;
        }
      }
    }
  } catch (err) {
    error = err instanceof Error ? err.message : String(err);
  }

  const totalMs = Date.now() - startTime;
  const qualityScore = error ? 0 : evaluateQuality(testCase, output);

  return {
    caseId: testCase.id,
    modelId: config.modelId,
    modelClass: config.modelClass,
    output,
    ttftMs,
    totalMs,
    inputTokens,
    outputTokens,
    qualityScore,
    error,
  };
}

/**
 * 出力の品質をスコアリングします(0.0〜1.0)。
 * テストケースのtaskTypeに応じて評価方法を変えます。
 * より精密な評価が必要な場合は、LLM-as-a-judgeに置き換えてください。
 */
function evaluateQuality(testCase: TestCase, output: string): number {
  if (!output.trim()) return 0;

  if (!testCase.expected) {
    // expected がない場合は出力があれば暫定スコア 0.5 を返します
    return 0.5;
  }

  switch (testCase.taskType) {
    case "extraction": {
      // JSONとして解析し、期待値との一致度を確認します
      try {
        const parsed = JSON.parse(output.trim());
        const expected = JSON.parse(testCase.expected);
        const keys = Object.keys(expected);
        const matchCount = keys.filter(
          (k) => String(parsed[k]) === String(expected[k])
        ).length;
        return matchCount / keys.length;
      } catch {
        return 0;
      }
    }

    case "classification": {
      // 期待ラベルが出力に含まれているかを確認します
      return output.includes(testCase.expected) ? 1.0 : 0.0;
    }

    case "summarization": {
      // 期待キーワードの包含率で簡易評価します
      const keywords = testCase.expected.split(/[、。\s]+/).filter(Boolean);
      const matchCount = keywords.filter((kw) => output.includes(kw)).length;
      return matchCount / keywords.length;
    }

    case "code-gen": {
      // 期待するキーワード(例: "Set")が含まれているかを確認します
      return output.includes(testCase.expected) ? 1.0 : 0.0;
    }

    case "reasoning": {
      // reasoning タスクはLLM-as-a-judgeで評価するのが望ましいです
      // ここでは出力の長さと構造の有無で簡易スコアを返します
      const hasStructure = output.includes("推奨") || output.includes("根拠") || output.includes("理由");
      const lengthScore = Math.min(output.length / 200, 1.0);
      return hasStructure ? Math.max(lengthScore, 0.6) : lengthScore * 0.5;
    }

    default:
      return 0.5;
  }
}

並列実行とコスト計算

// benchmark/orchestrator.ts
import { testCases } from "./test-cases.js";
import { runSingleCase, ModelConfig } from "./runner.js";
import { BenchmarkResult } from "./types.js";

/**
 * 比較したいモデルをここに定義します。
 * コスト情報は目安として記入し、実際の運用では各プロバイダの最新価格を参照してください。
 */
const models: ModelConfig[] = [
  {
    modelId: "claude-opus-4-8",
    provider: "anthropic",
    modelClass: "high-performance",
    // 以下は説明用の目安値です。実際の価格は公式ドキュメントを確認してください
    inputCostPer1M: 15.0,
    outputCostPer1M: 75.0,
  },
  {
    modelId: "claude-haiku-4-5",
    provider: "anthropic",
    modelClass: "lightweight",
    inputCostPer1M: 0.8,
    outputCostPer1M: 4.0,
  },
  {
    modelId: "gpt-5.5",
    provider: "openai",
    modelClass: "high-performance",
    inputCostPer1M: 5.0,
    outputCostPer1M: 30.0,
  },
  {
    modelId: "gpt-5.4-mini",
    provider: "openai",
    modelClass: "lightweight",
    inputCostPer1M: 0.75,
    outputCostPer1M: 4.5,
  },
];

/** トークン数とモデル設定からコスト(USD)を計算します */
function calcCostUsd(
  inputTokens: number,
  outputTokens: number,
  config: ModelConfig
): number {
  return (
    (inputTokens / 1_000_000) * config.inputCostPer1M +
    (outputTokens / 1_000_000) * config.outputCostPer1M
  );
}

async function runBenchmark(): Promise<void> {
  console.log(`ベンチマーク開始: ${testCases.length}ケース × ${models.length}モデル`);

  const results: BenchmarkResult[] = [];

  // テストケースごとに全モデルを並列実行します
  for (const testCase of testCases) {
    console.log(`\n[${testCase.id}] 実行中...`);

    const caseResults = await Promise.all(
      models.map((config) => runSingleCase(testCase, config))
    );

    for (const result of caseResults) {
      const config = models.find((m) => m.modelId === result.modelId)!;
      const costUsd = calcCostUsd(result.inputTokens, result.outputTokens, config);

      console.log(
        `  ${result.modelId.padEnd(20)} ` +
          `score=${result.qualityScore.toFixed(2)} ` +
          `ttft=${result.ttftMs}ms ` +
          `total=${result.totalMs}ms ` +
          `cost=$${costUsd.toFixed(6)} ` +
          `${result.error ? `ERROR: ${result.error}` : ""}`
      );
    }

    results.push(...caseResults);
  }

  printSummary(results);
}

/** タスク種別 × モデルで集計した比較サマリを出力します */
function printSummary(results: BenchmarkResult[]): void {
  console.log("\n========== サマリ ==========");

  const taskTypes = [...new Set(results.map((r) => {
    const tc = testCases.find((t) => t.id === r.caseId);
    return tc?.taskType ?? "unknown";
  }))];

  for (const taskType of taskTypes) {
    console.log(`\n--- ${taskType} ---`);

    const taskResults = results.filter((r) => {
      const tc = testCases.find((t) => t.id === r.caseId);
      return tc?.taskType === taskType;
    });

    for (const model of models) {
      const modelResults = taskResults.filter((r) => r.modelId === model.modelId);
      if (modelResults.length === 0) continue;

      const avgScore =
        modelResults.reduce((s, r) => s + r.qualityScore, 0) / modelResults.length;
      const avgTtft =
        modelResults.reduce((s, r) => s + r.ttftMs, 0) / modelResults.length;
      const avgTotal =
        modelResults.reduce((s, r) => s + r.totalMs, 0) / modelResults.length;
      const totalCost = modelResults.reduce((s, r) => {
        return s + calcCostUsd(r.inputTokens, r.outputTokens, model);
      }, 0);

      console.log(
        `  ${model.modelId.padEnd(20)} ` +
          `avgScore=${avgScore.toFixed(2)} ` +
          `avgTtft=${Math.round(avgTtft)}ms ` +
          `avgTotal=${Math.round(avgTotal)}ms ` +
          `totalCost=$${totalCost.toFixed(6)}`
      );
    }
  }
}

runBenchmark().catch(console.error);

実行結果の例(実際の値はモデルのバージョンや環境によって異なります)です。

ベンチマーク開始: 5ケース × 4モデル

[extract-001] 実行中...
  claude-opus-4-8       score=1.00 ttft=320ms total=1240ms cost=$0.000240
  claude-haiku-4-5      score=1.00 ttft=180ms total=580ms  cost=$0.000016
  gpt-5.5               score=1.00 ttft=410ms total=1380ms cost=$0.000275
  gpt-5.4-mini          score=1.00 ttft=210ms total=620ms  cost=$0.000041

[classify-001] 実行中...
  claude-opus-4-8       score=1.00 ttft=290ms total=980ms  cost=$0.000225
  claude-haiku-4-5      score=1.00 ttft=160ms total=490ms  cost=$0.000014
  gpt-5.5               score=1.00 ttft=380ms total=1100ms cost=$0.000240
  gpt-5.4-mini          score=1.00 ttft=190ms total=540ms  cost=$0.000036

[reasoning-001] 実行中...
  claude-opus-4-8       score=0.85 ttft=450ms total=3200ms cost=$0.000890
  claude-haiku-4-5      score=0.52 ttft=210ms total=1100ms cost=$0.000042
  gpt-5.5               score=0.78 ttft=520ms total=3500ms cost=$0.001810
  gpt-5.4-mini          score=0.48 ttft=240ms total=1250ms cost=$0.000272

========== サマリ ==========

--- extraction ---
  claude-opus-4-8       avgScore=1.00 avgTtft=320ms avgTotal=1240ms totalCost=$0.000240
  claude-haiku-4-5      avgScore=1.00 avgTtft=180ms avgTotal=580ms  totalCost=$0.000016
  ...

--- reasoning ---
  claude-opus-4-8       avgScore=0.85 avgTtft=450ms avgTotal=3200ms totalCost=$0.000890
  claude-haiku-4-5      avgScore=0.52 avgTtft=210ms avgTotal=1100ms totalCost=$0.000042
  ...

この例では、extractionタスクは軽量クラスで品質が同等なのにコストが大幅に低い一方、reasoningタスクでは高性能クラスと軽量クラスでスコアに差が出ています。このような結果を積み上げることで、「どのタスクに何を使うか」の判断が数値で裏付けられます。


06LLM-as-a-judge で品質評価を深める

自動スコアリングの限界

前節の評価スクリプトはキーワード一致や簡易ルールで品質をスコアリングしています。 要約や推論など自由記述のタスクでは、この方法では限界があります。

LLM-as-a-judgeは、別のLLMに評価させるアプローチです。 高性能クラスのモデルを審判役として使い、候補モデルの出力を評価させます。

// benchmark/llm-judge.ts
import Anthropic from "@anthropic-ai/sdk";

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

export interface JudgeInput {
  instruction: string;
  input: string;
  output: string;
  /** 比較対象の参考出力。ない場合は絶対評価を行います */
  referenceOutput?: string;
}

export interface JudgeResult {
  score: number;
  reason: string;
}

/**
 * 高性能クラスのモデルを使って出力を評価します。
 * スコアは1〜5の整数で返させ、0.0〜1.0に正規化します。
 */
export async function judgeWithLlm(input: JudgeInput): Promise<JudgeResult> {
  const referenceSection = input.referenceOutput
    ? `\n## 参考出力\n${input.referenceOutput}`
    : "";

  const prompt = `あなたはAI出力の品質評価者です。以下の指示・入力・出力のセットを評価してください。

## 指示
${input.instruction}

## 入力
${input.input}

## 評価対象の出力
${input.output}
${referenceSection}

## 評価基準
- 5: 指示を完全に満たしており、内容が正確で有用
- 4: ほぼ満たしており、軽微な問題のみ
- 3: 部分的に満たしているが、重要な問題がある
- 2: 指示の理解は見えるが、品質が低い
- 1: 指示を満たしておらず、内容が不正確または有害

以下のJSON形式のみで回答してください(前後に説明文を含めないでください):
{"score": <1〜5の整数>, "reason": "<簡潔な理由>"}`;

  const response = await anthropic.messages.create({
    // 評価には高性能クラスを使います(評価精度を確保するため)
    model: "claude-opus-4-8",
    max_tokens: 256,
    messages: [{ role: "user", content: prompt }],
  });

  const text =
    response.content[0].type === "text" ? response.content[0].text : "";

  try {
    const parsed = JSON.parse(text.trim()) as { score: number; reason: string };
    return {
      score: (parsed.score - 1) / 4, // 1〜5を0.0〜1.0に変換
      reason: parsed.reason,
    };
  } catch {
    return { score: 0.5, reason: "評価レスポンスのパースに失敗しました" };
  }
}

この評価器を evaluateQuality の代替として reasoningsummarization のタスクに適用することで、より精度の高いスコアが得られます。ただし審判役モデルにもコストがかかるため、ベンチマーク用途に限定して使うのが現実的です。


07モデルルーティングを実装する

ルーティングとは何か

ベンチマークの結果を踏まえて「このタスクには軽量クラス、あのタスクには高性能クラス」という方針が決まったとしても、それをアドホックにコード各所に散らしてしまうと後で変更しにくくなります。

ルーティングロジックを一箇所に集めることで、次の利点が生まれます。

  • モデルの変更が一箇所の修正で済む
  • A/Bテストやフォールバックを後付けしやすい
  • コスト上限や速度要件をポリシーとして管理できる

基本的なルーターの実装

// routing/model-router.ts

export type TaskType =
  | "extraction"
  | "classification"
  | "summarization-brief"
  | "summarization-detailed"
  | "code-gen-simple"
  | "code-gen-complex"
  | "reasoning"
  | "translation-general"
  | "translation-specialized"
  | "chat-simple"
  | "chat-complex";

export interface RoutingPolicy {
  taskType: TaskType;
  modelId: string;
  provider: "anthropic" | "openai";
  maxTokens: number;
  /** コスト上限(USD)を超えた場合にフォールバックモデルへ切り替えます */
  costLimitUsd?: number;
  fallbackModelId?: string;
}

/**
 * ルーティングポリシーを一元管理します。
 * ベンチマーク結果を踏まえてここを更新します。
 */
export const routingPolicies: RoutingPolicy[] = [
  // 単純抽出・分類は軽量クラスで十分
  { taskType: "extraction",           modelId: "claude-haiku-4-5",  provider: "anthropic", maxTokens: 512  },
  { taskType: "classification",       modelId: "claude-haiku-4-5",  provider: "anthropic", maxTokens: 128  },
  { taskType: "summarization-brief",  modelId: "claude-haiku-4-5",  provider: "anthropic", maxTokens: 512  },
  { taskType: "translation-general",  modelId: "claude-haiku-4-5",  provider: "anthropic", maxTokens: 2048 },
  { taskType: "code-gen-simple",      modelId: "claude-haiku-4-5",  provider: "anthropic", maxTokens: 1024 },
  { taskType: "chat-simple",          modelId: "claude-haiku-4-5",  provider: "anthropic", maxTokens: 1024 },

  // 複雑なタスクは高性能クラスを使用
  { taskType: "summarization-detailed",   modelId: "claude-opus-4-8",  provider: "anthropic", maxTokens: 2048 },
  { taskType: "translation-specialized",  modelId: "claude-opus-4-8",  provider: "anthropic", maxTokens: 2048 },
  { taskType: "code-gen-complex",         modelId: "claude-opus-4-8",  provider: "anthropic", maxTokens: 4096 },
  { taskType: "reasoning",                modelId: "claude-opus-4-8",  provider: "anthropic", maxTokens: 4096 },
  { taskType: "chat-complex",             modelId: "claude-opus-4-8",  provider: "anthropic", maxTokens: 2048 },
];

export function resolvePolicy(taskType: TaskType): RoutingPolicy {
  const policy = routingPolicies.find((p) => p.taskType === taskType);
  if (!policy) {
    // 未定義のタスクは安全側に倒して高性能クラスを使います
    console.warn(`[ModelRouter] 未定義のタスクタイプ: ${taskType}。高性能クラスにフォールバックします。`);
    return {
      taskType,
      modelId: "claude-opus-4-8",
      provider: "anthropic",
      maxTokens: 2048,
    };
  }
  return policy;
}

ルーターを使う統一インターフェース

// routing/llm-client.ts
import Anthropic from "@anthropic-ai/sdk";
import OpenAI from "openai";
import { TaskType, resolvePolicy } from "./model-router.js";

const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

export interface LlmRequest {
  taskType: TaskType;
  systemPrompt?: string;
  userMessage: string;
}

export interface LlmResponse {
  text: string;
  modelId: string;
  inputTokens: number;
  outputTokens: number;
}

/**
 * タスク種別を受け取り、ルーティングポリシーに従って
 * 適切なモデルへリクエストを送信します。
 * 呼び出し元はモデルを意識しません。
 */
export async function chat(request: LlmRequest): Promise<LlmResponse> {
  const policy = resolvePolicy(request.taskType);

  if (policy.provider === "anthropic") {
    const response = await anthropic.messages.create({
      model: policy.modelId,
      max_tokens: policy.maxTokens,
      system: request.systemPrompt,
      messages: [{ role: "user", content: request.userMessage }],
    });

    const text =
      response.content[0].type === "text" ? response.content[0].text : "";

    return {
      text,
      modelId: policy.modelId,
      inputTokens: response.usage.input_tokens,
      outputTokens: response.usage.output_tokens,
    };
  } else {
    const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [];
    if (request.systemPrompt) {
      messages.push({ role: "system", content: request.systemPrompt });
    }
    messages.push({ role: "user", content: request.userMessage });

    const response = await openai.chat.completions.create({
      model: policy.modelId,
      max_completion_tokens: policy.maxTokens,
      messages,
    });

    return {
      text: response.choices[0].message.content ?? "",
      modelId: policy.modelId,
      inputTokens: response.usage?.prompt_tokens ?? 0,
      outputTokens: response.usage?.completion_tokens ?? 0,
    };
  }
}

呼び出し側のコードはシンプルになります。

// 呼び出し例
import { chat } from "./routing/llm-client.js";

// 分類タスク: 内部では軽量クラスにルーティングされます
const classifyResult = await chat({
  taskType: "classification",
  systemPrompt: "以下のメッセージを分類してください。",
  userMessage: "支払いが完了しているのに届いていません。",
});

console.log(classifyResult.text);
// => "請求・支払い"
console.log(`使用モデル: ${classifyResult.modelId}`);
// => 使用モデル: claude-haiku-4-5

// 複雑な推論タスク: 内部では高性能クラスにルーティングされます
const reasoningResult = await chat({
  taskType: "reasoning",
  systemPrompt: "以下のデータを分析し、推奨アクションを提示してください。",
  userMessage: "直近3ヶ月の売上が毎月10%減少。新規顧客数は横ばいだが、継続率が低下...",
});

console.log(`使用モデル: ${reasoningResult.modelId}`);
// => 使用モデル: claude-opus-4-8

動的ルーティングの考え方

入力の複雑さに応じてルーティングを動的に変える方法もあります。 ひとつのアプローチは、入力の特徴量(トークン数・専門用語の有無・質問の種類)を使って振り分けることです。

// routing/dynamic-router.ts

export interface DynamicRoutingContext {
  inputText: string;
  /** 一般的なガイドライン: 入力がこのトークン数を超えたら高性能クラスを検討 */
  longInputThreshold?: number;
  /** 専門ドメインキーワードが含まれる場合に高性能クラスを優先するキーワードリスト */
  specializedKeywords?: string[];
}

export type ModelTier = "lightweight" | "high-performance";

/**
 * 入力の特性から適切なモデル階層を提案します。
 * これはあくまで「ヒント」であり、最終判断はルーティングポリシーと組み合わせます。
 */
export function inferModelTier(context: DynamicRoutingContext): ModelTier {
  const { inputText, longInputThreshold = 1000, specializedKeywords = [] } = context;

  // 入力が長い場合は高性能クラスを推奨
  // 1トークン ≒ 4文字(日本語では2〜3文字)の粗い目安
  const approxTokens = inputText.length / 3;
  if (approxTokens > longInputThreshold) {
    return "high-performance";
  }

  // 専門キーワードが含まれる場合は高性能クラスを推奨
  if (specializedKeywords.some((kw) => inputText.includes(kw))) {
    return "high-performance";
  }

  return "lightweight";
}

動的ルーティングはコスト効率を高める一方で、「なぜそのモデルが使われたか」のトレーサビリティが必要です。ログにモデルIDとルーティング理由を含めることで、後からデバッグしやすくなります。(関連記事: ログ設計と分散トレーシング:AI処理の可観測性を高める)


08選定早見表

ベンチマークを実施する前の初期選定や、判断に迷ったときの参考として活用してください。 実際の数値はベンチマークで検証してください。

タスク種別別の推奨クラス

タスク 推奨クラス 理由
固定フォーマットの情報抽出 軽量 推論不要。速度とコストを優先
ラベル分類(選択肢固定) 軽量 選択肢が明確。Function Callingで安定
短文の感情分析 軽量 パターン化されたタスク
汎用翻訳(日英・英日) 軽量 一般的な翻訳精度は安定
箇条書き化・構造化 軽量 変換ルールが単純
長文の概要要約 軽量〜中間 精度要件による
専門文書の詳細要約 高性能 ニュアンスの把握が必要
専門・法律・医療翻訳 高性能 ドメイン固有の判断が入る
コード生成(定型パターン) 軽量〜中間 タスクの複雑さによる
コード生成(設計・リファクタリング) 高性能 多段階の推論が必要
バグ分析・デバッグ支援 高性能 複数要因の統合が必要
根拠付き分析・レポート生成 高性能 推論能力が品質に直結
数学・論理推論 高性能(推論特化型も検討) 逐次的な思考が必要

3軸の優先度別おすすめパターン

優先したい軸 推奨アプローチ
コスト最小化 タスクを細分化し、可能な限り軽量クラスを使う。高性能クラスは最小限に
速度最大化 軽量クラス優先。並列処理と組み合わせる
品質最大化 高性能クラスを使用。LLM-as-a-judgeで品質を継続測定
バランス重視 ベンチマークで品質差を測定し、許容範囲内で軽量クラスに切り替える

判断フローチャート

flowchart TD
    A["タスクを受け取る"] --> B{"推論の深さはどのレベルか?"}
    B -->|"単純(抽出・分類)"| C["軽量クラスを試す"]
    B -->|"中間(要約・翻訳)"| D["軽量クラスで試し、品質不足なら高性能へ"]
    B -->|"複雑(推論・分析)"| E["高性能クラスから始める"]
    C --> F{"エラーコストは高いか?"}
    D --> F
    E --> F
    F -->|"高い(業務判断の補助、対外的なコンテンツ)"| G["高性能クラス"]
    F -->|"低い(内部処理、補助的な情報整理)"| H["軽量クラス"]
    G --> I["ベンチマークで実測し、閾値を設定"]
    H --> I
    I --> J["ルーティングポリシーに反映"]

09BizPlanでのモデル選定の考え方

私たちが開発している事業計画エージェント「BizPlan」では、エージェント内の処理をタスク種別で分解し、それぞれに異なるモデルクラスを割り当てる設計を採用しています。

具体的にどのモデルをどのタスクに使っているかは内部設計に関わるため詳細には触れませんが、設計思想として参考になる点をいくつか共有します。

精度が事業判断に直結する部分は高性能クラスを優先します。 事業計画の核心にある財務シミュレーションや競合分析といった部分は、推論の深さが出力の価値に直接影響します。この部分でのコスト削減は、品質の劣化として現れやすいため、高性能クラスを維持しています。

定型的な前処理・整形は軽量クラスで処理します。 入力されたテキストからの構造化抽出、テンプレートへの値埋め込み、言語チェックといった処理は、軽量クラスで十分なことがわかっています。エージェントの処理全体でみると、このような定型処理が意外と多く、ここを軽量クラスに切り替えることでコスト全体を抑えています。

品質監視を継続する仕組みが欠かせません。 モデルのバージョンアップや入力データの変化によって、一度決めたルーティングが最適でなくなることがあります。定期的に品質指標を計測し、ルーティングポリシーを見直す運用サイクルが重要です。


10運用上の注意点

モデルバージョンとAPIの変更への備え

モデルの世代交代は速く、今日の「高性能クラス」が数ヶ月後には「中間クラス」になることもあります。また、APIの提供自体が終了するケースもあります。

対策として、ルーティングポリシーのモデルIDを設定ファイルや環境変数で管理し、コードの変更なしに差し替えられる構造を推奨します。

// config/models.ts
export const MODEL_IDS = {
  HIGH_PERFORMANCE: process.env.MODEL_HIGH_PERFORMANCE ?? "claude-opus-4-8",
  LIGHTWEIGHT: process.env.MODEL_LIGHTWEIGHT ?? "claude-haiku-4-5",
} as const;

フォールバック設計

APIのレート制限やモデルの一時的な障害に備えて、フォールバック先を設定します。 軽量クラスから高性能クラスへのアップグレードフォールバック(品質確保)と、高性能クラスから軽量クラスへのダウングレードフォールバック(可用性確保)の2方向を想定しておくと安心です。(関連記事: エラーハンドリングとリトライ設計:LLMのエラーパターンに対処する)

コスト上限との組み合わせ

高性能クラスへの切り替えが想定外のコスト増につながらないよう、タスクごとに想定トークン数の上限を設けておくと安心です。ルーティングポリシーに maxTokens を含めているのはこのためです。月次のコスト見積もり方法については(関連記事: トークンコスト管理:消費を見積もり、最適化する)を参照してください。


11まとめ

タスクを特性ごとに分類し、自分のユースケースで実測してルーティングポリシーに反映する。この一連のサイクルがモデル選定の実際です。

汎用ベンチマークスコアは出発点にはなりますが、最終的な判断は「自分のタスクでの実測」が基準になります。 本記事で紹介したベンチマークスクリプトとルーターは、そのための足がかりとして活用していただければと思います。

選定方針はモデルの世代交代とともに見直しが必要です。継続的に計測する仕組みを作っておくことが、長期的なコスト・品質のバランスを保つ鍵になります。


12参考文献

  • Anthropic API Documentation — Models Overview(2026年6月時点)
  • OpenAI Platform Documentation — Models(2026年6月時点)
  • Anthropic SDK for TypeScript — GitHub repository
  • OpenAI Node.js Library — GitHub repository
  • "Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena" — Zheng et al., 2023(LLM-as-a-judgeの原著論文)
Author
管理者
Agent Store

記事で紹介した技術を、実際の業務でお試しください。

業務に合うエージェントを条件で絞り込んで選べます。すべて無料で、今すぐ利用できます。

エージェント一覧を見る →

コメント

まだコメントはありません。最初のコメントを投稿してみましょう。

コメントを投稿

ゲストコメントは管理者の承認後に公開されます。 ログインするとすぐにコメントが公開されます。

当サイトではCookieを使用しています。詳しくはCookieポリシーをご覧ください。