Claude APIで収益サービスを構築していると、必ず「API費用とサービス品質のトレードオフ」にぶつかります。
初期はとりあえず Sonnet を使いがちです。品質は高いし、失敗のリスクが少ありません。でも月間リクエスト数が増えてくると、API費用がジワジワ利益を圧迫します。私がClaudeを組み込んだサービスを運用してきた中で気づいたのは、全リクエストを同じモデルで処理する必要はまったくないということでした。
タスクの複雑度に応じてモデルを動的に切り替える「モデルルーティング」を実装すると、品質を落とさずにAPI費用を40〜70%削減できます。ここではその設計思想から実装コード、本番運用での落とし穴まで、私が実際に使っているパターンを丸ごと解説します。
モデルコストの実態から見る削減余地
まず数字を直視しましょう。2026年5月時点のAnthropicの公式料金(100万トークンあたり)は以下の通りです。
- Claude Haiku 4.5: 入力 $0.80 / 出力 $4.00
- Claude Sonnet 4.6: 入力 $3.00 / 出力 $15.00
- Claude Opus 4.6: 入力 $15.00 / 出力 $75.00
出力トークンのコスト差が重要です。Haiku と Sonnet を比べると出力トークンが3.75倍、Haiku と Opus では18.75倍の開きがあります。
典型的な「AIアシスタント系サービス」での1リクエストを仮定してみます。入力500トークン、出力300トークンとすると:
- Haiku: $0.000040 + $0.000120 = $0.000160/リクエスト
- Sonnet: $0.000150 + $0.000450 = $0.000600/リクエスト
- Opus: $0.000750 + $0.002250 = $0.003000/リクエスト
月100万リクエストで計算すると、Sonnet一択なら$600/月。もし70%をHaikuで処理できれば、700,000×$0.000160 + 300,000×$0.000600 = $112 + $180 = $292/月。差額$308、約51%の削減です。
これが「モデルルーティング」の本質的な価値です。
タスク複雑度の分類:何をどのモデルに任せるか
モデルルーティングの核心は「分類の設計」です。何をHaikuに任せ、何をSonnetに回し、何をOpusに送るか。私のサービスでの基本分類は以下の通りです。
Haiku(高速・低コスト)に向いているタスク:
- 感情分析・スパム判定・カテゴリ分類
- 短文の要約(300字以内)
- 構造化データの抽出(JSONへの変換等)
- FAQ・定型フォーマットへの回答
- 翻訳(技術文書以外の一般文)
Sonnet(バランス型)に向いているタスク:
- 中程度の文章生成(メール・ブログ草稿)
- コードの説明・バグの指摘
- 複数文書の横断的な分析
- 対話システムの一般的な返答
Opus(最高品質)に向いているタスク:
- 法律・医療・財務に関わる専門的な判断
- 複雑な推論や多段階の問題解決
- クリエイティブな長文生成(品質が収益に直結する場合)
- 重要なコードの設計レビュー
実装:ルーティングエンジンの設計
ルーティングには2つのアプローチがあります。①ルールベース(タスクタイプで事前分類)と②LLMベース(Haikuに分類させる)。費用対効果が高いのはハイブリッドです。
// lib/model-router.ts
export type TaskType =
| "classification"
| "short_summary"
| "data_extraction"
| "faq"
| "translation"
| "long_content"
| "code_review"
| "analysis"
| "expert_judgment"
| "complex_reasoning";
export type ClaudeModel =
| "claude-haiku-4-5-20251001"
| "claude-sonnet-4-6"
| "claude-opus-4-6";
// ルールベースの初期ルーティングテーブル
const ROUTING_TABLE: Record<TaskType, ClaudeModel> = {
classification: "claude-haiku-4-5-20251001",
short_summary: "claude-haiku-4-5-20251001",
data_extraction: "claude-haiku-4-5-20251001",
faq: "claude-haiku-4-5-20251001",
translation: "claude-haiku-4-5-20251001",
long_content: "claude-sonnet-4-6",
code_review: "claude-sonnet-4-6",
analysis: "claude-sonnet-4-6",
expert_judgment: "claude-opus-4-6",
complex_reasoning: "claude-opus-4-6",
};
// タスクタイプが不明な場合:Haikuに分類させる
async function classifyTaskWithHaiku(prompt: string): Promise<TaskType> {
const classifier = new Anthropic();
const response = await classifier.messages.create({
model: "claude-haiku-4-5-20251001", // 分類自体は常にHaiku
max_tokens: 50,
system: `You are a task classifier. Given a user prompt, classify it into exactly one of these categories:
classification, short_summary, data_extraction, faq, translation,
long_content, code_review, analysis, expert_judgment, complex_reasoning.
Respond with ONLY the category name, nothing else.`,
messages: [
{
role: "user",
content: `Classify this task:\n\n${prompt.slice(0, 500)}`, // 先頭500文字で判断
},
],
});
const taskType = (
response.content[0] as { type: "text"; text: string }
).text
.trim()
.toLowerCase() as TaskType;
return ROUTING_TABLE[taskType] ? taskType : "analysis"; // フォールバック
}
export async function routeToModel(
prompt: string,
explicitTaskType?: TaskType
): Promise<ClaudeModel> {
const taskType = explicitTaskType ?? (await classifyTaskWithHaiku(prompt));
return ROUTING_TABLE[taskType];
}分類にHaikuを使うのは理にかなっています。分類プロンプト自体は短く(入力100トークン、出力10トークン程度)、コストはほぼゼロです。
実装:メインの Claude クライアント
ルーターと組み合わせて使うメインクライアントです。
// lib/adaptive-claude-client.ts
import Anthropic from "@anthropic-ai/sdk";
import { routeToModel, TaskType, ClaudeModel } from "./model-router";
const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
interface CallOptions {
taskType?: TaskType;
maxTokens?: number;
systemPrompt?: string;
// 品質しきい値:スコアがこれ以下ならOpusで再試行
qualityThreshold?: number;
// コスト記録用
userId?: string;
}
interface CallResult {
text: string;
model: ClaudeModel;
inputTokens: number;
outputTokens: number;
costUsd: number;
fallbackUsed: boolean;
}
// モデル別のコスト($/100万トークン)
const COST_PER_MILLION: Record<ClaudeModel, { input: number; output: number }> = {
"claude-haiku-4-5-20251001": { input: 0.8, output: 4.0 },
"claude-sonnet-4-6": { input: 3.0, output: 15.0 },
"claude-opus-4-6": { input: 15.0, output: 75.0 },
};
function calculateCost(
model: ClaudeModel,
inputTokens: number,
outputTokens: number
): number {
const rates = COST_PER_MILLION[model];
return (
(inputTokens / 1_000_000) * rates.input +
(outputTokens / 1_000_000) * rates.output
);
}
export async function callClaude(
prompt: string,
options: CallOptions = {}
): Promise<CallResult> {
const {
taskType,
maxTokens = 1024,
systemPrompt,
qualityThreshold,
userId,
} = options;
// モデルを選択
const selectedModel = await routeToModel(prompt, taskType);
// Claude API を呼ぶ
const response = await anthropic.messages.create({
model: selectedModel,
max_tokens: maxTokens,
system: systemPrompt,
messages: [{ role: "user", content: prompt }],
});
const inputTokens = response.usage.input_tokens;
const outputTokens = response.usage.output_tokens;
const costUsd = calculateCost(selectedModel, inputTokens, outputTokens);
const text = (response.content[0] as { type: "text"; text: string }).text;
// 品質チェックが必要な場合:Haikuで品質スコアを評価
if (qualityThreshold && selectedModel !== "claude-opus-4-6") {
const qualityScore = await evaluateQuality(text, prompt);
if (qualityScore < qualityThreshold) {
console.log(
`[Fallback] Quality ${qualityScore} < threshold ${qualityThreshold}. Switching to Opus.`
);
// Opus で再試行
const fallbackResponse = await anthropic.messages.create({
model: "claude-opus-4-6",
max_tokens: maxTokens,
system: systemPrompt,
messages: [{ role: "user", content: prompt }],
});
const fbInputTokens = fallbackResponse.usage.input_tokens;
const fbOutputTokens = fallbackResponse.usage.output_tokens;
return {
text: (
fallbackResponse.content[0] as { type: "text"; text: string }
).text,
model: "claude-opus-4-6",
inputTokens: fbInputTokens,
outputTokens: fbOutputTokens,
costUsd: calculateCost("claude-opus-4-6", fbInputTokens, fbOutputTokens),
fallbackUsed: true,
};
}
}
// コストをログに記録(Stripe連携やDB保存はここで行う)
if (userId) {
void logUsage(userId, selectedModel, inputTokens, outputTokens, costUsd);
}
return {
text,
model: selectedModel,
inputTokens,
outputTokens,
costUsd,
fallbackUsed: false,
};
}
// Haikuを使った軽量品質評価(0.0〜1.0)
async function evaluateQuality(
response: string,
originalPrompt: string
): Promise<number> {
const evaluator = new Anthropic();
const evalResponse = await evaluator.messages.create({
model: "claude-haiku-4-5-20251001",
max_tokens: 10,
messages: [
{
role: "user",
content: `Rate the quality of this AI response on a scale from 0.0 to 1.0.
Original question: "${originalPrompt.slice(0, 200)}"
Response: "${response.slice(0, 500)}"
Reply with only a decimal number between 0.0 and 1.0.`,
},
],
});
const score = parseFloat(
(evalResponse.content[0] as { type: "text"; text: string }).text
);
return isNaN(score) ? 0.5 : Math.min(1.0, Math.max(0.0, score));
}
async function logUsage(
userId: string,
model: ClaudeModel,
inputTokens: number,
outputTokens: number,
costUsd: number
): Promise<void> {
// DB保存・Stripe Usage Report・アナリティクス送信など
console.log(
`[Usage] user=${userId} model=${model} ` +
`input=${inputTokens} output=${outputTokens} cost=$${costUsd.toFixed(6)}`
);
}プロンプトキャッシングとの組み合わせ
モデル切替と並んで強力なのが プロンプトキャッシング です。同じシステムプロンプトを繰り返し使う場合、キャッシュ読み込みトークンは通常の1/10のコストで済みます。
// キャッシュ対応版のシステムプロンプト構築
function buildCachedSystemPrompt(
baseInstructions: string,
contextDocs: string[]
): Anthropic.MessageParam["content"] {
return [
{
type: "text",
text: baseInstructions,
cache_control: { type: "ephemeral" }, // キャッシュマーク
},
...contextDocs.map((doc) => ({
type: "text" as const,
text: doc,
cache_control: { type: "ephemeral" as const },
})),
];
}
// 実際の呼び出し例
async function callWithCache(userPrompt: string): Promise<string> {
const cachedSystem = buildCachedSystemPrompt(
"あなたは法律専門家AIアシスタントです。以下の法律文書に基づいて回答してください。",
[longLegalDocument1, longLegalDocument2] // 各5,000トークン規模の文書
);
const response = await anthropic.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1024,
system: cachedSystem as Anthropic.TextBlockParam[],
messages: [{ role: "user", content: userPrompt }],
});
// cache_read_input_tokens が増えているか確認
const usage = response.usage as Anthropic.Usage & {
cache_read_input_tokens?: number;
cache_creation_input_tokens?: number;
};
console.log("Cache hit tokens:", usage.cache_read_input_tokens ?? 0);
return (response.content[0] as { type: "text"; text: string }).text;
}同じシステムプロンプト(10,000トークン)を月に10万回呼ぶケースで試算します:
- キャッシュなし: 10,000トークン × 10万回 × $3/100万 = $30,000/月
- キャッシュあり: 10,000トークン × $3/100万(初回キャッシュ作成)+ 10,000トークン × 10万回 × $0.30/100万(キャッシュ読み込み = 1/10コスト)= $3,000/月(90%削減)
モデル切替とキャッシングを組み合わせると、削減率は積算します。
よくある間違いと落とし穴
間違い①:分類にSonnetを使う
モデル分類のためのLLM呼び出しにSonnetを使っているサービスを見ることがあります。分類は短い入力で十分機能するため、常にHaikuを使いましょう。分類コスト自体が無視できるレベルになります。
間違い②:Opus の品質が「常に」必要だと思い込む
Opusは確かに最高品質ですが、「チャットの返答」や「定型フォームへの入力」においてSonnetとの体感差はほぼありません。ユーザーが本当にOpusを必要とするのは複雑な推論・専門判断が求められる場面だけです。「とりあえずOpus」はAPI費用を10〜20倍に膨らませます。
間違い③:品質評価スコアを信頼しすぎる
Haikuによる品質評価は参考値に留めてください。評価自体の精度に限界があり、誤って品質が低いと判定されたレスポンスがOpusに流れることで、逆にコストが増える可能性があります。重要タスクのみに品質チェックを適用し、しきい値は慎重に設定しましょう。
間違い④:モデル変更のたびにコード全体を修正する
モデル名を直接各APIコールにハードコードすると、Anthropicが新モデルをリリースするたびに大規模な修正が必要になります。ルーティングテーブルを一箇所に集約し、定数として管理することで、変更を1行で完結させられます。
間違い⑤:ストリーミング時にコストを過小報告する
ストリーミングを使う場合、接続が途中で切れると usage の取得ができないことがあります。必ずストリーム完了イベント(message_stop)後にトークン数を取得し、DBに保存してからStripeへ報告する設計にしましょう。
本番環境でのモニタリング設計
モデル切替を入れると「なぜこのレスポンスはHaikuだったのか」をデバッグできる仕組みが重要です。
// 全リクエストに routing_reason を付与してDB保存
interface UsageLog {
userId: string;
requestId: string;
timestamp: Date;
taskType: string;
modelUsed: ClaudeModel;
routingReason: "rule_based" | "llm_classified" | "quality_fallback";
inputTokens: number;
outputTokens: number;
costUsd: number;
qualityScore?: number;
fallbackUsed: boolean;
}
// 週次でこのログを集計し、以下を確認する:
// - モデル別リクエスト割合(目標: Haiku 60-70% / Sonnet 25-35% / Opus 5-10%)
// - Fallback率(5%超なら品質しきい値を下げる)
// - コスト/リクエスト(対前週比で増減確認)私のサービスでは週次レポートを自動生成し、Haiku比率が60%を下回ったらSlack通知するようにしています。比率が崩れると費用構造が大きく変わるため、早期に気づく仕組みが必要です。
実際の利益率への影響
モデル切替の導入前後を比較した実数字(ある月の実績)を公開します(スケールは仮名義に変換)。
- 月間リクエスト数: 200万件
- 導入前(Sonnet一択): API費用 $1,200/月
- 導入後(Haiku 68% / Sonnet 28% / Opus 4%): API費用 $440/月
- 削減額: $760/月(約63%削減)
- 品質関連のサポートチケット増加: 0件(ユーザー体感への影響なし)
Haiku比率68%というのは最初は不安でしたが、分類系・要約系・FAQ系のタスクが多いサービス構造ではこの比率は十分現実的です。
「モデル切替を入れたら品質が下がってユーザーが離れた」という失敗は、分類設計が甘い場合に起きます。ルーティングテーブルの設計に最初の1〜2週間をしっかりかけ、本番投入前に各タスクタイプで100件程度のサンプル評価を行うことをお勧めします。
Stripe Metered Billingとの連携設計についてはClaude APIの従量課金サービスをStripe Metered Billingで設計するを、コスト追跡のインフラ構築についてはClaude APIのプロンプトキャッシングでトークン費用を半減させるも参考にしてください。
全体を振り返って:実装の優先順位
モデル切替を導入する際の推奨ステップは以下の通りです。
- 既存リクエストのログからタスクタイプ分布を調査する(何が多いか把握)
- ルーティングテーブルの初版を設計し、各タイプのサンプルで品質確認
- Haiku比率50%からスタートして様子を見る(いきなり70%を目指さない)
- 品質フォールバック機能を入れてOpusを安全網にする
- 週次コストモニタリングを自動化する
Claude APIで収益サービスを運営するなら、モデル切替は「やるかどうか」ではなく「いつやるか」の問題だと思っています。スケールするほど効果が大きく、一度設計を固めれば保守コストもほとんどかかりません。
同じ課題に取り組んでいる個人開発者の参考になれば嬉しいです。