1記事10回超のLLM呼び出し、stage別コスト最適化の実装

プログラミング

1 本のブログ記事を自動生成する過程で、どのくらい LLM を呼び出しているか。運用を始めてから意識し始めた。

  • 企画と構成案出し: 1 回
  • 本文セクションごとのドラフト: 5 回
  • 文構成の妥当性チェック: 1 回
  • Google AdSense ポリシー適合確認: 1 回
  • SEO 観点の最適化提案: 1 回
  • 改善指摘への対応・全体リライト: 1〜3 回
  • 最終的な原稿チェック: 1 回

合計で 10〜13 回のモデル呼び出し が必要になる。月間 100 記事を生成すると、単純計算で 1,000 回を超える。

最初の試行では、すべての段階を Claude Opus で処理していた。高品質な出力を最優先にした選択だった。ところが 1 ヶ月の利用料が 数万円の単位 に跳ね上がり、個人運用の財務計画が一気に崩れた。

その後、全工程を Gemini 2.5 Flash に統一してみた。結果として月額料金は激減し、1,000 円程度 の実感を得た。しかし代償は大きかった。ドラフト品質の低下が目に見えて明らかになり、技術記事では変数や関数名が一貫しなくなり、事実関係の誤りも頻繁に見られるようになった。

最適解は両者の中間にあるはずだ。パイプラインの段階と内容に応じて、モデルを使い分けるという戦略である。content-factory では ModelSelector という選択エンジンと段階別マッピングでこれを実現した。本記事は、その設計思想と実装コードの記録である。


対象読者

LLM を活用したコンテンツ自動生成パイプラインのコスト最適化に課題を感じるエンジニアや開発チーム向け に、複数モデルの戦略的使い分けと、その実装方法を解説します。


基本方針

複数のAIコンピューティングリソース

設計の核となる考え方は以下の通りだ。

  • 高い推論力や文体理解が不可欠な作業 には力のあるモデル(Codex / Claude Sonnet)を充てる
  • 量的処理や定型的な判定 には軽量で高速、経済的なモデル(Gemini 2.5 Flash)を活用する
  • 予算上限が迫ったとき は、自動的に別のモデルへ切り替えるセーフティネットを設ける

呼び出しを分類する 2 つの軸

LLM の呼び出しをコード上で追跡し、制御するために、content-factory では 2 層の分類体系 を導入している。

  • stageType: パイプラインの進行段階(ideation_outlinedraftingreviewrewritefinal_review など)
  • taskType: 具体的な実行タスク(draft_generationquality_checkseo_analysiskeyword_suggestionfinal_polish など)

この 2 軸設計の利点は、同じ段階に属しても、タスクの難易度や特性が大きく異なる という現実に対応できる点にある。例えば review 段階を見ると、記事全体の構成の論理性を評価する quality_check は高度な推論が必要だが、SEO メタデータの検証である seo_analysis はパターンマッチングの範囲で処理できる。全タスクを同じモデルに委ねるのは、コスト効率と出力品質の両立を妨げる。


デフォルトマッピングテーブル

複数のタスクを自動的に異なるモデルに振り分けるワークフローシステムの設計図

タスク粒度でのモデル選択:

export const DEFAULT_TASK_MODEL_MAPPING: TaskModelMapping = {
  research_summary:          GEMINI_FLASH_MODEL_ID,
  draft_generation:          CODEX_MODEL_ID,          // 本文生成は高品質が決定的
  quality_check:             CODEX_MODEL_ID,          // 構成の論理性を厳密に判定
  adsense_check:             CODEX_MODEL_ID,
  seo_analysis:              GEMINI_FLASH_MODEL_ID,   // キーワード密度などの定量計算
  keyword_suggestion:        GEMINI_FLASH_MODEL_ID,
  final_polish:              CODEX_MODEL_ID,
  long_context_analysis:     GEMINI_FLASH_MODEL_ID,   // Gemini の 1M context 活用
  fact_check:                GEMINI_FLASH_MODEL_ID,   // ※低コスト優先だが、要件により Codex へ上書き推奨
  template_generation:       GEMINI_FLASH_MODEL_ID,
  image_keyword_generation:  GEMINI_FLASH_MODEL_ID,
  category_classification:   GEMINI_FLASH_MODEL_ID,
  final_review:              CLAUDE_SONNET_MODEL_ID,  // 最終段階は人間的な文章感覚が必須
};

