В эпоху, когда большие языковые модели (LLM) стали неотъемлемой частью промышленных пайплайнов, проблема «структурированного вывода» перестала быть просто академическим интересом. Это критический узел, от которого зависит работоспособность всей системы. Представьте себе сценарий: ваш бэкенд ожидает строго валидный JSON для передачи данных в базу данных или микросервис. Если модель выдает даже одну лишнюю запятую, не закрытую скобку или поле с неверным типом данных, весь пайплайн падает. До недавнего времени считалось, что для решения этой задачи необходимы модели размером в несколько миллиардов параметров, что делает их запуск дорогим и ресурсоемким.
Однако новое исследование от LiquidAI и сообщества Hugging Face бросает вызов этому утверждению. В этой статье мы подробно разберем методологию, с помощью которой удалось значительно улучшить способность небольшой модели LFM2.5-350M (всего 350 миллионов параметров) к соблюдению структурных схем. Используя алгоритм Group Relative Policy Optimization (GRPO) через библиотеку TRL, авторы достигли роста точности с 22.6% до 29.7% на бенчмарке IFStruct. И самое главное — этот процесс занимает всего около 100 шагов обучения и может быть выполнен на бесплатных GPU в Google Colab или Kaggle. Давайте погрузимся в детали этого эксперимента, разберем архитектуру наградных функций и посмотрим, как это можно применить в ваших проектах.
01Почему структурированный вывод — это отдельная задача?
Часто в бенчмарках способность модели выдавать структурированные данные (JSON, YAML, XML) «растворяется» в общих метриках рассуждений или извлечения информации. Однако для инженеров это отдельная, часто более сложная проблема. Модель может идеально понимать контекст запроса, но при этом генерировать невалидный синтаксис. Это явление известно как «schema compliance» (соответствие схеме). Если модель не может гарантировать, что вывод будет валидным и парсируемым, её интеграция в downstream-системы (системы, использующие этот вывод) становится невозможной без сложных и ненадежных пост-обработчиков.
Бенчмарк IFStruct (Structured Output Compliance Benchmark) был создан именно для того, чтобы изолировать и измерить эту способность. Он проверяет не только наличие правильных данных, но и строгое соответствие формату: наличие обертки в виде кодового блока, правильное количество элементов в списке, соответствие типов данных и отсутствие лишних полей. В этом контексте даже небольшая модель, обученная специфически под эту задачу, может превзойти более крупные модели, которые не были дообучены на структурных ограничениях.
02Подготовка окружения и базовая оценка
Прежде чем приступать к обучению, необходимо настроить среду. Гид состоит из двух частей: обучение (fine-tuning) и оценка (evaluation). Обучение требует GPU, а оценка может быть запущена локально на Mac благодаря оптимизированному серверу llama.cpp.
Для начала установим необходимые инструменты. Мы будем использовать uv для управления Python-зависимостями и llama.cpp для локального запуска модели. Установка llama.cpp через Homebrew выглядит следующим образом:
brew install llama.cpp
llama-server --versionПеред началом дообучения важно зафиксировать базовую производительность модели. Мы берем модель LFM2.5-350M в формате GGUF (BF16) и запускаем её через локальный сервер. Команда запуска сервера выглядит так:
llama-server \
-hf LiquidAI/LFM2.5-350M-GGUF:BF16 \
-c 32768 \
-np 4 \
-ngl 99 \
--alias LiquidAI/LFM2.5-350M \
--host 127.0.0.1 \
--port 8080Здесь важно обратить внимание на параметры: -ngl 99 заставляет llama.cpp выгрузить все слои на GPU (если он доступен), -c 32768 задает размер контекстного окна, а -np 4 позволяет обрабатывать четыре запроса параллельно. После запуска сервера мы проводим полный бенчмарк IFStruct на 2000 образцах:

uv run ifstruct-eval \
--model LiquidAI/LFM2.5-350M \
--base-url http://localhost:8080/v1 \
--api-key dummy \
--dataset data/test.jsonl \
--results-file results/lfm2.5-350m-llamacpp-base.json \
--n-threads 4 \
--max-tokens 2048 \
-vРезультаты базовой модели показали общий проход в 22.6% (452 из 2000). Это близко к заявленным авторами 21.1%, что подтверждает корректность нашей локальной настройки. Разбор ошибок показал, что чаще всего модель забывает обязательные поля (7228 случаев), ошибается в количестве элементов (738 случаев) или допускает несоответствие типов (540 случаев). Эти данные станут нашей точкой отсчета.
03Методология GRPO-тюнинга с использованием TRL
Сердцем этого эксперимента является алгоритм Group Relative Policy Optimization (GRPO). В отличие от классического PPO, GRPO не требует отдельной модели-критика (critic model), что значительно экономит память и вычислительные ресурсы. Вместо этого он оценивает преимущества (advantages) относительно группы сгенерированных ответов для одного и того же промпта. Это делает процесс обучения более стабильным и эффективным для небольших моделей.
Данные для обучения
Мы используем датасет nvidia/Nemotron-RL-instruction_following-structured_outputs. Он содержит пары «промпт — целевая JSON-схема — ожидаемое количество полей». Однако распределение данных в Nemotron отличается от IFStruct, поэтому мы применяем аугментацию:
- 40% данных получают инструкцию «верните вывод в закодированном блоке» (fenced code block). Это учит модель следовать формату вывода, а не просто генерировать сырой JSON.
- 20% данных преобразуются в задачи с массивом верхнего уровня. Схема оборачивается в массив с указанием требуемого количества элементов. Это учит модель генерировать «голые списки» (bare lists) и соблюдать их размер.
Всего для обучения используется около 500 образцов, что делает процесс очень легким.
Архитектура LoRA
Модель LFM2.5 использует гибридную архитектуру (аттеншн + свертки), поэтому мы настраиваем LoRA-адаптер на специфические модули. Мы обучаем только около 6 миллионов параметров (1.66% от всей модели), что позволяет избежать переобучения и сохранить общие языковые способности:
lora_config = LoraConfig(
r=16,
lora_alpha=32,
bias="none",
task_type="CAUSAL_LM",
target_modules=[
"q_proj", "k_proj", "v_proj", "out_proj",
"in_proj", "w1", "w2", "w3"
],
)Функции вознаграждения (Reward Functions)
Ключевой момент GRPO — это система наград. Мы определяем три функции, каждая из которых оценивает структуру вывода по шкале от 0 до 1:

- json_format_reward: Проверяет, является ли вывод парсируемым и в нужной форме (с кодовым блоком или без). Полный балл (1.0) за правильный формат, 0.2 за парсируемый, но неверный формат, и 0.0 за невалидный JSON.
- field_count_reward: Проверяет количество полей верхнего уровня. Точное совпадение дает 1.0, иначе балл линейно убывает в зависимости от ошибки.
- schema_validation_reward: Проверяет соответствие JSON-схеме. Учитывает нарушения ограничений и покрытие обязательных ключей.
Эти функции комбинируются с весами [1.0, 0.5, 2.0], где валидация схемы имеет наибольший вес, так как она наиболее критична для корректности данных.
04Процесс обучения и параметры
Обучение проводится всего 100 шагов. Это очень мало по меркам традиционного дообучения, но достаточно для GRPO, так как алгоритм быстро находит направление градиента, улучшающее структуру. Параметры конфигурации обучения:
from trl import GRPOConfig
training_args = GRPOConfig(
output_dir="./outputs/lfm25-350m-nemotron-schema-grpo",
learning_rate=5e-5,
max_steps=100,
warmup_steps=10,
per_device_train_batch_size=8,
gradient_accumulation_steps=4,
steps_per_generation=1,
max_completion_length=1024,
mask_truncated_completions=False,
temperature=1.1,
beta=0.01,
reward_weights=[1.0, 0.5, 2.0],
logging_steps=10,
save_steps=100
)Обратите внимание на temperature=1.1. Более высокая температура помогает сохранять разнообразие в группах генераций, что важно для оценки преимуществ в GRPO. Параметр beta=0.01 контролирует штраф KL-дивергенции, не позволяя модели слишком сильно отклоняться от базовой.
05Слияние и конвертация в GGUF
После завершения обучения мы сливаем LoRA-адаптер с базовой моделью, чтобы получить единый чекпоинт. Затем, для совместимости с нашим локальным сервером оценки, мы конвертируем модель в формат GGUF:
python llama.cpp/convert_hf_to_gguf.py \
PATH_TO_YOUR_MERGED_MODEL \
--outfile ./models/lfm25-350m-grpo-bf16.gguf \
--outtype bf16Затем запускаем сервер для дообученной модели на другом порту:
llama-server \
-m ./models/lfm25-350m-grpo-bf16.gguf \
--alias lfm25-350m-grpo-structured-output \
-c 32768 \
-np 4 \
-ngl 99 \
--host 127.0.0.1 \
--port 808106Результаты: Сравнение до и после
Запуск IFStruct на дообученной модели показал впечатляющие результаты. Общий балл вырос с 22.6% до 29.7%. Но самое интересное кроется в детализации:

- JSON формат: Процент валидных JSON-ответов вырос с 18.0% до 31.9% (+13.9 пунктов). Это прямое следствие обучения на формате вывода.
- Голые списки (Bare list): Процент валидных списков вырос с 16.6% до 29.7% (+13.1 пунктов). Модель научилась правильно обрабатывать массивы без обертки.
- YAML: Улучшение минимально (27.2% → 27.5%), так как данные для обучения были сфокусированы на JSON и списках.
Хотя 29.7% все еще ниже, чем у моделей размером в несколько миллиардов (например, Qwen3.5-2B с 33.15%), это доказывает, что даже маленькая модель может быть значительно улучшена за счет специфического RL-тюнинга. Ошибки сместились: теперь чаще встречаются проблемы с «лишними полями» (extraneous fields), что говорит о том, что модель стала более строгой к структуре, но иногда добавляет лишнюю информацию.
07Что это значит на практике
Для разработчиков, работающих с LLM в продакшене, этот эксперимент несет несколько важных посылов:
- Не игнорируйте малые модели. LFM2.5-350M или аналоги (Qwen2.5-0.5B, Phi-3-mini) могут быть отличными кандидатами для задач, где важна скорость и стоимость, если их правильно дообучить. Вам не всегда нужна модель в 7B+ параметров.
- RL-тюнинг доступен. Библиотека TRL и алгоритм GRPO делают процесс обучения с подкреплением доступным даже на бесплатных GPU. Вы можете создать свой собственный датасет из 500-1000 примеров, специфичных для вашей бизнес-логики, и улучшить модель под свои нужды.
- Структура — это не магия. Улучшение метрик структурного вывода часто требует не «больше данных», а «правильных наград». Четкое определение того, что считается ошибкой (лишнее поле, неверный тип, отсутствие ключа), и кодирование этого в reward function дает быстрый результат.
- Локальный стек. Использование llama.cpp для оценки и развертывания позволяет быстро итерировать процесс без зависимости от облачных API. Это снижает стоимость экспериментов.
В будущем мы можем ожидать появления более сложных reward-функций, учитывающих семантическую корректность данных, а не только синтаксис. Но уже сейчас, следуя этому гайду, вы можете существенно повысить надежность своих LLM-приложений, делая их вывод предсказуемым и парсируемым.
Полный код и ноутбук доступны в репозитории LiquidAI на GitHub. Мы рекомендуем клонировать репозиторий Liquid4All/ifstruct и запустить эксперимент самостоятельно, чтобы увидеть, как GRPO влияет на вашу конкретную задачу.
Источник: Hugging Face ↗
