Gemma 4 をツール呼び出しに使おうとしていましたが、多くの人と同じようにエラーが出続けていました。
ChatGPT に状況の整理を手伝ってもらいました。チャットテンプレートを渡して、いくつか異なるメッセージを試させたところ、ツール呼び出しがずっと壊れていました。ツール呼び出し自体は作れますが、結果を受け取らない(400/500 エラーでクラッシュするか、もしくはまた別のツール呼び出しを作り始めるだけ)という状態でした。ChatGPT は原因を調べるために llama.cpp のコードを見るよう提案し、いくつか探すべき箇所を教えてくれました。そこで common/chat.cpp に該当するものを見つけました。
コードを見直して、修正案を出してもらいました。すでにやってきたトラブルシューティングに基づいて、いくつか試すべきことを特定できたようです。最初の数個は直らなかったので、大量のロギングを追加しました。最終的にはうまく動くようになりました!
以下は、ChatGPT が挙げていた問題点です:
- Gemma 4 のテンプレート/ツールのフローは、通常の OpenAI っぽいフローとは違います。生の OpenAI 形式のアシスタント/ツール履歴は、パイプラインの適切なタイミングで Gemma 形式の
tool_responsesに変換する必要があります。 common_chat_templates_apply_jinja()では、ジェネリックなプロンプトの差分/generation-prompt の導出パスよりも前に、Gemma のツールレスポンス変換を行う必要がありました。common_chat_try_specialized_template()では、同じ Gemma 変換をもう一度実行してはいけません。workaround::gemma4_model_turn_builder::build()では、合成されたアシスタントメッセージに明示的な空のcontentが必要でした。- 最大の実際のクラッシュバグ:
workaround::gemma4_model_turn_builder::collect_result()で、任意の文字列のツール出力を JSON としてパースしようとしていました。これでは普通のツール結果(例:[DIR] Componentsなど)で破綻します。任意の文字列のツール出力を JSON として自動パースするのをやめて、文字列結果は文字列としてそのまま保持するようにしたところ、Gemma の継続パスが動き始めました。
build() では、チャットテンプレートで見た内容に基づいてその部分を追加しました(no content ではなく空の content が必要)。
私のテスト用プロンプトは、ツール呼び出し結果が追加された後の継続でした(User->Assistant w/ツール呼び出し->Tool result)。ツール結果はたまたま "[" で始まっていました(ディレクトリ一覧 - "[DIR] Components")が、それがある JSON パース用コードを引っ掛けました。上の collect_result() が話しているのはまさにここです。
自分のプログラムで少しだけテストしましたが動きます! Qwen3.5 でもテストしましたが、それでも動いたので、あまり壊れてはいないようです。
これは 100% ChatGPT によって生成されたコードです。Llama.cpp は AI の雑な出力コード(少なくともそうであってほしい)を嫌うかもしれませんが、それでも共有したかったです。たぶん、必要なぶんだけ llama.cpp を更新するために誰かの役に立つかもしれません。
こちらが私が作った gemma4_fix.diff です(ChatGPT のコードから作成)。誰かの助けになればと思います。差分ではなく、更新後のメソッドをそのまま投稿すべきでしたか?ちなみに、これが私の Reddit 初投稿です。
diff --git a/common/chat.cpp b/common/chat.cpp index 5b93c5887..7fb3ea2de 100644 --- a/common/chat.cpp +++ b/common/chat.cpp @@ -1729,59 +1729,60 @@ struct gemma4_model_turn_builder { } } - void collect_result(const json & curr) { - json response; - if (curr.contains("content")) { - const auto & content = curr.at("content"); - if (content.is_string()) { - // Try to parse the content as JSON; fall back to raw string - try { - response = json::parse(content.get<std::string>()); - } catch (...) { - response = content; - } - } else { - response = content; - } - } - - std::string name; - - // Match name with corresponding tool call - size_t idx = tool_responses.size(); - if (idx < tool_calls.size()) { - auto & tc = tool_calls[idx]; - if (tc.contains("function")) { - name = tc.at("function").value("name", ""); - } - } - - // Fallback to the tool call id - if (name.empty()) { - name = curr.value("tool_call_id", ""); - } - - tool_responses.push_back({{"name", name}, {"response", response}}); - } - - json build() { - collect(); - - json msg = { - {"role", "assistant"}, - {"tool_calls", tool_calls}, - }; - if (!tool_responses.empty()) { - msg["tool_responses"] = tool_responses; - } - if (!content.is_null()) { - msg["content"] = content; - } - if (!reasoning_content.is_null()) { - msg["reasoning_content"] = reasoning_content; - } - return msg; - } +void collect_result(const json & curr) { +json response; +if (curr.contains("content")) { +const auto & content = curr.at("content"); +if (content.is_string()) { +// ツール出力の生の文字列をそのまま保持する。任意のツール +// テキストは、必ずしも JSON として妥当ではありません。 +response = content.get<std::string>(); +} else { +response = content; +} +} + +std::string name; + +// 名前を対応するツール呼び出しに合わせる +size_t idx = tool_responses.size(); +if (idx < tool_calls.size()) { +auto & tc = tool_calls[idx]; +if (tc.contains("function")) { +const auto & fn = tc.at("function"); +if (fn.contains("name") && fn.at("name").is_string()) { +name = fn.at("name").get<std::string>(); +} +} +} + +// ツール呼び出し ID にフォールバック +if (name.empty()) { +name = curr.value("tool_call_id", ""); +} + +tool_responses.push_back({{"name", name}, {"response", response}}); +} + +json build() { +collect(); + +json msg = { +{"role", "assistant"}, +{"tool_calls", tool_calls}, +{"content", ""}, +}; +if (!tool_responses.empty()) { +msg["tool_responses"] = tool_responses; +} +if (!content.is_null()) { +msg["content"] = content; +} +if (!reasoning_content.is_null()) { +msg["reasoning_content"] = reasoning_content; +} +return msg; +} static bool has_content(const json & msg) { if (!msg.contains("content") || msg.at("content").is_null()) { @@ -1914,7 +1915,6 @@ std::optional<common_chat_params> common_chat_try_specialized_template( // Gemma4 format detection if (src.find("'<|tool_call>call:'") != std::string::npos) { - workaround::convert_tool_responses_gemma4(params.messages); return common_chat_params_init_gemma4(tmpl, params); } @@ -1958,14 +1958,10 @@ static common_chat_params common_chat_templates_apply_jinja(const struct common_ workaround::func_args_not_string(params.messages); } - params.add_generation_prompt = false; - std::string no_gen_prompt = common_chat_template_direct_apply_impl(tmpl, params); - params.add_generation_prompt = true; - std::string gen_prompt = common_chat_template_direct_apply_impl(tmpl, params); - auto diff = calculate_diff_split(no_gen_prompt, gen_prompt); - params.generation_prompt = diff.right; - - params.add_generation_prompt = inputs.add_generation_prompt; + const bool is_gemma4 = src.find("'<|tool_call>call:'") != std::string::npos; + if (is_gemma4) { + workaround::convert_tool_responses_gemma4(params.messages); + } params.extra_context = common_chat_extra_context(); for (auto el : inputs.chat_template_kwargs) { @@ -2005,6 +2001,24 @@ static common_chat_params common_chat_templates_apply_jinja(const struct common_ return data; } + if (is_gemma4) { + params.add_generation_prompt = inputs.add_generation_prompt; + params.generation_prompt = "<|channel>thought
<channel|>"; + + auto result = common_chat_params_init_gemma4(tmpl, params); + result.generation_prompt = params.generation_prompt; + return result; + } + + params.add_generation_prompt = false; + std::string no_gen_prompt = common_chat_template_direct_apply_impl(tmpl, params); + params.add_generation_prompt = true; + std::string gen_prompt = common_chat_template_direct_apply_impl(tmpl, params); + auto diff = calculate_diff_split(no_gen_prompt, gen_prompt); + params.generation_prompt = diff.right; + + params.add_generation_prompt = inputs.add_generation_prompt; + if (auto result = common_chat_try_specialized_template(tmpl, src, params)) { result->generation_prompt = params.generation_prompt; return *result; @@ -2187,4 +2201,3 @@ std::map<std::string, bool> common_chat_templates_get_caps(const common_chat_tem GGML_ASSERT(chat_templates->template_default != nullptr); return chat_templates->template_default->caps.to_map(); } - [リンク] [コメント]




