【2026年最新】ローカルLLM実践ガイド|Gemma 4からQwen 3.5に乗り換えた理由とtool calling非対応の現実解
Last updated on

【2026年最新】ローカルLLM実践ガイド|Gemma 4からQwen 3.5に乗り換えた理由とtool calling非対応の現実解


【2026年最新】ローカルLLM実践ガイド — Gemma 4からQwen 3.5に乗り換えた理由とtool calling非対応の現実解

2026年、ローカルLLMの選択肢は爆発的に増えた。Qwen 3.5とGemma 4はApache 2.0、Llama 4はLlama Community License——いずれも商用利用可能で、ベンチマークも拮抗している。

だが、ベンチマークで選ぶと痛い目を見る

筆者はGemma 4(当時最新)を業務監視システムに導入しようとして、tool callingが壊れている問題にぶつかった。Ollamaでは動かない。llama.cppでもパーサーにバグがある。結局、Qwen 3.5-27Bに乗り換え、tool callingを使わない設計に切り替えた。

本記事では、この移行で得た知見を全て公開する。モデル選定の判断基準、llama.cppでの実運用設定、tool callingなしで構造化出力を実現する方法、そしてクラウドAIへの自動フォールバック設計まで。

【広告・PR】 本記事には A8.net のアフィリエイトリンク(XServer VPS)が含まれます。記事内のリンクからサービス契約があった場合、執筆者に報酬が発生することがあります。掲載は独立した実体験に基づく評価で、報酬の有無とは無関係です。料金・条件はキャンペーン等で変動するため、契約前に必ず公式サイトで最新条件を確認してください。

なぜGemma 4からQwen 3.5に移行したのか

Gemma 4のtool calling問題(2026年4月時点)

Gemma 4は2026年4月2日にリリースされた。ベンチマークは素晴らしい。AIME 2026で89.2%、Codeforces ELOは2150。マルチモーダル対応で、MoE(Mixture of Experts:複数の専門ネットワークを切り替えて効率的に推論するアーキテクチャ)により効率も良い。

しかし、tool callingが壊れている

具体的には:

  • Ollama v0.20.x: tool callパーサーが失敗。ストリーミングではtool callデータがreasoningフィールドに入り、contentが空になる
  • llama.cpp: tool call配列パラメータがJSON文字列としてシリアライズされる({}を含む値で発生)
  • トークナイザーバグ: ガベージ出力が発生するケースあり

