前回の記事で xl-ai + ローカル Gemma との泥仕合を終えたあと、私の中にはひとつの結論が焼き付いていました。ローカルモデルは tool calling がまるでできない。 Gemma 3 27B は安定した JSON すら吐けず、「モデルが行儀よく tool-call 形式を話してくれる」前提のライブラリ(xl-ai のような)は接続した瞬間に爆発。最後はバックエンドに手作りのアダプタを挟んでなんとか救出した、という顛末でした。

そこでひらめいたわけです。ローカル AI をやる人が全員この壁にぶつかるなら、「tool-calling 修復プロキシ」はブルーオーシャンなのでは?

構想はシンプル。アプリとローカルモデルの間に挟む OpenAI 互換の中間レイヤーです。アプリは普段どおり標準の tools 形式でリクエストを送る。プロキシが tool 定義をモデルの噛み砕けるプロンプトに翻訳し、モデルの吐いた壊れた JSON を拾って修復し、スキーマで検証し、失敗したら自動リトライ――「tool calling のできないモデル」を、外から見れば「できるモデル」に見せかける。

美しい話に聞こえました。Vercel AI SDK や LangChain でローカルモデルを繋ぐ人はみんなまさにこれで愚痴っているのに、独立した修復レイヤーは市場に存在しない。

しかし今回は学習済みです。手を動かす前に、まず 1 日かけてベンチマークを書き、仮説を検証する。

そのベンチマークが、自らの手で私のアイデアを処刑しました。この記事はその完全な検死報告書です――ついでに、2026 年 6 月時点でローカル tool-calling 用モデルを選ぶための実用スコアボードも付けておきます。

(先に検証しようと思えたこと自体が前回の戦いの教訓でした。公式ライブラリの美しいデモの裏には、いつも強力なバックエンドという前提が隠れている。今回は自分が「ライブラリ作者」側に回るのだから、自分の前提とする「弱いバックエンド」が本当に存在するのか、先に確かめるべきだと。)


ベンチマークの設計#

テストケースは 25 個、6 カテゴリ。すべて実プロダクトで実際にぶつかる形です:

カテゴリ何を試すか
simple5単一ツールの直球:天気、メール送信、SQL(非英語プロンプト含む)
complex5ネストしたスキーマ:出席者がオブジェクト配列で required 付きのカレンダーイベント
html4HTML-in-JSON:xl-ai の applyDocumentOperations そのもの――JSON フィールドに HTML 文字列、引用符エスケープ地獄
parallel31 つの依頼で 2 回以上の call が必要:「東京とロンドンの天気を比較して」
nocall4呼ばない自制心:「天気と気候の違いは?」(get_weather ツールを持たせた状態で)
select4ツールを 4 つ並べて、正しいものを選べるか

各モデルは 2 つの条件で走らせます:

  • native:Ollama の OpenAI 互換 API + tools パラメータ――つまり Vercel AI SDK / LangChain が今日実際に送っているもの。
  • shim:「修復プロキシ」のシミュレーション――tools は渡さず、system prompt で JSON のみの出力を要求。返答に対してフェンス除去、<think> ブロック除去、括弧バランス走査による JSON 抽出、jsonrepair による修復、ajv でのスキーマ検証、エラー内容を添えた 1 回のリトライを行う。
js
// shim の中核:JSON 抽出(think ブロック除去、フェンス除去、
// 括弧バランス走査)→ 修復 → 検証 → リトライ
function parseShimOutput(text) {
  const raw = extractJson(text)        // 最初の括弧バランスの取れた {...} を探す
  let obj
  try {
    obj = JSON.parse(raw)
  } catch {
    obj = JSON.parse(jsonrepair(raw))  // 壊れた JSON の救急救命室
  }
  // ...この後 ajv スキーマ検証、失敗ならエラーメッセージ付きで 1 回リトライ
}

