記事量産でLLMモデルをどう使い分けるか — 用途別モデル選択の実運用設計
この記事について
複数のスクリプトが複数のLLMを呼ぶ構成になったとき、「どのスクリプトがどのモデルを使っているか」が散らばると、モデル世代の更新やコスト管理が困難になる。
このスタジオでは scripts/llm_registry.py を「全スクリプトのLLM単一真実源(single source of truth)」として整備し、モデル変更をこの1ファイルだけで済ませる設計にしている。この記事は、そこに至った設計判断と用途別モデル選択の実例を記録する。
設計の基本方針
purpose名が用途タグになる
llm_registry.py では「何のためのモデルか」を purpose(用途)名として定義する。スクリプト側は from llm_registry import get; model = get("validate") と書くだけでよい。モデル名の文字列がコードベースに散らばらない。
# どのスクリプトも同じインターフェースで呼ぶ
model = get("research") # → gemini-3.1-flash-lite
model = get("seo_task_gen") # → claude-haiku-4-5-20251001
model = get("matome_compose") # → Ollama優先、Gemini fallback
primary / fallback / ollama_primary の3段構え
"matome_compose": {
"primary": "gemini-3-flash-preview", # Ollama不達時のAPI fallback
"fallback": ["gemini-2.5-flash-lite", "gemini-2.0-flash"],
"ollama_primary": "gemma3:4b", # 起動していれば優先使用
},
ローカルLLM(Ollama)が起動していれば API を消費しない。落ちていれば Gemini にフォールバックする。コスト・プライバシー・レイテンシのトレードオフを用途ごとに調整できる。
preview表記を維持する
"latest" エイリアスは使わない方針にしている。理由は再現性。"latest" は裏でどのモデルを指しているか追跡できなくなる。preview モデルは gemini-3.1-pro-preview のように表記を保持し、GA 移行時に明示的に更新する(最終確認日も記録する)。
用途別モデル選択の実例
| purpose | 使用モデル(primary) | 選択理由 |
|---|---|---|
research | gemini-3.1-flash-lite | 大量リクエスト・低コスト優先。grounding 対応 |
validate | gemini-3.1-flash-lite | ソース検証・スコアリング。速度重視 |
image_prompt_expand | gemini-3.1-flash-lite | メモからプロンプト拡張。軽量で十分 |
image_gen_inline | gemini-3.1-flash-image-preview | 画像生成は専用モデル必須 |
scenario_punchline | gemini-3.1-pro-preview | 1ネタ抽出は reasoning が必要 |
seo_task_gen | claude-haiku-4-5-20251001 | 構造化出力の精度が重要。Gemini では不安定 |
matome_compose | gemma3:4b(Ollama優先) | API 不使用方針。ローカルで完結 |
trend_generate | gemma4:latest(Ollama優先) | 同上 |
Gemini と Claude の使い分け
Gemini(Flash系)はスループット優先の大量処理向け。research・validate・vet など、1セッションで何十回も呼ぶ用途には Gemini のほうがコストが低い。
Claude は構造化出力の精度が求められる用途に使う。seo_task_gen では Claude Haiku を primary にし、Sonnet をフォールバックにしている。理由はプロンプト通りの JSON スキーマを返す安定性。Gemini でも動くが、スキーマ違反の頻度が高く retry のコストが逆転することがあった。
Ollama(ローカルLLM)の位置づけ
matome_compose(おーぷん2ch まとめ自動生成)と trend_generate(トレンド記事生成)はローカルLLM優先にしている。
理由は2つ。
- API コスト: まとめ生成は1記事あたり100〜200リクエスト規模になる。API で捌くと費用対効果が成立しない
- プライバシー: まとめ素材には個人の書き込みが含まれる。ローカル完結が望ましい
Ollama のデメリットは出力品質の上限。gemma3:4b はGemini Flash 相当の精度で、最終判断には使えないが「編集済みリストを作る前処理」なら十分機能している。
フォールバックループの実装パターン
from llm_registry import chain
for model in chain("validate"):
try:
result = call_gemini(model, prompt)
break
except Exception:
continue
chain("validate") は [primary, *fallback] のリストを返す。primary が失敗したら次を試す。try/except のループに入れるだけで済む設計にしている。
スクリプト側が個別にモデル名を持たないため、world-wide な障害やモデル廃止のときに llm_registry.py を1か所直すだけで全スクリプトに反映される。
やってわかったこと
用途を名前で管理すると、コードの意図が読めるようになる。 get("scenario_punchline") は「1ネタ抽出の用途に割り当てたモデルを返せ」という意味が自明。"gemini-3.1-pro-preview" という文字列をハードコードするより、後で読んだときに判断の文脈が残る。
ローカルLLMとAPIの役割は「コスト段」ではなく「用途段」で決まる。 安いからローカルを使うのではなく、その用途にAPIを消費する必要がないからローカルを使う。区別が重要で、品質が足りない場合は用途を変えるかモデルを変えるかを判断する。
preview モデルの廃止タイムラインは必ず記録する。 gemini-3.1-flash-lite への移行記録(2026-05-15 確認)がなければ、廃止期限(2026-05-25)を見逃していた可能性がある。
環境・バージョン
- Ollama: 本稿確認時点で
gemma3:4b/gemma4:latestを使用(M1サーバーのローカル実行) - Gemini:
gemini-3.1-flash-lite(GA版 / 2026-05-15 確認) - Claude:
claude-haiku-4-5-20251001/claude-sonnet-4-6 scripts/llm_registry.py: 最終更新 2026-05-15
更新履歴
- 2026-07-23: 初版公開