llama.cppには修正PRがマージされたが(PR #21326、#21343)、筆者の環境では安定しなかった。Ollamaはそもそもv0.20.2時点でMetal 4に非対応。

移行先にQwen 3.5を選んだ理由

評価軸Gemma 4 31BQwen 3.5 27B判定
MMLU Pro85.2%86.1%Qwen
GPQA Diamond84.3%85.5%Qwen
AIME 202689.2%83.7%Gemma
コーディング2150 ELO1980 ELOGemma
日本語性能良好トップクラスQwen
tool calling安定性❌ 不安定✅ 安定Qwen
llama.cpp互換性△ パッチ必要◎ そのまま動くQwen
ライセンスApache 2.0Apache 2.0同等

数学・競プロではGemma 4が勝つ。だが筆者の用途は監視・分類・オーケストレーション。日本語のメッセージを分類し、JSON構造化出力を返すタスクが中心。Qwen 3.5の方が適していた。

llama.cppでのQwen 3.5運用設定(Mac Apple Silicon)

なぜOllamaではなくllama.cppか — M4/M5 Mac GPU推論の選択肢

Mac Apple Silicon(M5 Pro)での推論バックエンドは3つの選択肢がある。

バックエンド速度 (27B Q3_K_M)Metal対応tool calling安定性
llama.cpp18 tok/s✅ Metal 4✅(Jinjaテンプレート)
Ollama❌ Metal 4非対応△ モデル依存
MLX25-30 tok/s✅ ネイティブ❌ 非対応

OllamaはMetal 4に非対応(v0.20.2時点)で選択肢から外れた。MLXは速度では優位だが、OpenAI互換APIサーバーとしての機能が限定的。llama.cppが唯一、Metal 4 GPU対応+OpenAI互換API+tool callingテンプレートの全てを満たした。

起動設定

# launchdで常駐化(macOS)
llama-server \
  -m ~/models/gguf/Qwen3.5-27B-Q3_K_M.gguf \
  -c 32768 \
  --parallel 1 \
  --jinja \
  --flash-attn on \
  --metrics \
  --host 127.0.0.1 \
  --port 8080

各オプションの意味:

  • -c 32768: コンテキスト長。監視タスクには十分
  • --parallel 1: 同時リクエスト数。メモリ節約のため1に制限
  • --jinja: Qwen 3.5のfunction callingテンプレートを有効化
  • --flash-attn on: Metal GPUでFlash Attention有効化
  • --metrics: Prometheusメトリクスエンドポイント有効化(tok/s監視用)

thinking mode無効化: サーバー起動時ではなく、API呼び出し時にextra_body={"reasoning_budget": 0}で制御する方が柔軟。用途によってthinking modeのON/OFFを切り替えられる。

量子化の選択 — GGUF形式とは

GGUF(GPT-Generated Unified Format)は、llama.cppで使用されるモデルファイル形式。量子化とは、モデルの重みを32bit浮動小数点から低ビット(Q3=3bit、Q4=4bit等)に圧縮する技術で、精度を多少犠牲にしてファイルサイズとメモリ使用量を削減する。

Qwen3.5-27B-Q3_K_M.gguf  →  13GB  →  18 tok/s
Qwen3.5-27B-Q4_K_M.gguf  →  16GB  →  15 tok/s(VRAM制約)
Qwen3.5-27B-Q8_0.gguf    →  30GB  →  メモリ不足

Q3_K_Mは品質と速度のバランスが最も良かった。Q4_K_Mの方が品質は上だが、メモリ使用量が増えてスワップが発生し、実効速度が落ちる環境だった。

注意: 極端な量子化(Q2_K以下)はtool calling精度を著しく低下させる。Q3_K_M以上を推奨。

メモリが足りない場合: 統合メモリ36GB以上のMacなら27B Q4_K_Mが快適に動く。Apple Silicon Macの選び方は、ローカルLLM運用を視野に入れるならメモリ36GB以上を強く推奨する。

クラウドGPUで試したい場合: ローカル環境が不十分な場合、VPSにGPUインスタンスを立ててリモートでllama-serverを動かす方法もある。XServer VPSはコスパが良く、GPU付きプランでローカルLLMの検証環境としても使える。

tool calling非対応でも戦える構造化出力

問題:tool callingは不安定

Qwen 3.5はllama.cppのtool calling(--jinja)に対応しているが、実運用では問題がある。

  • 量子化モデルではtool callの引数パースが不安定
  • 小さいモデル(9B以下)ではtool呼び出しに固執して文脈を無視する
  • マルチターンのtool使用でループに入ることがある

そこで筆者は、tool callingを使わず、プロンプトエンジニアリング+json_schema制約で構造化出力を実現する方針に切り替えた。

方法1: json_schema制約付き出力(推奨)

llama.cppのresponse_formatパラメータでJSONスキーマを強制する方法。出力が必ずスキーマに従うため、パース失敗がゼロになる。

from openai import OpenAI

client = OpenAI(
    base_url="http://127.0.0.1:8080/v1",
    api_key="sk-local",  # llama-serverは認証不要だがSDKが要求
)

response = client.chat.completions.create(
    model="27b",
    messages=[{"role": "user", "content": f"分類してください: {message}"}],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "task_classification",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "priority": {"type": "string", "enum": ["P0", "P1", "P2", "P3"]},
                    "category": {"type": "string", "enum": ["client", "sales", "content", "infra", "monitor"]},
                    "action": {"type": "string", "enum": ["execute", "notify", "skip"]},
                },
                "required": ["priority", "category", "action"]
            }
        }
    },
    max_tokens=200,
)

利点: llama.cppの制約デコーディングにより、出力トークンがスキーマから逸脱しない。tool callingのパーサーを経由しないため安定。

欠点: 複雑なネスト構造は対応しづらい。enumの選択肢が多すぎると精度が下がる。

方法2: プロンプトエンジニアリング+後処理

JSONスキーマ制約が使えない環境(古いllama.cppバージョンなど)向け。

import json
import re

SYSTEM_PROMPT = """あなたはタスク分類AIです。
以下のJSON形式で必ず回答してください。他のテキストは出力しないでください。

{"priority": "P0-P3", "category": "client|sales|content|infra|monitor", "action": "execute|notify|skip"}
"""

result = llm_call(f"{SYSTEM_PROMPT}\n\n分類対象: {message}")

# JSON抽出(モデルが余計なテキストを付与するケースへの対処)
json_match = re.search(r'\{[^{}]+\}', result)
if json_match:
    parsed = json.loads(json_match.group())

方法3: thinking mode除去

Qwen 3.5はデフォルトでthinking mode(<think>...</think>タグ)を出力する。--reasoning-budget 0で抑制できるが、完全には消えないことがある。

import re

def clean_thinking(content: str) -> str:
    """Qwen 3.5のthinkingタグを除去"""
    content = re.sub(r"<think>.*?</think>\s*", "", content, flags=re.DOTALL).strip()
    content = re.sub(r"^</think>\s*", "", content).strip()
    return content

