Привет, Хабр! На связи Олег и Камилла из команды применения больших языковых моделей ecom.tech. Сейчас мы разрабатываем сервис по генерации кода с учётом внутренних конвенций и правил. В процессе работы часто сталкиваемся с ситуацией, когда агент сначала генерирует код, а затем пишет к нему тесты. Они выглядят корректно, но не всегда соответствуют внутренним стандартам и в полной мере покрывают нужный функционал.
Тесты живут долго: их не только пишут, но и правят, и расширяют. Единый стиль снижает когнитивную нагрузку для всех, кто к ним возвращается. Поэтому умение писать качественные тесты — важный навык для разработчика.
Мы задались вопросом: можно ли адаптировать небольшую LLM, например Qwen3-4B-Instruct, для генерации качественных unit-тестов на Kotlin с учётом специфики нашей команды? Решили попробовать сделать это с помощью эволюционного алгоритма — довольно экзотического способа дообучения. А затем сравнили его с классическими методами: SFT и GRPO.
Как можно дообучать LLM?
Часто путают цель дообучения и технику обновления параметров. Например, фраза «дообучаю модель с помощью LoRA» не совсем точна. SFT и RL — это способы оптимизации. LoRA — это эффективный метод снижения числа обучаемых параметров, который можно использовать, например, вместе с SFT.
RL-подходы, такие как GRPO (основной инструмент для обучения современных LLM), полезны, когда нужно оптимизировать модель не только по эталонным ответам, но и по функции вознаграждения (reward).
В нашем случае это особенно важно: качество unit-теста нельзя свести к «похожести на эталон». Нас интересует не только форма кода, но и его практическая полезность. Однако RL чувствителен к настройке reward-функции. Если награда слабо связана с реальной ценностью теста, оптимизация может стать нестабильной.
Эволюционные алгоритмы: голодные игры для моделей
В стандартных RL-методах мы исследуем пространство действий через одну точку — параметры текущей модели. А что, если исследовать всё пространство сразу — множеством точек и агрегировать результаты? Именно это предлагает алгоритм Evolution Strategies (ES).
Возьмём, например, 30 копий модели. К каждому параметру добавим гауссовский шум — получим 30 «возмущённых» версий. Каждая модель генерирует ответ, за который начисляется награда.
Награды нормализуются и используются как веса при обновлении базовой модели: чем выше награда, тем сильнее вклад соответствующего шума. Параметры базовой модели обновляются с учётом взвешенной суммы возмущений. Обновлённая модель становится новой базовой — и цикл повторяется.
Интуитивно: мы создаём множество случайных версий модели, оцениваем их, и двигаемся в сторону тех, что показали лучшие результаты. Вместо градиента ES использует усреднение по множеству направлений в пространстве параметров.
Этот подход не нов. Его впервые описали ещё в 1973 году. В 2017 году OpenAI возродил ES для обучения агентов в MuJoCo и Atari, достигнув результатов, сопоставимых с TRPO. Тогда модели были небольшими — до 1 млн параметров.
Преимущества ES: не нужно хранить градиенты, активации и состояние оптимизатора. Это снижает потребление памяти и упрощает распараллеливание.
В 2025 году Cognizant AI Lab вернулся к ES, применив его к LLM с 10 млрд параметров. При этом оказалось, что достаточно всего 30 «особей» — в отличие от тысяч в ранних реализациях. Это своего рода «парадокс благословения размерности».
Практические преимущества ES
ES даёт не только экономию ресурсов и простоту параллелизации. Он также:
- Менее чувствителен к выбору стартовой модели: там, где RL может не сойтись, ES стабильно прогрессирует.
- Реже страдает от reward hacking: вместо поиска «читерского» способа максимизировать награду, ES оптимизирует распределение решений, которое сложнее взломать.
- Не деградирует на длинных последовательностях.
- Проще в использовании: требует меньше тюнинга и даёт более воспроизводимые результаты.
В задаче Countdown (составление арифметического выражения) ES превзошёл лучшие RL-методы на 10–20% по accuracy. В Math reasoning (на базе Qwen2.5-Math-7B) он показал результаты на уровне SOTA или даже лучше. Хорошо справляется и с головоломками вроде Судоку и ARC-AGI.
Технические детали реализации
Ключевые особенности реализации ES:
- Шум к параметрам не хранится целиком — сохраняется только random seed, из которого его можно восстановить. Это экономит память.
- Шум добавляется и вычитается слой за слоем. Пиковое потребление GPU-памяти определяется размером самого большого слоя.
- Награды нормализуются с помощью z-оценки, что стабилизирует шкалу между итерациями.
- Все «возмущённые модели» работают в режиме greedy decoding — без случайности. Различия в ответах объясняются исключительно различиями в весах.
Хранение шума через seed и детерминированная генерация позволяют восстановить модель на любой итерации, если логировать награды. Правда, для этого нужно написать свой код — «из коробки» такая функция пока не поддерживается.
Важно: ES в статье применяется только к моделям, уже прошедшим pretrain. Насколько он эффективен при обучении с нуля — предстоит выяснить.
Описание данных
Мы решили проверить ES на реальной задаче: генерации unit-тестов для бэкенда на Kotlin.
На вход модель получала не просто код класса, а полный контекст, необходимый для осмысленной генерации тестов. Например, для класса CarService включались все зависимые классы, интерфейсы и конфигурации.
Датасет собирали в два этапа:
- Выделяли unit-тесты по naming patterns.
- LLM-агент собирал вокруг тестируемого класса полный контекст.
Получились пары: контекст → файл с тестами. Всего — 1500 примеров (1300 train, 200 test).
Для оценки использовали две метрики: Coverage и CodeBLEU.
Coverage — это functional coverage: пересечение публичных функций, покрытых в эталонном и сгенерированном тестах. Метрика показывает, насколько генерация затрагивает нужный функционал.
CodeBLEU — адаптация BLEU для кода. Учитывает:
- Совпадение n-грамм (с весами: ключевые слова важнее).
- Синтаксическую структуру через AST (игнорируя имена переменных).
- Поток данных (DFG) — зависимости между переменными.
Итоговый скор — взвешенная сумма этих компонент. CodeBLEU отвечает на вопрос: «Насколько код похож на хороший тест по форме?», а Coverage — «Касается ли он нужной логики?».
Поскольку оригинальный фреймворк не поддерживал Kotlin, мы добавили поддержку через tree-sitter-kotlin — внедрили синтаксический и семантический анализ, а также список ключевых слов.
Reward-функция — взвешенная сумма CodeBLEU (вес 0.6) и Coverage (вес 0.4). Больший вес у CodeBLEU, так как он лучше отражает качество генерации.
Запускаем эксперимент
Мы использовали открытый репозиторий Evolution Strategies at Scale. Взяли версию с инференсом на vLLM — она ускоряет обучение в 10 раз.
Аппаратная база: кластер из 8 GPU H100.
ES пока не имеет специализированных фреймворков. На одном GPU можно эффективно использовать только одну копию модели.
Авторы оригинальной статьи проходили по всему датасету на каждой итерации. У нас — 1300 примеров, 30 моделей, 1000 итераций. Это слишком дорого. Поэтому мы внедрили батчинг: на каждой итерации выбирали случайные 32 примера.
Уже после 500 итераций наблюдался устойчивый рост метрик на валидации. К концу обучения:
- CodeBLEU вырос на +21.3%,
- Coverage — на +18.6%.
Лучший результат показал ES: максимальный coverage (0.7381) и лучший итоговый reward. Результаты превзошли даже Qwen3-Coder-480B.
SFT показал высокий CodeBLEU, но низкий coverage: модель генерирует синтаксически правильные, но бесполезные тесты. GRPO привёл к деградации обеих метрик — оптимизация оказалась нестабильной.
Катастрофическое забывание
Недавно исследователи из UC Berkeley выявили серьёзный побочный эффект ES — катастрофическое забывание. При дообучении модель теряет ранее усвоенные навыки.
В эксперименте сравнивали ES и GRPO на моделях 1B–1.5B параметров. Оценивали по бенчмарку HellaSwag — выбор логичного продолжения бытовой ситуации.
Результаты:
- При обучении ES accuracy на HellaSwag постоянно падала.
- GRPO сохранял уровень, лишь слегка колеблясь.
При этом на целевой задаче GRPO показал результат, сопоставимый с ES.
Причина — в «плотности» обновлений. В ES каждый параметр модели обновляется на каждой итерации. Анализ показал: разреженность обновлений у ES крайне низкая, а норма изменений — на порядки выше, чем у GRPO.
Градиентные методы концентрируют изменения в подпространствах, связанных с задачей. ES же вносит глобальное смещение, сильно отклоняя модель от исходной — отсюда и потеря общих навыков.
Мы проверили это на своей модели, оценив её по бенчмарку GPQA — сложным научным вопросам уровня аспирантуры.
Результаты подтвердились:
- Zero-shot: снижение accuracy на 2.1%.
- Five-shot CoT: падение на 5.3%.
При этом польза от five-shot упала на 41–72%, что указывает на ухудшение in-context learning. Похоже, специализация на генерации кода сместила внутренние представления модели, ослабив её способность к научным рассуждениям.
Evolution Strategies — мощная альтернатива RL
Мы лично убедились: на нашей задаче ES дал отличный прирост метрик и повторил закономерности из оригинальной статьи. Подход работает и хорошо переносится на новые кейсы.
Да, есть шероховатости — в первую очередь, катастрофическое забывание. Но темпы развития AI таковы, что элегантные решения этих проблем — вопрос ближайшего времени.
Если ресурсы не ограничены, а сохранение общих навыков критично — GRPO остаётся надёжным выбором. Но эволюция эволюционных алгоритмов идёт полным ходом. Запасайтесь попкорном — самое интересное только начинается.