氷河期世代のクラウドエンジニア、terralien です。「まずローカルの Ollama で試して、うまくいったらクラウドの API に切り替えます」という進め方は、費用の面でも学習の面でもとても良い手だと思っています。実際そうしています。
ただ、その切り替えのときに詰まる場所は毎回決まっていて、しかもエラーメッセージが原因を教えてくれないところに集中しています。
数字はすべて手元の実測です。MacBook Pro(Apple M5 Pro / 64GB / macOS 26.6.1)+ Ollama 0.32.14 + gemma4:latest、クライアントは Python 3.11.2 + litellm 1.98.0。2026-08-29 に計測しました。クラウド API のキーは環境変数から外した状態で測っています。
用途 ── 揃えてくれるのは「呼び方」だけです
LiteLLM は、100以上の提供元を litellm.completion(model=..., messages=[...]) という1つの形に揃えてくれるライブラリです。差し替えが model 文字列1つで済むのは、実際とても楽です。
ただし、揃うものと揃わないものがあります。
| 揃う | 揃わない | |
|---|---|---|
| 関数の呼び方・引数名 | ✅ | |
レスポンスの形(choices[0].message.content) | ✅ | |
トークン数の項目名(usage.prompt_tokens) | ✅ | |
| モデル名の書き方 | ❌ 提供元ごとに別世界 | |
| 例外クラスと原因の対応 | ❌ 後述。ここが一番効く | |
| APIキーの要不要と検証タイミング | ❌ ローカルは素通り |
各国のコンセント形状を1つに揃える変換プラグのようなものです。挿せるようになるだけで、電圧が揃うわけではありません。 挿さったからといって、機器が無事とは限らない。
代表要素 ── 3つ
| 要素 | 何をするか | 罠 |
|---|---|---|
model="<provider>/<model>:<tag>" | 提供元・モデル・タグを1つの文字列で指定する | 3階層あり、どこがズレても似たエラーになる。プレフィクスを忘れると LLM Provider NOT provided |
api_base= | 接続先を上書きする | 指定しないと localhost:11434 などの既定値。別ホストで動かしている場合、黙って手元を見に行く |
api_key= | 認証情報 | ローカル接続では検証されない。デタラメでも通る(実測) |
ollama/ と ollama_chat/ という2つのプレフィクスがあるのも紛らわしいところです。前者は生成用、後者はチャット用のエンドポイントに繋がります。手元で測った差は、暖機したあとで 0.69秒 対 0.66秒、入力トークンが 27 対 23。速度はほぼ同じで、プロンプトの組み立て方が違うぶんトークンが少し変わる、という程度でした。新しく書くなら ollama_chat/ で問題ありません。
AIが書く例 ── 実際に書かせたもの
手元の Qwen3.8:27b に「LiteLLM で Ollama と OpenAI のどちらにも同じコードで問い合わせる関数を書いて」と頼みました(一字も直していません)。
import litellm
def ask_llm(prompt: str, model: str = "ollama/llama3", api_key: str = None) -> str:
# Ollama の場合は api_key を "ollama" に設定
if model.startswith("ollama/"):
api_key = "ollama"
response = litellm.completion(
model=model,
messages=[{"role": "user", "content": prompt}],
api_key=api_key
)
return response.choices[0].message.content
読みやすいですし、コメントまで日本語で付いています。そしてこの関数は、こちらの環境では既定値のままだと動きません。ollama/llama3 が入っていないからです。
このコードには、これから見る3つの穴が全部入っています。
監査① ── モデル名は3階層あり、どこがズレても似た顔で落ちます
model 文字列は provider/model:tag の3階層です。それぞれを1つずつ壊して投げました。
渡した model | 何がズレているか | 結果 |
|---|---|---|
gemma4:latest | provider が無い | BadRequestError: LLM Provider NOT provided |
ollama/llama3 | model が入っていない | APIConnectionError: model 'llama3' not found |
ollama_chat/gemma4:8b | tag が違う | APIConnectionError: model 'gemma4:8b' not found |
ollama_chat/gemma4 | tag 省略 | ✅ 成功(:latest として解決される) |
provider 抜けだけは BadRequestError で、しかもメッセージが親切です。ここは問題ありません。
問題は下2つで、モデルが「入っていない」のか「名前を間違えた」のかを、エラーは区別していません。どちらも not found です。ollama pull を忘れただけなのか、gemma4 を gemma-4 と書いたのか、実行時には分かりません。
読むときの監査ポイントは、モデル名がリテラルで散らばっていないかです。
# ❌ AIが書いた形。既定値が実在するかどうか、実行するまで分からない
def ask_llm(prompt, model="ollama/llama3"): ...
# ✅ 起動時に1回だけ確かめる(ollama なら /api/tags で一覧が取れる)
AVAILABLE = {m["name"] for m in httpx.get(f"{OLLAMA}/api/tags").json()["models"]}
if MODEL.split("/", 1)[1] not in AVAILABLE:
raise SystemExit(f"{MODEL} が手元にありません。ollama pull してください")
起動時に落とすのが正解です。リクエスト単位で落ちると、原因がユーザーの入力に見えてしまいます。
監査② ── 例外クラスが、原因を表していません
ここが本題です。同じ APIConnectionError が、まったく違う原因で飛んできます。
| 実際に起きていたこと | 例外クラス | メッセージ |
|---|---|---|
| モデル名の打ち間違い | APIConnectionError | model 'gemmma4:latest' not found |
| モデルを pull していない | APIConnectionError | model 'llama3' not found |
| 推論サーバが落ちている | APIConnectionError | [Errno 61] Connection refused |
| APIキーが未設定 | InternalServerError | Missing credentials. Please pass an api_key... |
上3つが同じクラスなのも困りますが、4行目が本当に厄介です。APIキーを設定していないという、こちらの設定ミス以外の何物でもない状態が、500系(サーバ内部エラー)として返ってきます。
これが何を意味するか。よくあるリトライの書き方を思い出してください。
@retry(retry=retry_if_exception_type((APIConnectionError, InternalServerError)),
stop=stop_after_attempt(5), wait=wait_exponential())
def ask(prompt): ...
「接続エラーとサーバエラーはリトライ対象」——一般論としては完全に正しい設計です。そしてこの設計のもとでは、モデル名のタイプミスと、APIキーの設定漏れが、指数バックオフ付きで5回リトライされます。直るわけがないものを、待ち時間を増やしながら5回試すわけです。
念のため、LiteLLM 自身の num_retries=3 は賢く振る舞いました。存在しないモデル名で測ったところ、リトライ無し 0.07 秒に対して num_retries=3 は 0.03 秒。再試行していません。 問題は、この判断を自前の except で書き直したときに失われることです。ライブラリが持っている分類を、自作のリトライで上書きしていないか。ここが読みどころです。
対処は単純で、クラスではなくメッセージで分けるしかありません。
except APIConnectionError as e:
if "not found" in str(e): # 設定ミス。リトライしても直らない
raise SystemExit(f"モデルが見つかりません: {e}")
raise # 本当の接続断。ここだけリトライに乗せる
文字列一致で分岐するのは気持ち悪いですが、気持ち悪さを消すために全部リトライに乗せるほうが高くつきます。
監査③ ── ローカルでは、APIキーは検証されません
AIが書いたコードには api_key = "ollama" というハードコードがありました。文字列 "ollama" に意味があるかを確かめるため、完全にデタラメな値を渡してみました。
| 接続先 | api_key | 結果 |
|---|---|---|
| ローカル Ollama | "sk-this-is-not-a-real-key" | ✅ 成功(東京 が返る) |
| ローカル Ollama | "ollama" | ✅ 成功 |
クラウド(gpt-4o) | 未設定 | ❌ InternalServerError: Missing credentials |
ローカル接続は認証しないので、キーが何であろうと通ります。ここが「ローカルで動いたのでクラウドに切り替えます」が滑る場所です。キーを渡す配線が壊れていても、ローカルでは1回もテストされません。
| コードの形 | 判定 |
|---|---|
api_key = "ollama" のハードコード | ❌ 意味のない文字列。クラウドに切り替えた瞬間に消える経路 |
api_key=os.environ["OPENAI_API_KEY"] | ○ 未設定なら KeyError で起動時に落ちる |
api_key=os.environ.get("OPENAI_API_KEY") | △ None が黙って渡り、リクエスト時に 500系で返る |
| キーをソースに直書き | ❌ 論外。git に入ったキーは後から消せない |
.get() と [] の違いだけで、気づくタイミングが「起動時」から「本番の1リクエスト目」に変わります。AIが書くコードは、ほぼ .get() か引数の既定値 None です。
監査④ ── 最初の1回を、計測に使わない
おまけですが、実害があったので書いておきます。プレフィクスによる速度差を測ろうとして、最初にこういう数字を得ました。
| 条件 | 1回目 | 暖機後(3回の中央値) |
|---|---|---|
ollama/gemma4:latest | 31.6 秒 | 0.69 秒 |
ollama_chat/gemma4:latest | 0.79 秒 | 0.66 秒 |
1回目だけ見ると「40倍遅い」という結論になります。実際は、1回目にモデルをメモリへ読み込んでいただけでした。暖機後は誤差の範囲です。
ローカル推論では、モデルのロードが数十秒かかり、一定時間使わないとアンロードされます。AIに書かせたベンチマークコードには、まず暖機処理が入っていません。「ローカルは遅い」という結論が出たら、それが計測の産物でないかを先に疑ってください。こちらは危うく間違った表を載せるところでした。
隣で読むもの
| 記事 | ここと繋がるところ |
|---|---|
| ストリーミング応答を読む | 乗り換え先で最初に挙動が変わるのがストリーミングまわりです |
| while True の中でAIが喋り続けます | モデルを差し替えると反復回数が変わる、の実測はこちら |
チートシート
| 見たもの | 疑うこと | 直し方 |
|---|---|---|
model="ollama/llama3" の既定値 | 手元に無いかもしれない | 起動時に /api/tags と突き合わせて落とす |
プレフィクスなしの model | LLM Provider NOT provided | ollama_chat/ などを付ける |
except APIConnectionError: retry | タイプミスもキー未設定もリトライされる | メッセージで分岐し、設定ミスは即座に落とす |
except InternalServerError: retry | キー未設定がここに来る(実測) | 同上 |
api_key = "ollama" | 意味のない値。切り替え時に消える経路 | 環境変数から取り、未設定なら起動時に落とす |
os.environ.get("...KEY") | None が黙って渡る | os.environ["...KEY"] で早く落とす |
api_base の指定が無い | 既定で手元を見に行く | 明示するか、環境変数で渡す |
| ローカルで動いたので OK としている | 認証経路が1度もテストされていない | キーが要る接続先で1回通す |
| 1回だけ測った速度 | ロード時間が混ざる(実測 31.6 秒 → 0.69 秒) | 暖機してから複数回の中央値 |
読めるようになったか、ひとつだけ確認
Q. APIConnectionError を捕まえて指数バックオフでリトライする実装は、妥当ですか?
半分だけ妥当です。実測では、推論サーバの停止(Connection refused)と、モデル名の打ち間違い(model 'gemmma4:latest' not found)が、どちらも APIConnectionError で返りました。前者はリトライで直る可能性があり、後者は何回試しても直りません。クラスだけで分けると、設定ミスを待ち時間付きで繰り返すことになります。
この記事を書きながら、ollama/ と ollama_chat/ の速度差の表を一度書いて、消しました。40倍という数字が出た瞬間に「面白い記事になるぞ」と思ってしまったのが良くなかったです。都合のいい数字ほど二度測る、という当たり前のことを、40倍を消すという形で覚え直しました。