この処理は全てのLLM呼び出しに一律で適用している。

実装アーキテクチャ:ローカルLLM+クラウドAIの二層構成(Apple Silicon最適化)

役割分離の設計思想

全てをローカルLLMで賄おうとすると破綻する。筆者の環境では以下のように役割を分離している。

┌─────────────────────────────────────┐
│  ローカルAI (Qwen 3.5-27B, :8080)   │
│  役割: 監視・分類・振り分け          │
│                                     │
│  ・サービスのヘルスチェック           │
│  ・受信メッセージの優先度分類         │
│  ・タスクのルーティング              │
│  ・異常検知時のアラート生成           │
│  ・状況レポート生成                  │
└──────────────┬──────────────────────┘
               │ 重い仕事は委任

┌─────────────────────────────────────┐
│  クラウドAI (Claude)                 │
│  役割: 全実務                       │
│                                     │
│  ・記事生成・SNS投稿                 │
│  ・コードレビュー・品質評価           │
│  ・営業提案・顧客対応                │
│  ・ブラウザ操作                      │
└─────────────────────────────────────┘

ローカルAIは「振り分け係」に徹する。軽量な分類タスクだけを処理し、実務はクラウドAIに委任する。

Python統一呼び出しレイヤー

ローカルAIとクラウドAIを透過的に切り替えるライブラリを実装した。

"""llm_provider.py — ローカルAI統一呼び出しレイヤー"""

from openai import OpenAI
import subprocess
import re
import os

def llm_call(prompt: str, *, provider: str = "llama", model: str = "27b",
             max_tokens: int = 4096, temperature: float = 0.7, timeout: int = 120) -> str:
    """統一LLM呼び出し — プロバイダーを意識せず使える"""
    if provider == "claude":
        return _call_claude(prompt, model=model, timeout=timeout)
    return _call_llama(prompt, model=model, max_tokens=max_tokens,
                       temperature=temperature, timeout=timeout)

def _call_llama(prompt, *, model="27b", max_tokens=4096,
                temperature=0.7, timeout=120) -> str:
    """llama-server (OpenAI互換API) 呼び出し"""
    client = OpenAI(base_url="http://127.0.0.1:8080/v1", api_key="sk-local")

    try:
        response = client.chat.completions.create(
            model=model,
            messages=[{"role": "user", "content": prompt}],
            max_tokens=max_tokens,
            temperature=temperature,
            timeout=timeout,
        )
        content = response.choices[0].message.content or ""
        # thinkingタグ除去
        content = re.sub(r"<think>.*?</think>\s*", "", content, flags=re.DOTALL).strip()
        return content

    except Exception:
        # llama-server停止時 → Claude CLIに自動フォールバック
        return _call_claude(prompt, model="sonnet", timeout=timeout)

def _call_claude(prompt, *, model="sonnet", timeout=300) -> str:
    """Claude CLIフォールバック"""
    cmd = ["claude", "--model", f"claude-{model}-4-6",
           "--print", "--bare", prompt]
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
    return result.stdout.strip()

ポイント: _call_llamaが失敗したら自動的に_call_claudeにフォールバックする。llama-serverがダウンしても、呼び出し側のコードを変更する必要がない。

Bash版の呼び出し

# scripts/lib/llm-provider.sh
source scripts/lib/llm-provider.sh

# タスクタイプでルーティング
LLM_TASK_TYPE=health_check llm_call "サーバー状態を確認"  # → ローカルAI
LLM_TASK_TYPE=article_write llm_call "記事を書いて"        # → Claude

llm-routing.confで1行変更するだけでルーティングを切り替えられる。

# scripts/lib/llm-routing.conf
LLM_ROUTE_health_check=llama:27b
LLM_ROUTE_article_write=claude:opus
LLM_ROUTE_message_classify=llama:27b

モデル検証で学んだこと

試して落ちたモデルたち

筆者は最終的にQwen 3.5-27Bに落ち着くまでに、いくつかのモデルを試した。

