Проблема: модели ненавидят Kotlin. Особенно тесты
Откройте любой бенчмарк на 16.04.2026. Посмотрите на результаты по Kotlin. Особенно по генерации unit-тестов. Цифры удручают. Даже свежий Qwen3-4B-Instruct, который в теории знает синтаксис, выдает тесты, которые либо не компилируются, либо проверяют не то, либо просто повторяют сигнатуру функции.
Вы пробовали промпт-инжиниринг? 'Напиши исчерпывающий тест для этой функции на Kotlin с использованием JUnit 5 и MockK'. Результат - пять вариантов одного и того же тривиального assert. Ни edge cases, ни моков, ни проверки исключений. Почему? Потому что модели обучали на Python и JavaScript. Kotlin для них - темный лес.
Классические методы дообучения здесь работают плохо. SFT (Supervised Fine-Tuning) требует идеально размеченных пар 'код-тест'. Где их взять тысячи? GRPO (Group Relative Policy Optimization) - модно, но для нишевых задач данных катастрофически не хватает. Нужен другой подход.
Эволюционный алгоритм: отбор вместо обучения
Забудьте на минуту про градиенты и лосс-функции. Представьте, что вы - божество естественного отбора для кода. Ваша модель - Qwen3-4B-Instruct. Она генерирует 100 вариантов теста для одной функции. Большинство - мусор. Но 2-3 варианта... они почти работают. Вы берете эти 'выжившие' тесты, слегка мутируете их (просите модель переписать с другими данными, другим assertion), и снова запускаете отбор. Через 10-15 'поколений' у вас появляются идеальные тесты. Это и есть эволюционный алгоритм для дообучения LLM.
Почему это работает для Kotlin? Потому что нам не нужно учить модель языку с нуля. Нужно отсеять плохие паттерны и закрепить хорошие. Модель уже 'знает' Kotlin. Прочно закрепила ошибки. Мы не переучиваем, а направляем эволюцию в нужную сторону.
1 Готовим среду и 'первобытный суп'
Берем Qwen3-4B-Instruct. На 16.04.2026 это одна из лучших 4B-моделей для кода. Не берите базовую версию - только Instruct. Загружаем в vLLM или Transformers. Нам нужна быстрая инференция.
pip install transformers torch accelerate vllm
# Или для vLLM
pip install vllm
Датасет - 50-100 реальных Kotlin функций из вашего проекта (или из открытых репозиториев). Не нужно пар 'код-тест'. Нужен только код. Функции должны быть разными: с nullable-типами, корутинами, extension-функциями, data-классами.
2 Создаем функцию приспособленности (Fitness Function)
Это сердце алгоритма. Функция, которая оценивает качество сгенерированного теста по шкале от 0 до 1. Что в нее включить:
- Компиляция: Запускаем kotlinc на тесте. Прошел компиляцию? +0.3 балла. Нет? 0.
- Запуск: Тест проходит (green)? +0.5 балла.
- Покрытие: Используем JaCoCo или Kover. Проверяем, покрывает ли тест все ветви исходной функции. Каждая покрытая ветвь +0.05 (макс +0.2).
- Стиль: Соответствие Kotlin Coding Conventions (проверяем через ktlint). +0.1 за идеальный код.
import subprocess
def fitness_function(test_code: str, original_function: str) -> float:
score = 0.0
# 1. Компиляция
compile_result = subprocess.run(
['kotlinc', '-cp', 'junit-jupiter-api-5.10.0.jar', '-script'],
input=test_code.encode(),
capture_output=True
)
if compile_result.returncode == 0:
score += 0.3
# 2. Запуск теста (упрощенно)
run_result = subprocess.run(
['kotlin', 'TestRunnerKt'],
input=test_code.encode(),
capture_output=True
)
if 'PASSED' in run_result.stdout.decode():
score += 0.5
# 3. Анализ покрытия (заглушка)
# Здесь интеграция с Kover API
return min(score, 1.0) # Максимум 1.0
Важно: функция приспособленности должна работать быстро. Компиляция Kotlin - тяжелая операция. Кешируйте результаты. Или используйте более легкие проверки сначала (синтаксический анализ), и только лучшие кандидаты отправляйте на полную компиляцию.
3 Запускаем эволюцию: мутация, кроссовер, отбор
Теперь алгоритм. Для каждой функции в датасете:
- Генерация начальной популяции: Запрашиваем у модели 20 вариантов теста. Промпт: 'Generate a comprehensive unit test for this Kotlin function using JUnit5 and MockK. Include edge cases.'
- Оценка: Каждый тест получает fitness score.
- Отбор: Выбираем top-5 тестов с наивысшим score.
- Мутация: Для каждого из top-5 создаем 3 'мутанта'. Как? Новый промпт: 'Take this Kotlin test and modify it to: a) use different test data, b) add one more assertion, c) check for a specific exception.'
- Кроссовер (опционально): Берем два хороших теста, просим модель 'смешать' их лучшие части.
- Повтор: Новая популяция = top-5 + мутанты + кроссоверы. Возвращаемся к шагу 2. 10-15 поколений.
Код основного цикла:
from vllm import LLM, SamplingParams
llm = LLM(model='Qwen/Qwen3-4B-Instruct')
def evolve_test(function_code: str, generations=10):
population = []
# Начальная популяция
prompts = [f'Write a Kotlin test for: {function_code}' for _ in range(20)]
outputs = llm.generate(prompts, SamplingParams(temperature=0.8, max_tokens=500))
population = [output.outputs[0].text for output in outputs]
for gen in range(generations):
# Оценка
scores = [fitness_function(test, function_code) for test in population]
# Отбор top-5
top_indices = np.argsort(scores)[-5:]
top_tests = [population[i] for i in top_indices]
# Мутация
mutants = []
for test in top_tests:
mutate_prompt = f'''Original test: {test}
Create 3 variations: change test data, add negative case, mock external dependency differently.'''
mutant_outputs = llm.generate([mutate_prompt], SamplingParams(n=3, temperature=0.7))
mutants.extend([out.outputs[0].text for out in mutant_outputs])
# Новая популяция
population = top_tests + mutants
# Лучший тест текущего поколения
best_test = top_tests[-1]
best_score = scores[top_indices[-1]]
if best_score > 0.9: # Достаточно хороший тест
break
return best_test, best_score
А что с SFT и GRPO? Сравниваем на практике
Параллельно мы подготовили два классических подхода. Для SFT - собрали 500 пар 'код-тест' (половина сгенерирована эволюционным алгоритмом, половина ручная). Обучили LoRA адаптер. Для GRPO - использовали реализацию от Microsoft, с reward-моделью, которая предсказывает fitness score.
| Метод | Время обучения | Качество тестов (средний fitness) | Компилируемость | Главная проблема |
|---|---|---|---|---|
| SFT (LoRA) | 4 часа на A100 | 0.65 | 78% | Переобучение на шаблоны из датасета |
| GRPO | 8 часов на A100 | 0.71 | 82% | Нестабильность, reward hacking |
| Эволюционный алгоритм | 30 мин на CPU (пакетно) | 0.88 | 96% | Вычислительно затратно на популяцию |
Эволюционный алгоритм выигрывает по качеству и не требует тонны размеченных данных. Но он медленнее на одну функцию (нужно много вызовов модели). Однако его можно распараллелить. И главное - он адаптируется под конкретную кодовую базу. SFT дает одну модель на все случаи. Эволюция - оптимальный тест для каждой функции.
Ошибки, которые всех достали (и как их избежать)
- Слишком высокая температура при мутации. Ставьте 0.6-0.7. Иначе получите синтаксический бред вместо осмысленных вариаций.
- Fitness function только проверяет компиляцию. Тогда модель быстро научится генерировать пустые тесты с правильным синтаксисом. Добавляйте метрики покрытия и качества.
- Используете старую версию Qwen3. На 16.04.2026 уже есть Qwen3.5-4B-Instruct. Берите ее. В ней исправлены баги с Kotlin и улучшена работа с инструкциями. Проверьте интеграцию с llama.cpp, если бежите на CPU.
- Эволюция застряла на локальном максимуме. Все тесты похожи, улучшений нет. Добавьте 'миграцию' - периодически добавляйте в популяцию несколько совершенно случайных тестов (высокая температура).
Что в итоге? Гибрид
Чистый эволюционный алгоритм - мощно, но дорого для production. SFT - дает обобщенную модель, но среднее качество. Правильный путь на 16.04.2026:
- Сначала запустите эволюционный алгоритм на репрезентативной выборке функций (100-200 штук).
- Соберите датасет из лучших сгенерированных тестов (пары 'код-тест').
- Дообучите Qwen3-4B-Instruct с помощью SFT на этом датасете.
- Для особо сложных функций в production используйте 'быструю эволюцию' на дообученной модели как дополнительный шаг.
Получается двухэтапный процесс: эволюция создает золотой датасет, SFT упаковывает знания в модель. Качество тестов подскакивает до 0.92-0.95 по fitness. И самое главное - модель начинает понимать, что такое 'хороший тест на Kotlin'. Не просто синтаксис. А именно инженерную практику.
P.S. Если все это кажется сложным, подождите полгода. Кто-нибудь уже завернет этот пайплайн в SaaS с кнопкой 'Сгенерировать тесты для репозитория'. Но пока вы можете сделать это сами. И получить преимущество.