段階粒度でのモデル選択:

export const DEFAULT_STAGE_MODEL_MAPPING: StageModelMapping = {
  ideation_outline:  GEMINI_FLASH_MODEL_ID,
  drafting:          CODEX_MODEL_ID,
  review:            CODEX_MODEL_ID,
  rewrite:           CODEX_MODEL_ID,
  final_review:      CLAUDE_SONNET_MODEL_ID,
};

この 2 つのマッピング構造が、全体コスト戦略の土台となる。ユーザーは後述の DB 設定画面から、これらをタスクやステージ単位で上書きできるようになっている。

モデル名について:記事の公開日時点では具体的な ID(例:codex/gpt-5.4gemini/gemini-2.5-flash など)を指していますが、ご利用の際は 運用環境で最新のモデル ID に読み替えてください。API 側の更新に伴い、実際に使用可能なモデル名が変わる可能性があります。


モデル選択のロジック

ModelSelector.selectModel() メソッドは、与えられた taskTypestageType から、優先度ベースで最適なモデル ID を返す。

async selectModel(taskType?: LLMTaskType, stageType?: LLMStageType): Promise<string> {
  // DB からユーザー設定を取得
  const customMapping      = await this.getCustomTaskModelMapping();
  const customStageMapping = await this.getCustomStageModelMapping();
  
  // taskType が与えられない場合は、stageType から推論
  const resolvedStageType  = stageType ?? (taskType ? getStageForTask(taskType) : undefined);

  // 各レイヤーの候補を解決
  const defaultTaskModel  = taskType ? DEFAULT_TASK_MODEL_MAPPING[taskType] : undefined;
  const defaultStageModel = resolvedStageType ? DEFAULT_STAGE_MODEL_MAPPING[resolvedStageType] : undefined;
  const stageModel = resolvedStageType ? customStageMapping?.[resolvedStageType] : undefined;
  const taskModel  = taskType ? customMapping?.[taskType] : undefined;

  // 優先度(高い順): ユーザー指定タスク → ユーザー指定ステージ → デフォルトタスク → デフォルトステージ → フォールバック
  const modelToUse = taskModel ?? stageModel ?? defaultTaskModel ?? defaultStageModel ?? CODEX_MODEL_ID;
  const selectedModel = normalizeModelId(modelToUse);

  // 予算消費状況を確認し、上限超過なら自動的に別モデルへ
  const shouldFallback = await this.checkCostLimit(this.getModelConfig(selectedModel).provider);
  if (shouldFallback) {
    return this.getFallbackModel(taskType, resolvedStageType);
  }

  return selectedModel;
}

選択ロジックの重要な 3 つのポイント:

  • ユーザーによる手動指定が最優先。「今月は quality_check だけ Claude Opus に指定したい」といった柔軟な運用を可能にする
  • ユーザー設定がない場合、デフォルトマッピング表に従う
  • さらに、月次予算の消費状況をリアルタイムチェックし、超過予測時は自動フォールバック

コスト上限に基づくフォールバック機構

月次コスト上限に達すると安いモデルへ自動で切り替わるフォールバックシステム

月間の利用額が指定した予算上限に近づくと、自動的にコスト効率の良いモデルへ切り替わる仕組みである。

const DEFAULT_COST_LIMITS: CostLimitConfig = {
  codexMonthlyLimit:  3000,  // Codex 関連(Codex CLI など)の月額上限(円)
  geminiMonthlyLimit: 1000,  // Gemini API の有料枠の月額上限(円)
};

const COST_WARNING_THRESHOLD  = 0.8;  // 80% 消費時に警告
const COST_FALLBACK_THRESHOLD = 0.9;  // 90% 消費時に自動フォールバック

