
На связи Олег и Камилла из команды применения больших языковых моделей ecom.tech. Сейчас мы разрабатываем сервис по генерации кода с учетом внутренних конвенций и правил. В процессе работы над этим сервисом мы часто сталкиваемся с ситуацией, когда агент сначала генерирует код, а затем пишет к нему тесты, которые хоть и выглядят правильно, но не всегда соответствуют внутренним конвенциям и покрывают необходимый функционал исходного кода.
Стоит отметить, что сами тесты живут долго, их не только пишут, но и правят, и расширяют. Единый стиль снижает когнитивную нагрузку для всех, кто потом к этим тестам вернётся. Поэтому умение писать корректные тесты является важным навыком для разработчика.
Мы задались вопросом: можно ли адаптировать маленькую LLM, например Qwen3-4B-Instruct, для генерации качественных unit-тестов для разработки бэкенда на Kotlin с учетом внутренней специфики наших котлинистов? И решили мы это сделать с помощью весьма экзотического способа дообучения LLM – эволюционного алгоритма. А потом ещё и сравнить этот алгоритм со ставшими уже классикой методами дообучения LLM: SFT и GRPO.
Как можно дообучать LLM?
Когда говорят о дообучении модели, часто смешивают цель обучения и технику обновления параметров. Порой можно услышать: «Я дообучаю модель с помощью LoRA», но это не совсем корректно. SFT и RL – это разные способы оптимизировать модель. А LoRA – это эффективный метод снижения числа обучаемых параметров при дообучении, который можно использовать, например, вместе с SFT.
RL-подходы вроде GRPO (по сути, основная рабочая лошадка для обучения всех современных LLM) имеет смысл рассматривать, когда хочется оптимизировать модель не только по эталонным ответам, но и по функции вознаграждения (reward).
В нашем случае это особенно актуально, потому что качество unit-теста нельзя полностью свести к «похожести на эталон»: нас интересует не только форма кода, но и его практическая полезность. При этом RL-сценарии чувствительны к настройке системы поощрений (reward shaping): если награда (reward) слабо связана с реальной полезностью теста, оптимизация может становиться нестабильной.
Эволюционные алгоритмы или голодные игры начинаются
В стандартных RL-методах мы исследуем пространство действий – фактически прощупываем ландшафт целевой функции через призму одной точки в пространстве параметров. А что если попробовать нечто совсем иное? Исследовать сам ландшафт параметров напрямую – сразу множеством точек и как-то агрегировать полученную информацию от каждой из них? Как раз это и предлагает сделать алгоритм Evolution Strategies (далее ES). Чтобы лучше понять разницу, стоит взглянуть на рис. 2, где θ обозначает параметры модели, которые оптимизируются в процессе обучения.
Давайте возьмём несколько (допустим, 30) экземпляров модели. Затем к каждому параметру модели независимо добавим гауссовский шум. Проделаем это с каждым экземпляром. Грубо говоря, получим 30 вариантов «возмущенной» базовой модели. Получаем то самое множество точек в пространстве.
Попросим каждую возмущенную модель дать ответ и вычислим награду. Отнормируем награды и используем их как веса при соответствующих шумах: чем выше награда, тем больший вклад данного шума в обновление. Введем константы α и σ для контроля силы и масштаба шума – аналоги механизма learning rate. Обновляем параметры базовой модели, прибавляя взвешенную сумму возмущений. Обновлённая модель становится «базовой» для следующей итерации t. Таким образом, агрегируем информацию от того самого множества точек на каждом шаге цикла обучения. Вот и все, собственно.
Псевдокод алгоритма представлен на рис. 4. Интуитивно алгоритм работает следующим образом: мы создаем несколько случайно возмущенных версий текущей модели, оцениваем их качество и сдвигаем параметры в сторону тех изменений, которые привели к лучшим результатам. Таким образом, вместо вычисления градиента, ES использует усреднение информации от множества случайных направлений в пространстве параметров.
К слову, этот класс алгоритмов вовсе не нов. Он был освещен в оригинальной работе ещё в далёком 1973 году. Затем его воскресили уже в эпоху deep learning – статья от OpenAI в 2017 году. С помощью ES они обучали роботов в среде MuJoCo, решали задачи Atari и при этом достигли результатов, сопоставимых с популярным тогда RL-методом TRPO. Размер моделей в тех экспериментах достигал всего лишь 1 млн параметров.
Авторы отметили, что ES позволяет не тащить тяжёлый груз в виде градиентов, активаций и состояния оптимизатора в памяти, что значительно снижает потребление ресурсов. Кроме того, алгоритм легко поддается распараллеливанию.
И вот настала эра LLM… и очередной этап воскрешения! На этот раз Cognizant AI Lab в 2025 году решила стряхнуть пыль с ES-алгоритмов. В статье авторы рассказывают, как с помощью ES дообучали LLM, размеры которых (в отличие от экспериментальных моделей OpenAI) достигали порядка 10 млрд параметров. При этом авторы показывают своими экспериментами, что ES позволяет успешно дообучить LLM при крайне небольшой популяции «возмущенных моделей» – всего 30 особей (для сравнения: в ранних реализациях ES использовались популяции от 10 000 и выше). Такой вот своеобразный «парадокс благословения размерности» в эволюционных алгоритмах (подробнее в этой статье).
Если говорить про практические плюсы ES, то они выходят за рамки банальной экономии ресурсов и удобной параллелизации. Во-первых, метод оказывается менее чувствительным к выбору базовой модели: там, где RL может просто не сойтись без аккуратного подбора стартовой точки, ES даёт стабильный прогресс. Во-вторых, он заметно реже страдает от reward hacking. Вместо того, чтобы находить один «читерский» способ максимизировать награду (как это часто делает RL), ES оптимизирует распределение решений, которое сложнее взломать. В-третьих, он не деградирует на длинных последовательностях. Наконец, ES проще в использовании и даёт более воспроизводимые результаты без бесконечного тюнинга и дорогих перезапусков.
В статье наиболее подробно освещаются эксперименты по задаче Countdown, где необходимо составить арифметическое выражение из заданных чисел и операций. Использовались instruct-модели от 0.5B до 7B параметров из семейств Qwen и Llama. Алгоритм ES смог обойти лучшие варианты RL на 10–20 % по accuracy.
В задаче Math reasoning ES (дообучали Qwen2.5-Math-7B) также демонстрирует сопоставимые результаты с 7B-SOTA решениями (на момент написания оригинальной статьи) или даже превосходит их.
Неплохо ES справляется и со сложными задачами-головоломками (Судоку, ARC-AGI бенчмарк).
Теперь немного о деталях реализации данного алгоритма:
1) Тензоры шума, прибавляемые к параметрам LLM, не хранятся целиком – сохраняется лишь random seed, из которого тензор можно детерминировано восстановить. Это существенно экономит память.
2) Шум добавляют и вычитают (для восстановления исходных весов) на лету, слой за слоем. Поэтому пиковое потребление GPU-памяти определяется операцией с самым большим слоем (размер тензора шума = размеру слоя).
3) Нормализацию наград на каждой итерации проводят с помощью z-оценки, что согласовывает reward-шкалу между итерациями и задачами.
4) Все «возмущенные модели» генерируют ответы в детерминированном (greedy decoding) режиме. Случайность при генерации исключена. Любое различие в качестве ответов между моделями в популяции объясняется исключительно различием весов.
Отдельно стоит отметить, что хранение шума через random seed в сочетании с greedy decoding позволяет гибко манипулировать артефактами обучения. Забыли включить сохранение чекпоинтов? Не хватило места на диске? Не беда: если логировать награды, полученные от каждой модели в популяции, можно восстановить веса дообученной ES-модели на любой итерации. Правда, для этого придётся навайбкодить написать свой код. Репозиторий авторов пока не предоставляет такой возможности «из коробки».
Важно также отметить, что исследование ES, представленное в статье, ориентировано исключительно на модели, уже прошедшие стадии pretrain. Насколько ES-методы эффективны для обучения LLM с нуля, исследователям ещё предстоит выяснить (очень на это надеемся).
Описание данных
После всей этой теории нам захотелось ответить на прикладной вопрос: что вообще произойдет в реальной задаче генерации unit-тестов для разработки бэкенда на Kotlin, если мы будем использовать ES для дообучения LLM?
На вход модель получала не просто код тестируемого класса, а полный контекст, необходимый для осмысленной генерации тестов. Предположим, нам нужно написать unit-тест для класса CarService (пример полного контекста представлен на рис. 9).
На выходе мы хотели получать готовый файл с unit-тестами. Датасет собирался в два этапа. Сначала из репозитория выделяли unit-тесты по naming patterns. Затем LLM-agent на базе qwen-code с доступом к репозиторию собрал вокруг тестируемого класса весь контекст, который нужен для корректной генерации тестов. В итоге формировались пары вида: контекст → файл с тестами. Всего удалось собрать 1500 примеров (1300 train, 200 test). Для оценки использовали две метрики: Coverage и CodeBLEU. Подробнее о них.
Метрики
Первая метрика – Coverage. В нашем эксперименте Coverage – это не runtime coverage, а functional coverage: пересечение публичных функций, покрытых в эталонном и сгенерированном тестовом файле.
То есть метрика отвечает не на вопрос «сколько строк кода реально исполнилось», а на вопрос «насколько генерация вообще попала в тот же публичный функционал, что и эталонный тестовый файл». Именно это различие для нас было критичным.
Вторая метрика – CodeBLEU, предложенная в 2020 году исследователями из Microsoft Research. В отличие от классического BLEU, который сравнивает тексты на уровне n-грамм, CodeBLEU учитывает специфику кода. Итоговый скор – это взвешенная сумма четырёх компонент: стандартного n-gram match (BLEU), взвешенного n-gram match, syntax match и dataflow match.
CodeBLEU частично наследует BLEU и учитывает совпадение n-грамм между сгенерированным кодом и эталоном. Но в отличие от стандартного BLEU, различные токены имеют разные веса: ключевые слова языка программирования (например, return, if, int) получают больший вес.
Таким образом, метрика учитывает не только совпадение токенов, но и их важность для корректности кода, см. рис. ниже:
Для учета синтаксической структуры код сначала преобразуется в абстрактное синтаксическое дерево (AST). Затем из дерева извлекаются все поддеревья, которые представляются в виде S-выражений. Сравнение проводится на уровне структуры: имена переменных и конкретные значения игнорируются, а учитывается только форма конструкции. Например, выражения return a + b и return x + y считаются эквивалентными с точки зрения синтаксиса. Синтаксический скор определяется как доля поддеревьев эталона, найденных в сгенерированном коде, см. рисунок:
Для учета семантики используется граф потока данных (Data Flow Graph, DFG), в котором вершины соответствуют переменным, а рёбра показывают, откуда переменная получает своё значение. Например, в выражении x = a + b переменная x зависит от a и b. Такие зависимости формируют структуру вычислений программы. Чтобы избежать влияния имён переменных, они нормализуются (например, var_0, var_1). Далее сравниваются зависимости между переменными в эталонном и сгенерированном коде.
Семантический скор определяется как доля совпадающих зависимостей:
Итоговый CodeBLEU представляет собой взвешенную комбинацию n-граммного, синтаксического и семантического совпадения, что позволяет учитывать как поверхностное сходство кода, так и его структуру и логику выполнения.
Одна загвоздка: оригинальный фреймворк поддерживает лишь ограниченный набор языков. Kotlin в этот клуб, увы, не приняли. К счастью, вайбокодинг в наше время помогает решить многие проблемы быстро и безболезненно! С помощью tree-sitter-kotlin внедрили поддержку Kotlin для компонент Syntax Match, Dataflow Match и добавили список ключевых слов языка.
Итак, метрика CodeBLEU отвечает на вопрос «Насколько код похож на хороший тест по форме?», а метрика Coverage: «Насколько этот тест вообще касается нужной логики?».
Для эксперимента мы использовали довольно простую reward-функцию – взвешенную сумму CodeBLEU и Coverage с весами 0.6 и 0.4 соответственно. Больший вес дали CodeBLEU, потому что он лучше отражал качество самой генерации. Coverage оставили как важную компоненту практической полезности тестов.
Запускаем эксперимент
Код проекта “Evolution Strategies at Scale” полностью открыт – эксперименты авторов можно воспроизвести и гибко адаптировать под свои нужды. Авторы активно поддерживают репозиторий и развивают решение. В частности, доступна версия с инференсом на vLLM (узкое место классического ES), которая ускоряет обучение в 10 раз по сравнению с оригинальной реализацией. Именно эту версию мы взяли за основу.
Что по железу: мы использовали кластер из 8 H100.
ES-алгоритмы только набирают популярность и пока рассматриваются скорее как экспериментальные – специализированных фреймворков и оптимизированных пайплайнов для них ещё нет. На одном GPU в процессе дообучения можно эффективно использовать лишь одну копию LLM.
Стоит отметить, что авторы на каждой итерации проходились по всему обучающему датасету (для Countdown у них было всего 200 примеров). Наш датасет содержит ~1300 примеров, поэтому ждать ответы от 30 моделей для каждого из них на 1000 итераций было невероятно затратно по времени (даже при максимальной утилизации каждого GPU). В силу этих ограничений – мы внедрили батчинг: на каждой итерации случайным образом выбирались 32 примера из обучающей выборки. Уже после 500 итераций был заметен устойчивый прирост по всем метрикам на валидационном датасете. Далее темп роста стал скромнее, но тренд на валидации продолжал идти вверх. К концу обучения CodeBLEU вырос на +21.3%, а Coverage – на +18.6% относительно базовой модели.
Неплохие результаты для экспериментального метода, правда?
Лучший результат показал ES-алгоритм: максимальный coverage (0.7381) и лучший итоговый reward.
Результаты метрик оказались выше показателей даже для Qwen3-Coder-480B!
SFT демонстрирует высокий CodeBLEU, но крайне низкий coverage – модель генерирует синтаксически корректные тесты, которые практически не покрывают логику. GRPO приводит к деградации обеих метрик, что указывает на нестабильность оптимизации в данном сетапе.
Катастрофическое забывание
Увы, за всё приходится платить… Буквально пару месяцев назад вышла статья от исследователей UC Berkeley, посвящённая побочным эффектам ES-дообучения LLM – в частности, проблеме катастрофического забывания (catastrophic forgetting) ранее усвоенных навыков. Авторы воспроизвели эксперименты с дообучением из оригинальной работы “Evolution Strategies at Scale: LLM Fine-Tuning Beyond Reinforcement Learning”, а также провели аналогичное обучение стандартным GRPO для моделей масштаба 1B и 1.5B параметров.
Цель была простой: проверить для двух методов, как изменились общие способности дообученных моделей по сравнению с базовой. Для оценки использовали бенчмарк HellaSwag – задачу на выбор логичного продолжения описания бытовой ситуации. Результаты оказались удручающими: с каждой новой итерацией ES accuracy модели на HellaSwag падала – наблюдалась значительная деградация. В то же время при обучении GRPO модель лишь немного колебалась, сохраняя свои показатели на бенчмарке. При этом accuracy на целевой задаче дообучения у GRPO была сопоставима с ES.
В чём причина? Авторы указывают на «плотный» характер обновления весов в ES.
Уже из самой формулы ES-алгоритма
видно, что на каждой итерации ненулевое изменение получает буквально каждый параметр модели. Для подтверждения своей гипотезы они анализировали разреженность и норму Фробениуса (или l2-норму) для матриц обновлений весов.
Выяснилось, что разреженность обновлений ES на каждой итерации крайне низкая (почти все веса имеют дельту обновления >10-6), а масштаб нормы изменений на несколько порядков больше, чем у GRPO. По мнению авторов, это подтверждает гипотезу о том, что градиентные методы оптимизации концентрируют изменения в подпространствах, непосредственно связанных с целевой задачей, минимизируя влияние на остальные навыки. ES же вносит глобальное смещение весов, существенно отклоняя дообученную модель от базовой, что и приводит к катастрофическому забыванию.
Мы решили проверить эти выводы на собственном эксперименте и оценили нашу ES-дообученную модель на бенчмарке GPQA – наборе научных вопросов аспирантского уровня сложности (Graduate-Level Google-Proof Q&A).
К сожалению (или к счастью), выводы подтвердились: в формате zero-shot снижение accuracy относительно базовой модели в среднем составило 2.1%, а с Five-shot CoT – 5.3%.
Причём наблюдался характерный паттерн: деградация в five-shot chain-of-thought режиме прослеживалась заметно активнее (польза от five-shot упала на 41–72%), что указывает на частичное ухудшение способности модели к in-context learning. Специализация на генерации структурированного кода, по всей видимости, сместила внутренние представления модели, ослабив способность к лаконичному и точному научному рассуждению.
Evolution Strategies – мощная альтернатива традиционному RL
И мы убедились в этом лично! На нашей собственной задаче ES показал отличный прирост метрик и те же закономерности, что и в оригинальной статье. Это доказывает главное: подход реально работает и отлично переносится на новые кейсы. Да, пока есть шероховатости, но темпы развития AI таковы, что элегантные решения текущих проблем – лишь вопрос ближайшего времени.
Конечно, если вычислительные ресурсы для вас не проблема, а малейшая деградация общих способностей модели недопустима – старый добрый RL в лице того же GRPO остается непоколебим и выдает качественные результаты. В общем, эволюция эволюционных (простите за тавтологию) алгоритмов разворачивается прямо на наших глазах. Запасаемся попкорном – самое интересное в мире AI только начинается!
А пока можно почитать статью о применении ES в alignment, в соавторах которой Xin Qiu из Cognizant AI Labs, и посмотреть стрим с ним – где он подробнее расскажет об экспериментах и поделится планами по дальнейшим исследованиям ES.
Автор: Kamilla_Z


