ローカル LLM の tool calling には 3 段階ある — 「対応している」と「ちゃんと動く」は別の話
TL;DR: ローカル LLM で tool calling を使おうとすると「対応しているはずなのに動かない」にぶつかる。調べてみると、tool calling の「対応」には 3 段階あって、モデルによって到達点がバラバラだった。
① そもそも呼ぶか
② フォーマットは正しいか
③ 引数は壊れないか。
この 3 段階のどこで止まるかで「動く / 動かない」の意味が変わる。CodeRouter を作る動機そのものだった話。
あらすじ — 10 話目です
第 1 話 (v1.8.1)ガチで動かしてみたら 3 連敗した話
第 2 話 (v1.8.2)自分が作った診断ツールに自分が騙された話
第 3 話 (v1.8.3)Ollama で詰んだ Qwen3.6 を llama.cpp で動かしたら、もう 1 つ偽陽性を出してた話
第 4 話 (v1.8.4)「フレームワーク待ち」の前提が翌日崩れた話
第 5 話 (v1.8.5)Claude Code とローカル LLM を繋ぐ 5 通りの経路
第 6 話 (v1.9.0)自分の検証フローに自分が騙された話
第 7 話 (v1.10.0)見積 3〜4 週間 → 実 2 日の話
第 8 話 (v2.0.0/v2.1.0)「動く」を「壊れない」に変える話
第 9 話 (v2.2.0)壊れたら自分で直す話
第 10 話 (本記事)tool calling の「対応」には 3 段階ある話
1〜9 話は CodeRouter の開発を追いかけた連作。第 10 話は視点を変えて、そもそも CodeRouter がなぜ必要なのかの根っこにある問題——ローカル LLM の tool calling——を整理します。
「tool calling 対応」は嘘じゃないけど足りない
HuggingFace のモデルカードや Ollama のドキュメントに「tool calling 対応」と書いてあるモデルは増えてきました。でもその「対応」の意味が、モデルによって全然違います。
Claude や GPT-4 の tool calling に慣れた状態で、ローカルの 7B〜32B モデルに同じことをやらせると、「対応しているはずなのに動かない」 が頻発します。エラーが出るならまだいい。エラーが出ずに、ただ tool を呼ばないことがある。
この現象を自分なりに整理した結果、tool calling の「対応度」には 3 つの段階があることに気づきました。
3 つの段階
Level 1: 呼ぶか呼ばないか
一番基本的な話。tool のスキーマをプロンプトに渡して「この tool を使え」と言ったとき、そもそも tool を呼ぶ応答を返すかどうか。
Level 1 で止まるモデルの典型的な症状:
tool のリストを渡しても、テキストで「ファイルを確認しましょう」と書くだけで実際には呼ばない
たまに呼ぶけど、5 回に 1 回くらい。残りはテキストで「結果は以下の通りです」と空想を書く
system prompt が長い(Claude Code の 15〜20K トークン)と、tool の存在自体を忘れる
これはモデルの訓練データに tool calling のパターンが十分含まれていないことが原因。小さいモデル(7B 以下)や、coding 特化でない汎用モデルでよく起きます。
CodeRouter 側でできることは少ない。モデル自体が「呼ぶ」判断をしないので、後から直しようがない。プロバイダーのフォールバック(呼ばないモデル → 呼ぶモデル)で対処するしかありません。
Level 2: フォーマットが正しいか
tool を呼ぶ判断はできる。でも 呼び方が間違っている。
Level 2 で止まるモデルの典型的な症状:
tool_calls フィールドに入れず、テキスト本文に JSON を書く
OpenAI 形式の function_call ではなく、独自のタグ(<tool_call>...</tool_call>)で出力する
JSON として壊れている(引用符が抜けている、末尾カンマがある)
これが CodeRouter の tool-call repair が対処する問題。モデルは「この tool を呼びたい」という意図を持っている。ただ出力フォーマットがフレームワークの期待と合わない。
CodeRouter は応答テキストの中から tool-call っぽい JSON を探し出して、正しい tool_calls 構造に組み直します。テキストに埋め込まれた JSON、コードブロック内の JSON、連続する複数の JSON——それぞれのパターンを検出して修復。
モデルの応答(テキスト本文):
"ファイルを確認します。```json{"name":"Bash","arguments":{"command":"ls"}}```"
CodeRouter の修復後:
tool_calls: [{function: {name: "Bash", arguments: {command: "ls"}}}]
content: "ファイルを確認します。"このレベルの問題は、実は一番多い。Qwen3-Coder や DeepSeek-Coder-V2 など、coding に強いモデルの多くが「tool を呼ぶ意図はあるがフォーマットが非標準」の状態にあります。
Level 3: 引数が壊れないか
tool を呼ぶ。フォーマットも正しい。でも 引数の中身がおかしい。
Level 3 で止まるモデルの典型的な症状:
file_path に存在しないパスを入れる
command に複数のコマンドを改行なしで詰め込む
必須引数を省略する
型が違う(文字列を期待しているところに数値を入れる)
これはモデルの「推論能力」の問題で、フォーマットの問題ではない。CodeRouter の wire-level 修復では対処できない領域。引数の中身の正しさは、モデルの知識と推論の質に依存します。
3 段階の分布
実際にローカルモデルを試した体感(CodeRouter の doctor --check-model + 実セッションの観察)での大まかな分布:

上の表のうち Gemma 4 と Qwen2.5-Coder の数字は、Ollama v0.23.1 の Anthropic API 互換エンドポイントに対して 10 回ずつ自動テストした実測値です(scripts/verify_ollama_0_23.py)。他は体感ベースの観察です。
Gemma 4 が印象的なのは、最小の e4b (9.6GB) ですら Level 3 に完全到達していること。従来は 32B 以上でないと Level 2 すら安定しなかったのが、Gemma 4 + Ollama v0.23.1 の組み合わせで一気にハードルが下がりました。
ただし傾向として モデルサイズと Level は概ね相関する ことは依然として言えます。Qwen2.5-Coder:7B が Level 0 に沈んでいるのは、7B クラスの限界の一例です。Gemma 4 e4b が例外的に優秀なのは、Google が tool calling を訓練段階で重視した結果だと推測します。
CodeRouter が対処できる範囲

CodeRouter が本当に価値を発揮するのは Level 2 です。モデルの意図は正しいのにフォーマットだけが合わない——この「翻訳可能なズレ」を修復するのが CodeRouter の中核機能。
Level 1 はフォールバックで回避できるけど、それは「別のモデルに逃げる」だけ。Level 3 は CodeRouter の守備範囲外。
なぜこの話を整理したかったか
CodeRouter を作り始めた頃(v0.3〜v0.7)、「ローカル LLM の tool calling が壊れる」という問題を一枚岩で捉えていました。「壊れる → 直す」。シンプル。
でも実際にいろんなモデルで試すと、「壊れる」の中身がモデルごとに違うことに気づきました。ある人が「tool calling が動かない」と言うとき、Level 1 の話をしているのか Level 2 の話をしているのか Level 3 の話をしているのかで、対処が全然違う。
Level 1 の問題を抱えている人に tool-call repair を勧めても意味がない。Level 3 で困っている人にフォーマット修復は的外れ。
3 段階に分けて考えることで、「自分の問題はどこにあるか」「どの道具で対処できるか」が見えやすくなる。CodeRouter の README に「Tool-call repair」と書いているけど、それが効くのは Level 2 の話であって、全部を魔法のように直すわけじゃない、という正直な整理。
フレームワーク側の事情
もう 1 つ複雑なのは、フレームワーク(Ollama / llama.cpp / LM Studio)ごとに tool calling の実装が違うこと。
同じモデル(例えば Qwen3-Coder:32B)を Ollama で動かすのと LM Studio で動かすので結果が変わる。これは第 4 話(LM Studio 0.4.12 で Qwen3.5 が動いた話)で体験しました。
理由は、フレームワークが モデルの tool calling 出力をどう解釈するか が違うから。モデルは生トークンとして <tool_call>{"name":"..."} みたいな文字列を出力しているだけ。それを構造化された tool_calls フィールドに変換するのはフレームワークの仕事。
Ollama: chat template に従って tool calling をパースする。template が不完全だと壊れる。
LM Studio: 独自の tool calling パーサーを持っている。モデルによっては Ollama より品質が良い。
llama.cpp (直接): tool calling のパースは自分で書く必要がある。自由度は最大。
つまり「モデルの Level」と「フレームワークの解釈品質」の掛け算で最終結果が決まる。同じ Level 2 のモデルでも、フレームワークの解釈がうまければ Level 3 に近い動作になるし、解釈が雑だと Level 1 に落ちることもある。
CodeRouter は フレームワークの後ろに座っている ので、フレームワークが解釈しきれなかった分を追加で修復する立場です。
2026-05 追記: Ollama v0.23.1 の Anthropic API 互換
この記事を書いた後、状況が 1 つ大きく動きました。Ollama v0.23.1 が Anthropic Messages API のネイティブエミュレーションを追加したのです。
これまで Ollama は OpenAI 互換 API (/v1/chat/completions) だけを提供していて、Claude Code と繋ぐには CodeRouter のような翻訳レイヤーが必須でした。v0.23.1 からは /v1/messages エンドポイントで Anthropic 形式のリクエスト/レスポンスを直接受け付けます。
実機検証した結果:
Gemma 4 + Ollama v0.23.1: 全サイズ (e4b/26b/31b) で type: "tool_use" ブロックが正規に返る。Level 3 完全到達。CodeRouter の翻訳も修復も不要
Qwen2.5-Coder:7B + Ollama v0.23.1: Anthropic API 経由でもテキスト本文に JSON を書く(Level 0)。CodeRouter の tool-call repair を通すと 5/5 PASS
つまり、フレームワーク側の事情がもう 1 段階複雑になりました。「モデルの Level」×「フレームワークの解釈品質」に加えて、「API 形式(OpenAI / Anthropic)」 という軸が加わった。同じ Ollama でも、OpenAI 互換経由と Anthropic 互換経由で結果が変わりうる。
これは第 4 話(LM Studio が Anthropic 互換を追加した話)と同じパターンの環境変化です。そしてまた第 5 話の教訓——プロダクトの存在意義は環境変化で書き換わる——が効いています。
メタ教訓
連作 1〜9 話のメタ教訓:
(1 話) ネットの評判と実機動作は別物
(2 話) 診断ツール自身も診断され続ける必要がある
(3 話) バグは同じツールの別の場所にも繰り返し現れる
(4 話) 結論には賞味期限がある
(5 話) プロダクトの存在意義は環境変化で書き換わる
(6 話) ドキュメントより実装の挙動が真実
(7 話) 罠を踏むたびに「踏めない仕組み」に変えると、機能追加コストが急激に下がる
(8 話) 「観測する」と「介入する」は質的に違う
(9 話) 「壊れないようにする」と「壊れた後を設計する」は別の仕事
第 10 話の教訓:
「対応している」の中身を分解しないと、正しい道具を選べない。
「tool calling 対応」は Yes/No じゃなく、少なくとも 3 段階のスペクトラムだった。問題を「一枚岩の壊れ」として見ている限り、間違った層に間違った道具を当ててしまう。
これは tool calling に限った話じゃなくて、「X に対応」「Y をサポート」と書いてある機能は、たいてい内部に段階がある。その段階を分解して初めて「自分の問題はどこにあるか」が見える。
次の話
連作 10 話を通じて CodeRouter の開発過程を追いかけてきました。次は一歩引いて、「CodeRouter とは結局何なのか」 を 1 記事で俯瞰します。連作を最初から読んでいない人向けに、なぜこの道具が生まれて、何を解決するのかを整理する話。
CodeRouter のリポジトリ: https://github.com/zephel01/CodeRouter
uvx --from coderouter-cli coderouter serve --port 8088 で v2.2.0 が動きます。Python 3.12 以上、依存 5 個。
いいなと思ったら応援しよう!
サーバー代とコーヒー代になります☕ 役に立ったら応援よろしくお願いします!