AIアプリを作り始めた頃は、1つのモデルだけ使えれば十分でした。
たとえばOpenAIだけを使うなら、API Keyを発行してSDKを入れ、数行書けばすぐに動きます。
ただ、実際に開発を進めていくと、だんだん状況が変わってきます。
• コード生成はClaudeも試したい
• 汎用的なチャット用途ではGPT系も使いたい
• 長文要約やマルチモーダルではGeminiも気になる
• 中国語タスクではDeepSeekやQwenも比較したい
こうなると問題は「モデルを1つ呼べるかどうか」ではなく、
複数モデルをどうやって綺麗に扱うかに変わってきます。
しかも、モデルごとに別々のAPIやSDKを直接扱い始めると、すぐに次のような課題が出てきます。
• 認証情報の管理が分散する
• SDKやレスポンス形式の差分吸収が必要になる
• モデル切り替えのたびにコード修正が増える
• A/Bテストやフォールバック実装が面倒になる
• コストや利用量の把握が難しくなる
そこで今回は、OpenAI互換APIを使って、マルチLLM呼び出し層を1つにまとめる方法を紹介します。
例として使うのはCrazyrouterです。
理由はシンプルで、OpenAI互換のインターフェースで複数モデルを扱いやすいからです。
Qiita向けなので宣伝ではなく、あくまで**「統一呼び出し層を作る実装例」**として見てもらえれば十分です。
───
なぜモデル呼び出しを抽象化したほうがいいのか
まず、ありがちな最初のコードはこんな感じです。
from openai import OpenAI
client = OpenAI(
api_key="YOUR_API_KEY",
base_url="https://crazyrouter.com/v1"
)
resp = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "user", "content": "Pythonでクイックソートを書いて"}
]
)
print(resp.choices[0].message.content)
もちろんこれで動きます。
ただし、プロジェクトが少し大きくなると、この書き方はすぐに苦しくなります。
たとえば、
• モデル名がアプリ全体に散らばる
• 業務ロジックとLLM呼び出しが密結合になる
• リトライやタイムアウトの扱いが統一できない
• 失敗時のフォールバック先を差し込めない
• モデル比較用のコードが増えて見通しが悪くなる
つまり、最初は便利でも、後で保守コストが上がるわけです。
なので、早い段階で「LLMを呼ぶ層」を1段挟んでおくと楽になります。
───
環境準備
今回はPythonのOpenAI SDKを使います。
pip install openai
OpenAI互換APIであれば、既存コードの資産をそのまま活かしやすいのがメリットです。
───
最小構成のマルチモデル呼び出し
まずは、モデル名だけ差し替えれば複数モデルを試せる最小構成です。
from openai import OpenAI
client = OpenAI(
api_key="YOUR_CRAZYROUTER_KEY",
base_url="https://crazyrouter.com/v1"
)
def chat(model: str, prompt: str) -> str:
response = client.chat.completions.create(
model=model,
messages=[
{"role": "user", "content": prompt}
]
)
return response.choices[0].message.content
if name == "main":
prompt = "Pythonでクイックソートを書いてください"
print(chat("gpt-4o", prompt))
print(chat("claude-sonnet-4-20250514", prompt))
print(chat("gemini-2.5-pro", prompt))
print(chat("deepseek-chat", prompt))
この時点でも、
• 呼び出し方式を統一できる
• モデル差し替えが簡単
• 比較実験を始めやすい
という利点があります。
ただし、これだけだと実運用ではまだ足りません。
───
クライアントクラスとして切り出す
次に、少しだけ実戦的にしてみます。
リトライ、共通設定、例外処理を1か所にまとめます。
この時点でも、
• 呼び出し方式を統一できる
• モデル差し替えが簡単
• 比較実験を始めやすい
という利点があります。
ただし、これだけだと実運用ではまだ足りません。
───
クライアントクラスとして切り出す
次に、少しだけ実戦的にしてみます。
リトライ、共通設定、例外処理を1か所にまとめます。
import time
from typing import Optional
from openai import OpenAI
class MultiModelClient:
def init(
self,
api_key: str,
base_url: str = "https://crazyrouter.com/v1",
max_retries: int = 2,
retry_delay: float = 1.5,
):
self.client = OpenAI(
api_key=api_key,
base_url=base_url
)
self.max_retries = max_retries
self.retry_delay = retry_delay
def chat(
self,
model: str,
prompt: str,
system_prompt: Optional[str] = None,
temperature: float = 0.7,
max_tokens: int = 1024,
) -> str:
messages = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
messages.append({"role": "user", "content": prompt})
last_error = None
for attempt in range(1, self.max_retries + 2):
try:
response = self.client.chat.completions.create(
model=model,
messages=messages,
temperature=temperature,
max_tokens=max_tokens,
)
return response.choices[0].message.content
except Exception as e:
last_error = e
print(f"[WARN] model={model}, attempt={attempt}, error={e}")
if attempt <= self.max_retries:
time.sleep(self.retry_delay)
raise RuntimeError(f"Request failed after retries: {last_error}")
使い方はこんな感じです。
if name == "main":
llm = MultiModelClient(api_key="YOUR_CRAZYROUTER_KEY")
result = llm.chat(
model="gpt-4o",
prompt="Bloom Filterをバックエンドエンジニア向けに説明してください",
system_prompt="あなたは経験豊富なソフトウェアエンジニアです",
temperature=0.2
)
print(result)
この形にしておくと、後からログ出力、メトリクス、キャッシュなどを差し込みやすくなります。
───
モデル名を業務コードに散らさない
マルチモデル対応で地味に大事なのがこれです。
モデル名を業務コードのあちこちに直書きしないこと。
たとえば以下のように、役割ベースの設定にしておくとかなり扱いやすくなります。
MODEL_CONFIG = {
"fast_chat": "gpt-4o-mini",
"best_writing": "claude-sonnet-4-20250514",
"best_reasoning": "gemini-2.5-pro",
"cn_task": "deepseek-chat",
}
呼び出し側は、モデル名そのものではなく、役割を指定します。
answer = llm.chat(
model=MODEL_CONFIG["best_reasoning"],
prompt="この設計のボトルネックを分析してください"
)
この方法の良いところは、
• モデル入れ替え時に業務コードを触らなくていい
• A/Bテストの切り替えがしやすい
• ステージング/本番で設定を変えやすい
• 将来的に設定ファイルや環境変数へ逃がしやすい
という点です。
───
フォールバックを入れる
実際の運用では、メインモデルが失敗したときに別モデルへ切り替えたいケースがよくあります。
たとえば、
• 品質優先でClaudeを第一候補
• 失敗時はGPTへ
• さらに失敗したらDeepSeekへ
といった形です。
簡単な実装例を載せます。
import time
from typing import Optional, List
from openai import OpenAI
class MultiModelClient:
def init(
self,
api_key: str,
base_url: str = "https://crazyrouter.com/v1",
max_retries: int = 2,
retry_delay: float = 1.5,
):
self.client = OpenAI(api_key=api_key, base_url=base_url)
self.max_retries = max_retries
self.retry_delay = retry_delay
def _single_chat(
self,
model: str,
prompt: str,
system_prompt: Optional[str] = None,
temperature: float = 0.7,
max_tokens: int = 1024,
) -> str:
messages = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
messages.append({"role": "user", "content": prompt})
last_error = None
for attempt in range(1, self.max_retries + 2):
try:
response = self.client.chat.completions.create(
model=model,
messages=messages,
temperature=temperature,
max_tokens=max_tokens,
)
return response.choices[0].message.content
except Exception as e:
last_error = e
print(f"[WARN] model={model}, attempt={attempt}, error={e}")
if attempt <= self.max_retries:
time.sleep(self.retry_delay)
raise RuntimeError(f"{model} failed: {last_error}")
def chat_with_fallback(
self,
models: List[str],
prompt: str,
system_prompt: Optional[str] = None,
temperature: float = 0.7,
max_tokens: int = 1024,
) -> str:
last_error = None
for model in models:
try:
print(f"[INFO] trying model={model}")
return self._single_chat(
model=model,
prompt=prompt,
system_prompt=system_prompt,
temperature=temperature,
max_tokens=max_tokens,
)
except Exception as e:
print(f"[WARN] fallback model failed: {model}, error={e}")
last_error = e
raise RuntimeError(f"All fallback models failed: {last_error}")
使用例:
if name == "main":
llm = MultiModelClient(api_key="YOUR_CRAZYROUTER_KEY")
result = llm.chat_with_fallback(
models=[
"claude-sonnet-4-20250514",
"gpt-4o",
"deepseek-chat"
],
prompt="Redis分散ロックのPython実装例を書いてください",
system_prompt="あなたはシニアバックエンドエンジニアです",
temperature=0.2
)
print(result)
このくらいでも、かなり実用的です。
簡易A/Bテストスクリプト
モデル比較をするとき、感覚だけで「なんとなくこっちが良い」と判断しがちですが、
最低限、レイテンシと出力の比較くらいはすぐ回せるようにしておくと便利です。
import time
from openai import OpenAI
client = OpenAI(
api_key="YOUR_CRAZYROUTER_KEY",
base_url="https://crazyrouter.com/v1"
)
MODELS = [
"gpt-4o",
"claude-sonnet-4-20250514",
"gemini-2.5-pro",
"deepseek-chat"
]
PROMPT = "LRUキャッシュを実装したPythonクラスを書き、計算量も説明してください。"
def run_once(model: str, prompt: str):
start = time.time()
try:
response = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
temperature=0.2,
max_tokens=1200
)
elapsed = time.time() - start
text = response.choices[0].message.content
return {
"model": model,
"success": True,
"latency_sec": round(elapsed, 2),
"preview": text[:200]
}
except Exception as e:
elapsed = time.time() - start
return {
"model": model,
"success": False,
"latency_sec": round(elapsed, 2),
"error": str(e)
}
if name == "main":
results = []
for model in MODELS:
results.append(run_once(model, PROMPT))
for item in results:
print("=" * 80)
print(item)
もちろん本格的なbenchmarkではありませんが、
• どのモデルが速いか
• 失敗しやすいモデルがあるか
• 出力の雰囲気にどんな差があるか
をざっくり確認するには十分です。
───
タスクごとにルーティングする
現実のアプリでは、全タスクを同じモデルで処理する必要はありません。
むしろ、分けたほうが素直です。
たとえばこんな設定にしておくと扱いやすいです。
TASK_ROUTER = {
"simple_chat": "gpt-4o-mini",
"code_generation": "claude-sonnet-4-20250514",
"reasoning": "gemini-2.5-pro",
"chinese_task": "deepseek-chat",
}
ヘルパー関数を用意して、
def route_model(task_type: str) -> str:
return TASK_ROUTER.get(task_type, "gpt-4o")
呼び出し側ではこうします。
ta
sk_type
= "code_generation"
model = route_model(task_type)
result = llm.chat(
model=model,
prompt="スレッドセーフなPythonのSingleton実装を書いてください"
)
print(result)
自分のアプリ側で小さなモデル調度レイヤーを持つ設計になってきます。
マルチモデル前提のアプリでは、この考え方がかなり重要だと思っています。
───
なぜOpenAI互換APIが便利なのか
複数の公式APIをそれぞれ直接つなぐ方法も、もちろんあります。
ただ、開発初期やPoC段階では、次のような面倒が出やすいです。
• SDKごとに書き方が違う
• エラーハンドリングが分散する
• A/B比較コードが増える
• モデル切り替えのたびに差分吸収が必要になる
その点、OpenAI互換APIでまとめて扱えると、
呼び出し層をかなり単純化できます。
今回例として使ったCrazyrouterも、その意味では「特定の1モデルを使うためのサービス」というより、
マルチモデル時代の呼び出し基盤として扱いやすいという印象です。
特に、
• 既存のOpenAI SDK資産を活かしたい
• モデル比較を頻繁にやりたい
• フォールバックやルーティングを入れたい
• 将来的にテキスト以外も扱いたい
というケースでは、こういう統一入口はわりと相性がいいです。
───
まとめ
マルチLLM対応を始めるとき、最初にやりがちなのは「とりあえず複数モデルを呼べるようにする」ことです。
でも、少し先を考えるなら、最初から以下を意識したほうが楽です。
• モデル呼び出しを1層に抽象化する
• モデル名をコード全体に散らさない
• リトライと例外処理をまとめる
• フォールバックを入れられる設計にする
• A/B比較しやすい形にする
• タスクとモデル選択を分離する
これをやっておくと、あとでモデルを差し替えるときのコストがかなり下がります。
もし既にOpenAI SDKベースでコードを書いていて、
複数モデルをまとめて扱える入口を探しているなら、CrazyrouterのようなOpenAI互換APIを試してみる価値はあると思います。