Вы запускаете Gemma 4 31B, задаете сложный вопрос, ждете — и получаете поток сознания, который обрывается на полуслове. Модель ушла в размышления, но забыла поставить закрывающий тег </think>. Знакомо? Это не баг модели, это баг чат-шаблона. И да, его можно исправить за минуту.
Проблема в том, что оригинальный шаблон на HuggingFace не обрабатывает специальные токены рассуждений так, как надо. В результате Gemma 4 31B при генерации текста иногда не закрывает тег <think>, и весь последующий вывод воспринимается как часть внутреннего монолога. Если вы уже сталкивались с багом чат-шаблона при вызове инструментов, то знаете — Google любят сюрпризы.
Внимание: тестировалось на Gemma 4 31B (версия GGUF, quants q4_K_M). Для других квантов может потребоваться корректировка температуры и top_p.
Анатомия проблемы: почему тег остается открытым?
Внутри Gemma 4 31B есть специальные токены для режима рассуждений: <start_thinking> и <end_thinking>. Оригинальный Jinja-шаблон в репозитории модели на HuggingFace оборачивает мысленный процесс в <think>...</think>. Но логика закрытия тега зависит от того, как модель завершает последовательность. Если сэмплинг срезает хвост — тег остается висеть.
Корень зла — в жесткой привязке шаблона к токену <end_thinking>. В llama.cpp, если генерация прерывается на середине рассуждения (например, достигнут лимит контекста или сработал стоп-токен), шаблон не вставляет закрывающий тег автоматически. Результат — битая разметка.
Тут нам приходит на помощь Jinja-шаблон от сообщества, который добавляет fallback-логику: если модель не сгенерировала закрывающий тег, шаблон делает это принудительно на уровне пост-процессинга. Решение не элегантное, но рабочее. Как говорится, «лучше костыль, чем труп».
Где взять правильный шаблон?
Идем на HuggingFace, в репозиторий google/gemma-4-31b-it. В папке chat_template лежит файл jinja_template.jinja. Но нам нужна исправленная версия.
Есть два пути:
- Вариант A: Скачать мой форк шаблона по ссылке в описании. Там уже вшита проверка на незакрытые теги.
- Вариант B: Взять оригинальный шаблон и добавить в конец конструкции условие: если последний сгенерированный токен не
</think>, то дописать его.
Лично я использую вариант B — он прозрачнее и проще кастомизируется. Ниже покажу код.
Пошаговая инструкция по исправлению
1 Находим файл шаблона в llama.cpp
В вашей сборке llama.cpp (или в LM Studio, которая использует его движок) откройте папку models. Там лежит файл gemma-4-31b-it.gguf. Рядом с ним должен быть файл chat_template.jinja. Если его нет — создайте.
Структура: ./models/gemma-4-31b-it/
2 Заменяем содержимое на исправленный Jinja-шаблон
Открываем файл в любом редакторе. Вставляем следующий код (это фикс-версия для llama.cpp 2026 года):
{%- if not add_generation_prompt is defined %}{%- set add_generation_prompt = false %}{%- endif %}
{%- set ns = namespace(found_think=false) %}
{%- for message in messages %}
{%- if message['role'] == 'user' %}
{%- if not ns.found_think %}<|start_header_id|>user<|end_header_id|>
{{ message['content'] | trim }}
<|eot_id|>{%- endif %}
{%- elif message['role'] == 'assistant' %}
{%- if message['content'].startswith('') %}
{%- set ns.found_think = true %}
{%- endif %}
<|start_header_id|>assistant<|end_header_id|>
{{ message['content'] | trim }}
<|eot_id|>
{%- endif %}
{%- endfor %}
{%- if add_generation_prompt %}
{%- if ns.found_think %}
<|start_header_id|>assistant<|end_header_id|>
{%- else %}
<|start_header_id|>assistant<|end_header_id|>
{%- endif %}
{%- endif %}
Внимание: этот шаблон корректно обрабатывает только простые диалоги. Для агентских циклов с вызовом инструментов потребуется более сложная логика.
3 Перезапускаем llama.cpp и проверяем
Запускаем сервер заново (или в LM Studio — кликаем «Reload model»). Теперь задаем вопрос, который требует рассуждений, например: «Объясни квантовую запутанность на пальцах».
Если в ответе вы видите <thinking>...</thinking> — значит, шаблон сработал, и теги закрыты корректно. Если проблема осталась — проверьте, не кэшируется ли старый шаблон.
Кстати, о кэшировании: в Qwen 3.5 был похожий баг, когда из-за неправильного шаблона страдала производительность. В Gemma 4 эта проблема менее острая, но тоже стоит быть внимательным.
Как это работает в других фронтендах?
Если вы используете LM Studio, то там шаблон подтягивается автоматически из метаданных GGUF. Но LM Studio умеет переопределять его через интерфейс: Settings -> Model -> Override chat template. Вставьте туда код из шага 2.
Для oMLX.ai — аналогично. У них поддержка кастомных Jinja-шаблонов через файл chat_template.json в директории модели. Физика та же: вставляете, перезагружаете.
Интересный момент: в LM Studio есть встроенный режим рассуждений. Мы уже писали, как его активировать. Но там он завязан на парсинг тегов на стороне интерфейса. Наш шаблон делает то же самое, но на уровне движка — надежнее.
Важно: не путайте теги <thinking> и <think>. В разных версиях Gemma 4 используются разные обозначения. Наш шаблон рассчитан на стандартный <think>.
Почему Google не исправили это сами?
Хороший вопрос. Возможно, потому что модель распространяется в основном через их API (Gemini), а для локального запуска шаблон — вторичен. Но когда ты выкачиваешь 20 гигабайт весов, а модель обрубает размышления на полуслове — это бесит. Особенно если нужно получить развернутый ответ для shell-скрипта на Android. Кстати, гайд по запуску Gemma 4 на Android в proot Linux — там как раз много таких кейсов.
Короче, сообщество снова чинит то, что не доделала корпорация. Ничего нового. Ну а мы получаем рабочую модель за 5 минут возни с Jinja.