採点は「call すれば合格」ではありません。ツール名が正しいこと、引数が宣言されたスキーマの ajv 検証を通ること、内容チェックを通ること(メールが本当に bob@example.com に送られたか)、parallel は必要な数の call を全部出すこと、そして nocall ケースで 1 回でも call したら即死。temperature: 0、すべて RTX 5060 Ti 16GB 一枚 + Ollama、量子化は各モデルのデフォルト Q4 です。

(正直に言っておくと:25 ケース、1 パス、Q4 量子化――これは学術論文ではなくスモークテストです。ただ、下の数字を見れば同意してもらえるはず。0/25 と 25/25 の差は、ノイズで説明できるものではありません。)


スコアボード#

native スコア順:

モデルnativeshim備考
qwen3.5:4b25/2522/25満点、しかも 4B
qwen3.5:9b25/2525/25満点
granite4.1:8b ✨24/2524/25IBM の新世代
ministral-3:8b ✨24/2524/25Mistral のエッジ路線
granite4:tiny-h22/2512/25shim で半減
lfm2.5:8b ✨22/255/25「tool calling のために生まれた」と自称;shim で 5 まで崩壊
mistral-nemo22/2525/252024 年の古参、意外に戦える
nemotron-3-nano:4b ✨21/2523/25NVIDIA のエージェント路線
qwen3.6:27b ✨21/2523/25身内の 4B の弟分に負ける、後述
gemma4:e4b20/2524/25
gemma4:e2b19/2518/25
gemma4:12b18/2520/25html カテゴリ 0/4、一族の呪い
hermes3:8b18/2523/25
qwen3:4b16/2517/25qwen3.5:4b と比較:一世代の差 = 9 問
phi4-mini12/2522/25
llama3.2:3b9/2516/25
functiongemma:270m ✨7/250/25270M の特化モデル、ネイティブ形式しか認めない
command-r7b4/2511/25native の 4 点は全部 nocall の「沈黙得点」
deepseek-r1:8b4/2523/25thinking モデルの二重人格、後述
glm4:9b4/2513/25command-r7b と同じく沈黙で稼ぐ
gemma3:12b0/2523/25API が 400 を直接返す:not support tools
gemma3:4b0/2519/25同上

(✨ = 2026 年 5〜6 月リリースの新モデル、執筆週に pull して実測)


発見 1:前回の戦いは冤罪ではなかった――Gemma 3 は本当にゼロ点#

まず一番下の 2 行から。Gemma 3 の native は 0/25――しかも答えを間違えたのではなく、Ollama の API が 400 を直接返します:

registry.ollama.ai/library/gemma3:12b does not support tools

モデルの chat template に tool という概念がそもそも存在しない。つまり前回 xl-ai と戦ったあの戦争は、初日から勝ち目がなかったのです。私の繋ぎ方が悪かったのではなく、あれは API レベルで存在しないものだった。 この 0/25 を見た瞬間、遅れてきた名誉回復のような気分になりました。

そして shim は gemma3:12b を 0 点から 23/25 まで引き上げます。言い換えれば――私の修復プロキシのアイデアは、2025 年世代のモデルに対しては完全に成立していた。 プロンプト戦略 + JSON 修復 + リトライで、「API が tools を拒否するモデル」に 92% を出させることが本当にできる。

このベンチマークを 2025 年の中頃に回していたら、たぶん私は開発を始めていたでしょう。


発見 2:世代の断層――窓は Qwen3.5 が出た日に閉まっていた#

次に一番上の 2 行。

qwen3.5:4b、native 25/25、満点。 4B のモデルです。3.4GB。私の中流階級 GPU で軽々と動く。

「だいたい使える」ではなく 25 問全問正解:ネストしたスキーマも全部、HTML-in-JSON も全部(<b>final</b> タグを保持しろという問題まで)、並列すべきときは並列し、黙るべきときは黙り、4 択のツール選択も全問正解。結果の JSON を睨んで 1 問でも間違いを探そうとしましたが、ありませんでした。

同一ファミリー内の比較はさらに残酷です:

  • qwen3 → qwen3.5(同じ 4B):16/25 → 25/25
  • gemma3 → gemma4(同じ 12B):0/25 → 18/25(「API が拒否」から「だいたい動く」へ)