数値はいずれも、UI 設定画面でユーザーが自由に変更できる。Codex CLI や Claude Code CLI のサブスクリプション料と、Gemini API の有料枠は、厳密には使用量に応じた従量課金ではなく、月額制の予算枠として扱う設計になっている。これは「このプロバイダには月間予算をいくら確保するか」という運用判断の表現方式だ。

checkCostLimit(provider) は、api_usage テーブルから当月の累計利用額を算出し、閾値超過の有無を判定する。

フォールバック処理のチェーン:

async getFallbackModel(taskType, stageType): Promise<string> {
  // タスク指定がある場合はそのフォールバック先を、なければステージのデフォルトを採用
  const fallbackSource =
    (taskType ? DEFAULT_FALLBACK_MAPPING[taskType] : undefined) ??
    (stageType ? DEFAULT_STAGE_MODEL_MAPPING[stageType] : undefined) ??
    CODEX_MODEL_ID;
  const fallbackModel = normalizeModelId(fallbackSource);

  // フォールバック先のモデルも予算枠に達していないか確認
  // 簡略化のため一部省略していますが、実装では以下のように取得します:
  // const fallbackConfig = this.getModelConfig(fallbackModel);
  const shouldFallbackAgain = await this.checkCostLimit(this.getModelConfig(fallbackModel).provider);
  
  // フォールバック先もダメなら、最終手段としてもう一方のプロバイダへ
  if (shouldFallbackAgain) {
    return this.getModelConfig(fallbackModel).provider === "codex" 
      ? "gemini/gemini-2.5-flash" 
      : CODEX_MODEL_ID;
  }
  return fallbackModel;
}
export const DEFAULT_FALLBACK_MAPPING: TaskModelMapping = {
  research_summary:          CODEX_MODEL_ID,          // Gemini → Codex へシフト
  draft_generation:          GEMINI_FLASH_MODEL_ID,   // Codex → Gemini へシフト
  quality_check:             GEMINI_FLASH_MODEL_ID,
  adsense_check:             GEMINI_FLASH_MODEL_ID,
  seo_analysis:              CODEX_MODEL_ID,
  keyword_suggestion:        CODEX_MODEL_ID,
  final_polish:              GEMINI_FLASH_MODEL_ID,
  long_context_analysis:     CODEX_MODEL_ID,
  fact_check:                CODEX_MODEL_ID,          // 精度重視時は Codex へシフト推奨
  template_generation:       CODEX_MODEL_ID,
  // ...
};

この実装の面白さは、プロバイダ間での双方向フォールバック だ。

  • Codex の枠が埋まった → Gemini へ逃がす
  • Gemini の枠が埋まった → Codex へ逃がす
  • 両方の枠が埋まった → 最終フォールバック(おそらく最軽量モデル)

Codex サブスクリプションと Gemini API 有料枠は独立した予算源なので、一方の枯渇がもう一方に直結しない。いずれかのプロバイダに余裕があれば、処理は途切れずに続行できる。


実運用で見えてきた効果

複数のAIモデル運用における、コスト効率と品質パフォーマンスの比較分析データ

3 ヶ月の運用を通して得た、定性的・定量的な観察をまとめた。正確な会計記録ではなく、運用の感触に基づく参考値 として読んでいただきたい。

比較軸 全段階 Opus 運用 全段階 Gemini Flash 運用 Stage-based 混合運用
月額コスト(100 記事ベース) 数万円規模 1,000 円未満 数千円規模
ドラフト品質(体感値) ◎ 優秀 △ 不十分 ○〜◎ 実用的
Review 段階での合格率 90%+ 50〜60% 75〜85%

※ 出力費用は、プロバイダ側のモデル単価と、1 本の記事あたりの消費トークン数に左右される。参考値として、draft_generation 1 回あたり 平均 1.5〜2.0 万トークン消費 され、Codex での換算料金は 1 回あたり数百円程度 という実績から逆算した。詳細は運用環境によって大きく異なるため、実際の運用時には各自で計測・記録することを強く推奨する。

