投稿 - ここで続く内容は、Claude Opus 4.6 がツール呼び出しで動かなかった件についてログ解析を何百回も往復した後、さらに Qwen 3.5 モデルがローカルの llm プロバイダーや Nano-Gpt によって混乱してしまったことを踏まえて生成されたものです。当時、Pi コーディングエージェントで自分が使うために修正しました。
必要だった修正の一部はもはや不要になっています(TLDR は末尾)が、ほとんどは今もなお適用可能です。これは今日の時点で検証済みです。
Qwen 3.5 モデルを使っていて、モデルのパフォーマンス、ツール呼び出し、あるいは全般的な不安定さに問題がある場合、以下の参照は有用な読み物になるかもしれません。
結局のところ、pi coding agent + llamacpp + Bartowski's quants(安定性のため)に対する以下の修正が、すべての Qwen 3.5 モデル(Q5_k_L)で、私の体験を 99% の信頼性と品質にまで引き上げたのはこれでした。
誰かの役に立てば幸いです。(これは、このスレッドへのより長い回答として動機づけられたものでした - https://www.reddit.com/r/LocalLLaMA/comments/1scucfg/comment/oei95fn/)
OPUS がここから生成したレポート-->>
エージェント的なセットアップ(コーディングエージェント、関数呼び出しループ)で Qwen 3.5 を動かす? ツール呼び出しを壊す 4 つのバグ、どのサーバーが何を修正したか、そしてクライアント側でまだ必要なことは何か。 --- バグ 1. XML ツール呼び出しがプレーンテキストとして漏れる。 Qwen 3.5 はツール呼び出しを <function=bash><parameter=command>ls</parameter></function> のように出力します。 サーバーがこれを解釈できない場合(特に XML の前にテキストがある場合、または thinking が有効な場合)、finish_reason: stop として生のテキストで到達します。 エージェントはそれを実行しません。 - llama.cpp: https://github.com/ggml-org/llama.cpp/issues/20260 -- 文字列の前にテキストがあると <tool_call> がパースできない(peg-native パーサー) 開発中。 - llama.cpp: https://github.com/ggml-org/llama.cpp/issues/20837 -- thinking ブロック内で出力されたツール呼び出し 開発中。 - Ollama: https://github.com/ollama/ollama/issues/14745 -- まだ時々、修正後でもツール呼び出しをテキストとして出力することがある 開発中。 - vLLM: https://github.com/vllm-project/vllm/issues/35266 -- ストリーミングで、開きの { ブレースが欠落する。 https://github.com/vllm-project/vllm/issues/36769 -- パーサーで ValueError。 2. <think> タグがテキストに漏れてコンテキストを汚染する。 llama.cpp は、enable_thinking: false であっても内部的には thinking=1 を強制します。 タグがターンをまたいで蓄積し、マルチターンセッションを破壊します。 - llama.cpp: https://github.com/ggml-org/llama.cpp/issues/20182 -- b8664 でまだ未解決。 https://github.com/ggml-org/llama.cpp/issues/20409 で 27B/9B/2B の全てにまたがって確認済み。 - Ollama は未閉じの </think> のバグ(https://github.com/ollama/ollama/issues/14493)を持っており、v0.17.6 で修正済み。 3. finish_reason が間違っている。 サーバーはツール呼び出しが存在しているのに "stop" を送る。 エージェントはそれを最終回答として扱う。 4. 標準でない finish_reason。 一部のサーバーは "eos_token"、""、または null を返す。 多くのフレームワークは、ツール呼び出しが存在するかを確認する前に、未知の値でクラッシュする。 --- サーバーの状況(2026 年 4 月) ┌─────────┬─────────────────────────────────────────┬──────────────────────────────────────────────┬─────────────┐ │ │ XML パース │ think 漏れ │ finish_reas │ │ │ │ │ on │ ├─────────┼─────────────────────────────────────────┼──────────────────────────────────────────────┼─────────────┤ │ LM │ 最良のローカルオプション(https://lms │ │ ふつう │ │ Studio │ tudio.ai/changelog/lmstudio-v0.4.7 で修正) │ 改善済み │ 正しい │ │ 0.4.4 │ │ │ │ ├─────────┼─────────────────────────────────────────┼──────────────────────────────────────────────┼─────────────┤ │ vLLM │ Works(--tool-call-parser qwen3_coder)、 │ 修正済み │ ふつう │ │ 0.19.0 │ ストリーミングのバグ │ │ 正しい │ ├─────────┼─────────────────────────────────────────┼──────────────────────────────────────────────┼─────────────┤ │ Ollama │ Improved since https://github.com/ollam │ 修正済み │ 時々 │ │ 0.20.2 │ a/ollama/issues/14493、ただしまだ不安定 │ │ 間違っている │ ├─────────┼─────────────────────────────────────────────────────────┼──────────────────────────────────────────────┼─────────────┤ │ llama.c │ パーサーは存在するが、thinking で失敗 │ 壊れている(https://github.com/ggml-org/llama.cp │ 間違っている │ │ pp │ pp/issues/20182 の b8664 で発生) │ パーサー │ │ b8664 │ │ │ │ 失敗 │ └─────────┴─────────────────────────────────────────┴──────────────────────────────────────────────┴─────────────┘ --- やるべきこと Unsloth の GGUF を使う。 標準の Qwen 3.5 Jinja テンプレートには https://huggingface.co/Qwen/Qwen3.5-35B-A3B/discussions/4 (|items フィルターがツール引数で失敗する) がある。 Unsloth は 21 個のテンプレート修正を同梱している。 クライアント側でのセーフティネットを追加する。 サーバーが見落としているものを拾う、3 つの小さな関数: import re, json, uuid # 1. テキスト内容から Qwen の XML ツール呼び出しをパース def parse_qwen_xml_tools(text): results = [] for m in re.finditer(r'<function=([\w.-]+)>([\s\S]*?)</function>', text): args = {} for p in re.finditer(r'<parameter=([\w.-]+)>([\s\S]*?)</parameter>', m.group(2)): k, v = p.group(1).strip(), p.group(2).strip() try: v = json.loads(v) except: pass args[k] = v results.append({"id": f"call_{uuid.uuid4().hex[:24]}", "name": m.group(1), "args": args}) return results # 2. 漏れた think タグを取り除く def strip_think_tags(text): return re.sub(r'<think>[\s\S]*?</think>', '', re.sub(r'^<think>\s*', '', text)).strip() # 3. finish_reason を修正 def fix_stop_reason(message): has_tools = any(b.get("type") == "tool_call" for b in message.get("content", [])) if has_tools and message.get("stop_reason") in ("stop", "error", "eos_token", "", None): message["stop_reason"] = "tool_use" 互換フラグを設定(Pi SDK / OpenAI 互換クライアント): - thinkingFormat: "qwen" -- OpenAI の reasoning フォーマットではなく enable_thinking を送信する - maxTokensField: "max_tokens" -- max_completion_tokens ではない - supportsDeveloperRole: false -- developer ではなく system ロールを使う - supportsStrictMode: false -- ツールスキーマに strict: true を送らない --- モデルは賢い。壊れるのは配管(plumbing)側です。 [link] [comments]