たった 1 年で、ネイティブ tool calling は「ローカルモデルの集団的障害」から「新世代の標準装備」になった。前回の記事に書いた「うちのローカル Gemma は tool calling がまるで安定しない」という一文は、2025 年のモデルには事実でしたが、2026 年のモデルにはもう古い情報です。


発見 3:修復レイヤーは新モデルに対してむしろ減点要因#

これがプロキシのアイデアへの二本目のナイフで、より深く刺さります。

qwen3.5:4b を見てください:native 25/25、shim 22/25。同じモデルに私の「修復レイヤー」を被せただけで 3 問落ちる。granite4:tiny-h はもっと悲惨:22/25 が 12/25 まで叩き落とされる。

理由は難しくありません。新世代モデルは訓練の段階で公式の tool-calling 形式(自前の chat template、自前の特殊トークン)に深くアラインされている。私の shim は tool 定義を「JSON だけで答えよ」という土着プロンプトに翻訳する――正規の訓練を受けた人に、私の発明した方言で喋れと強要するようなものです。ネイティブの能力が、私の「親切」によって妨害される。

これはプロキシの価値提案にとって致命的です:

  • 旧モデル(gemma3)に対して:shim の圧勝、0 → 23。でも、それなら ollama pull qwen3.5 一発で済むのでは?
  • 新モデル(qwen3.5)に対して:shim はマイナス資産。

修復プロキシの唯一の生息地は「旧モデルに縛られていて、tool calling が必要で、モデルを替えられない」ユーザーだけ。この交差点はプロジェクトを支えるには狭すぎる。


発見 4:ケーススタディ――モデルにはそれぞれの死に方(と生き方)がある#

deepseek-r1:8b の二重人格:native 4/25(ネイティブの tool call をほぼ出さず、推論が終わると自然言語でそのまま答える)、shim 23/25(JSON を要求すれば素直に吐く、しかも高品質)。thinking モデルは shim 条件の最大の受益者です――<think> ブロックを剥がせば、中の JSON はむしろきれい。

command-r7b と glm4:9b の「沈黙得点」:native 各 4/25、しかもその 4 点は全部 nocall カテゴリ由来――つまりベンチマーク全体を通して tool call を 1 回も出さず、「呼んではいけない」問題で黙っていただけで 4 点拾った。tools タグはただの飾りです。

HTML-in-JSON は Gemma 一族の呪い:gemma4 は 3 サイズとも html カテゴリで 0/4。典型的な失敗はスキーマ検証が operations[0]type フィールド欠落を検出するパターン――HTML エスケープの圧力の下で構造が崩壊する。まさに xl-ai のシナリオそのもの:gemma4 を BlockNote に繋いでも、血と涙はリプレイされます。同じ問題で qwen3.5 は 4/4。モデル選びを間違えたら、フレームワーク側でどう繋いでも無駄です。

mistral-nemo、老兵は死なず:2024 年中頃の 12B の古参が native 22/25。当時 Mistral は function calling を訓練の第一級目標に据えていて、2 年後の今も新世代と腕相撲ができる。tool calling が訓練目標に入っていたかどうかは、パラメータ数より重い。


2026 年 6 月の新兵たち:世代まるごとアイデアの対立側に並んだ#

執筆した週に、Ollama library 上の 2026 年 5〜6 月リリースの新モデル(16GB のカードに載るもの)を全部 pull して追加実測しました。qwen3.5 の満点が「点」だったとすれば、新兵たちが繋いだのは「線」です:

granite4.1:8b と ministral-3:8b が揃って 24/25。 IBM と Mistral の新世代が、どちらも満点まであと 1 問。tool calling はもう Qwen の専売特許ではなく、新世代の入場料です。