モデル結果不採用理由
Qwen 3.5-35B-A3B (Flash-MoE)❌ 不採用日本語が文字化け。サーバーも不安定。llama.cppでMoE GGUFがkernel panic(GitHub #19825)
Gemma 4 31B❌ 不採用tool callingが壊れている。Ollamaは非対応。llama.cppもトークナイザーバグ
Qwen 3.5-9B Q4_K_M△ フォールバック用動くが精度不足。分類タスクでの誤判定率が27Bの3倍
Qwen 3.5-27B Q3_K_M✅ 採用安定動作。日本語分類で実用レベル。18 tok/sで監視タスクに十分

MoEモデルの罠

Qwen 3.5-35B-A3Bは「35Bパラメータのうち3Bだけアクティブ」というFlash-MoE。理論上は軽くて速いが、実際は:

  • llama.cppでGGUF変換するとkernel panic
  • 公式のsafetensors形式で直接推論するとサーバーが不安定
  • 英語は問題ないが日本語が文字化け

2026年4月時点ではDenseモデル一択。MoEのGGUF対応はllama.cppの今後のアップデートに期待。

パフォーマンスチューニング(Apple Silicon Metal GPU)

Metal GPUの活用

Flash Attentionは、Attention計算のメモリ効率を改善するアルゴリズム。通常のAttentionがO(n²)のメモリを消費するのに対し、Flash AttentionはタイリングによりVRAM使用量を大幅に削減する。

# Flash Attentionを有効化(必須)
--flash-attn on

# Metal GPUのレイヤー数を明示(デフォルトで全レイヤーGPU)
-ngl 99

M5 Proの統合メモリアーキテクチャでは、CPU⇔GPU間のデータコピーが不要。これがllama.cppのMetal対応の最大の利点。

thinking mode無効化の効果

# thinking modeあり(デフォルト)
--reasoning-budget -1  # → 平均応答時間 4.2秒

# thinking mode無効化
--reasoning-budget 0   # → 平均応答時間 1.8秒

監視タスクでは「深く考える」必要はない。思考トークンを省略するだけで応答時間が半分以下になる。

ヘルスチェック

# llama-serverの状態確認
curl http://localhost:8080/health
# → {"status":"ok"}

# launchdで常駐化 → ダウン時自動復旧
launchctl load ~/Library/LaunchAgents/com.local.llama-server.plist

よくある質問

Q: OllamaじゃなくてMLXは?

MLXはApple Silicon向けに最適化されており、速度ではllama.cppを上回る(同じ27Bモデルで25-50%高速)。ただし、OpenAI互換APIサーバーとしての機能が限定的で、response_format(json_schema制約)に対応していない。速度重視ならMLX、API互換性重視ならllama.cpp

Q: Gemma 4のtool callingはいつ直る?

llama.cppにはトークナイザー修正がマージ済み(2026年4月)。Ollamaはv0.21で対応予定。ただし、量子化モデルでのtool calling精度は構造的に限界がある。json_schema制約の方が安定する可能性が高い。

Q: クラウドAIなしで全部ローカルでやれる?

27Bモデルで実用的にこなせるのは、分類・要約・レポート生成など「出力が短いタスク」。長文記事の生成、複雑なコードレビュー、マルチステップのエージェント動作は、クラウドAIの方が圧倒的に品質が高い。筆者は「分類はローカル、実務はクラウド」の二層構成を推奨する。

まとめ

判断ポイント筆者の選択理由
モデルQwen 3.5-27B日本語性能・llama.cpp互換性・安定性
バックエンドllama.cppMetal 4対応・OpenAI互換API・tool callingテンプレート
構造化出力json_schema制約tool callingより安定・パース失敗ゼロ
量子化Q3_K_M品質と速度のバランス(13GB/18 tok/s)
アーキテクチャローカル+クラウド二層分類=ローカル、実務=クラウド

ローカルLLMは「万能ツール」ではない。得意なことに特化させ、苦手な部分はクラウドに任せる。この割り切りが、実運用での安定性を生む。

ベンチマークではなく、自分のタスクで動くかどうか。それがモデル選定の唯一の基準だ。


関連記事

ローカルLLMやAI開発に興味がある方は、以下の記事も参考にしてほしい。

GPUサーバーでローカルLLMを動かしたい方へ: Macのメモリが足りない、あるいはリモートから常時アクセスしたい場合、VPSにGPUインスタンスを立てるのが現実的な選択肢だ。XServer VPSはGPU付きプランがコスパに優れ、llama-serverの常駐にも適している。


参考リソース

本記事の執筆にあたり、以下の情報源を参照・検証した。

モデル公式・ベンチマーク

llama.cpp tool calling関連

比較・分析記事

筆者環境での検証条件

  • ハードウェア: Mac(Apple Silicon M5 Pro / 統合メモリ36GB)
  • llama.cpp: mainブランチ最新ビルド(2026年4月8日時点)
  • 検証期間: 2026年4月2日〜4月9日(Gemma 4リリース翌日から1週間)
  • 計測方法: llama-serverの/healthエンドポイント+手動タイマー計測(tok/sはllama-server標準出力から取得)

本記事のリンクにはアフィリエイト広告(A8.net)を含みます。XServer VPSを契約された場合、執筆者に報酬が発生することがあります。サービス選定は実体験に基づく独立した評価であり、掲載順や評価は報酬の有無と無関係です。料金・条件は2026年4月時点の情報を参照しており、契約前には公式サイトで最新条件を必ず確認してください。記事最終更新日: 2026-04-27。