全工程を Opus で賄う場合と比べると、月額コストは 1 桁レベルで削減 される。その一方で、全工程を Flash に統一したときに現れた品質低下(本文の一貫性欠如、固有名詞のハルシネーション)は、大部分が解消した。

肝要なのは、全体をコスト最小化の方向に倒さないこと だ。ドラフト本文の生成と最終レビューの段階では、品質低下の許容度が低い。反対に、「キーワード候補を 5 個リスト化する」といった単純で定型的なタスクなら、Flash の速度と経済性で十分な役割を果たす。


UI 経由での設定上書き

運用画面から、ユーザーが task ごと、stage ごとにモデル指定を細かく調整できる機能がある。

┌─ LLM 環境設定
├─ タスク別モデル割り当て
│  ├─ draft_generation:   codex/gpt-5.4     [▼ 変更]
│  ├─ quality_check:      codex/gpt-5.4     [▼ 変更]
│  ├─ seo_analysis:       gemini/flash-2.5  [▼ 変更]
│  ├─ fact_check:         gemini/flash-2.5  [▼ 変更] ※高精度要件時は Codex へ上書きを推奨
│  ├─ final_review:       claude-sonnet-4-5 [▼ 変更]
│  └─ ...
│
└─ ステージ別モデル割り当て
   ├─ drafting:          codex/gpt-5.4      [▼ 変更]
   ├─ review:            codex/gpt-5.4      [▼ 変更]
   ├─ final_review:      claude-sonnet-4-5  [▼ 変更]
   └─ ...

各項目のドロップダウンメニューから、その時点で利用可能なモデル一覧を選択できる。月末の予算消費状況を見ながら「今月は final_review も Codex に集中させよう」といった時間軸での最適化も容易だ。


実装全文と関連資料

本記事で紹介したコードの 完全版は GitHub で公開中 です:

  • リポジトリ: https://github.com/[organization]/content-factory
  • ModelSelector の実装: packages/server/src/services/ModelSelector.ts
  • ヘルパー関数群 など細かな実装詳細も、同リポジトリを参照してください

関連記事:

  • 「LLM パイプライン設計の全体像」 — モデル選択以外の段階別処理設計:[URL]
  • 「Codex / Gemini API 料金体系の読み方」 — 月額予算の立て方:[URL]

学んだこと

複数軸による戦略最適化マトリックス

  1. 「すべてを 1 つのモデルで統一する」は、コスト・品質ともに局所最適解に過ぎない。パイプラインの各段階が求める能力が異なる以上、モデルも使い分けるべきだ

  2. デフォルト戦略、ユーザーカスタマイズ、コスト上限フォールバックの 3 層構造 が、実装複雑度と運用柔軟性のバランスを最も取りやすい

  3. フォールバック経路は非対称性を認める。Codex が枯渇したら Gemini、その逆も然り。プロバイダ依存を減らす設計がシステムの堅牢性を高める

  4. task 粒度のマッピングが、stage 粒度より先に決まるべき。タスクは個別の特性が明確なので、設定値も決めやすい。stage はその集約結果として見える化される

実装コード量としては、300 行弱の ModelSelector クラス で、全体のコスト構造を大きく改善できた。正確なコスト削減率の計測を欠いているものの、Opus 連打の頃の「高くつきすぎて試し書きもためらわれる」状態から「気軽に検証できる」予算レベルまで下がったのは確実だ。ROI 観点では、個人規模のプロジェクトとしては十分高い水準と考えている。

似た構造を自分のパイプラインで組むなら、本記事のデフォルトマッピング表を起点 にしてほしい。最初の 1 ヶ月は表通りに運用しながら、実際のコスト推移と品質を記録する。その記録に基づいて、自分のユースケースに合わせた調整を段階的に加えていく、という進め方が最も現実的だ。「まず task レベルのマッピングを詰めて、stage はその集約結果として設計する」という順序を意識すると、考えやすくなるだろう。


X(旧 Twitter)でのフォロー、および ニュースレター(内容・設計・運用事例の定期配信)への登録 も大歓迎です。最新の知見やトラブルシューティング、実装パターンを随時共有しています。

コメント