lfm2.5:8b は表の中で一番ドラマのある行:Liquid AI の公式コピーはまさに「民生ハードウェアで高速かつ信頼性の高い tool calling のために」と謳っていて、native 22/25 はその看板に恥じない――ところが shim では 5/25 まで崩壊、全表最大の逆落差です。ネイティブ形式に特化しすぎたモデルに土着の JSON プロンプトを無理やり食わせると、言葉そのものを忘れる。qwen3.5 の 3 問ダウンよりも暴力的に、同じ一つの事実を証明しています:修復レイヤーは新モデルにとって中立ではない。毒である。

qwen3.6:27b、今回いちばん高くついた 1 問、21/25――身内の 4B の弟分に敗北。 17GB のモデルを 16GB のカードに詰め込むと、レイヤーの 14% が CPU に追い出され、推論速度は一桁落ち、50 回の推論を終えるだけで 2 時間近くかかりました。それでスコアは qwen3.5:4b(3.4GB、全部 VRAM 内、爆速)より 4 問低い。民生 GPU でローカル AI をやる全員への一行:VRAM に収まる「小さくて新しい」は、はみ出す「大きい」に勝つ。 パラメータ数の見栄は、オフロードの前では無価値です。

functiongemma:270m はおまけの小ネタ:Google 製 270M の function-calling 特化モデル、native 7/25(このサイズなら上出来)、shim 0/25――モデル全体がネイティブ形式を中心に蒸留されていて、その形式の外では 1 問も生き残れない。特化モデルの極端な見本として、「形式ロックイン」現象の対照群をちょうど務めてくれました。


判決:アイデアは死んだ。だが良い死に方だった#

あの修復プロキシに戻ります。死亡診断書は 3 行:

  1. 問題そのものが自然消滅しつつある。 tool-calling 修復は「モデル世代の問題」であって「構造的な問題」ではない。新世代オープンモデル(筆頭は qwen3.5)はすでにネイティブで解決済みで、今後も良くなる一方。
  2. 修復レイヤーは新モデルにマイナス資産。 ターゲットユーザーがモデルを更新した瞬間、あなたのプロダクトは「助け」から「妨害」に変わる。
  3. 代替手段はコマンド 1 行。 ollama pull qwen3.5:4b、3.4GB、無料。どんなプロキシの導入コストもこれには勝てない。

というわけで、このプロジェクトはプロダクトコードの 1 行目を書く前に処刑されました。執行人は自分で書いた 25 個のテストケース。所要時間、1 日。

今日からローカルで tool calling をやるなら、実用アドバイスは 4 行に圧縮できます:

  • 迷ったらこれ:qwen3.5(4b で既に満点、VRAM 予算があれば 9b);granite4.1:8b と ministral-3:8b は合格点の第二志望
  • VRAM に収まらない大型モデルを無理に載せない:qwen3.6:27b は 16GB カード上で遅い上に 4B に負ける
  • thinking モデルを使うなら:deepseek-r1 は自前の JSON 出力レイヤー必須、native の tool call には触るな
  • 地雷:構造化出力における gemma 全系列(特にペイロードに HTML/markup を含むとき)、および 2025 年上半期以前のモデル全部

最後に#

前回の記事の結びにこう書きました。「公式ライブラリの美しいデモの裏には、たいてい非常に強力なバックエンドという前提が隠れている」。この記事はその鏡像です:スタートアップのアイデアの美しい青写真の裏には、たいてい「変化しない世界」という前提が隠れている。 私は「ローカルモデルは tool calling ができない」という痛みが持続すると仮定しましたが、モデル世代の入れ替わりは、私がプロキシを書き上げる速度より速かった。

1 日のベンチマークが、1〜2 ヶ月を食い潰すはずだったプロジェクトを殺した。失敗に聞こえますが、実際には今年一番割の良い取引でした――検証の価値は自分の正しさを証明することではなく、まだ安いうちに自分の間違いを発見することにある。

ベンチマークのコードと全 22 モデルの生の結果 JSON は GitHub にあります。25 ケースは実プロダクトで実際にぶつかる形(xl-ai 式の HTML-in-JSON キラー問題を含む)をカバーしています。手元のモデルで回してみてください――もしどれかが私の表を殴り倒したら、教えてください。殴られるのは歓迎です。もう慣れましたから。