SBSE на практике и связь с современными ИИ-подходами
Восемь предыдущих статей трека были про механику: как закодировать решение, как посчитать фитнес, как устроены отжиг, генетические алгоритмы, NSGA-II, муравьи, EvoSuite и автоматический ремонт программ. Всё это работает в лаборатории. Вопрос, ради которого писался трек, другой: что из этого действительно доехало до продакшена, как это туда встроить у себя и что произошло с областью после того, как в неё пришли большие языковые модели.
Короткий ответ на последний вопрос, чтобы он не висел до середины статьи: LLM не убили SBSE. Они заняли в нём конкретное место — место оператора порождения кандидатов. А фитнес-функция, которая отделяет удачного кандидата от правдоподобного мусора, никуда не делась и стала важнее, чем была: чем дешевле генерация, тем дороже верификация как узкое место.
Часть 1. Что реально работает в продакшене
Промышленный ландшафт: карта внедрений
Начнём с фактов, а не с обещаний. Ниже — внедрения, о которых есть публичные инженерные отчёты, а не только академические статьи на бенчмарках.
| Компания / проект | Задача | Метод | Где почитать |
|---|---|---|---|
| Meta, Sapienz | генерация E2E-тестов для Android | многокритериальный поиск (NSGA-II-подобный) по последовательностям UI-событий | Mao, Harman, Jia, ISSTA 2016 |
| Meta, SapFix | автоматический патч для крэшей, найденных Sapienz | шаблоны + мутационный поиск, верификация тестами | Marginean et al., ICSE-SEIP 2019 |
| Meta, TestGen-LLM | доусиление существующих юнит-тестов | LLM-генерация + детерминированные фильтры | Alshahwan et al., 2024, arXiv:2402.09171 |
| Google, OSS-Fuzz / ClusterFuzz | непрерывный поиск уязвимостей | покрытийно-направленный фаззинг (эволюционный по сути) | google.github.io/oss-fuzz |
| Google, MLGO | инлайнинг и распределение регистров в LLVM | обученная политика вместо ручной эвристики | arXiv:2101.04808 |
| Apache TVM, AutoTVM / Ansor | подбор расписаний тензорных вычислений | поиск по пространству расписаний + суррогатная модель стоимости | Ansor, OSDI 2020 |
| Halide | авто-планировщик графики/вычислений | поиск с обученной моделью стоимости | Adams et al., SIGGRAPH 2019 |
| MIT, OpenTuner | универсальный автотюнинг программ | ансамбль поисковых алгоритмов с bandit-распределением бюджета | opentuner.org |
| Google Brain, AmoebaNet | архитектура нейросети | регуляризованная эволюция | Real et al., AAAI 2019 |
| DeepMind, AlphaDev | машинные коды сортировки в libc++ | RL-поиск в пространстве ассемблерных инструкций | Nature, 2023 |
Обратите внимание на закономерность: промышленный SBSE почти всегда прячется за фасадом обычного инструмента. Никто не продаёт «генетический алгоритм». Продают «автоматический тюнер компилятора», «генератор тестов», «фаззер». Пользователь не должен знать, что внутри турнирная селекция.
Sapienz: канонический пример, разобранный по косточкам
Sapienz — самая поучительная история трека, потому что там видны все компромиссы.
Задача. Найти краши в Android-приложении Facebook/Instagram/Messenger до того, как их найдут пользователи. Пространство поиска — последовательности UI-событий (тапы, свайпы, ввод текста).
Три ингредиента (напомню постановку из первой статьи):
- Представление — хромосома = набор из нескольких коротких последовательностей событий (тест-сьют, а не отдельный тест: оптимизируется весь набор целиком).
- Фитнес — три цели одновременно: максимум покрытия, максимум числа найденных крашей, минимум длины последовательности. Третья цель — не эстетика: инженеру, которому прилетел баг-репорт, нужен воспроизводимый сценарий из 8 шагов, а не из 300.
- Операторы — мутация событий и кроссовер между сьютами, поверх — Парето-отбор (многокритериальность).
Что было главной инженерной болью. Не алгоритм. Одна оценка фитнеса = запуск приложения на эмуляторе или реальном устройстве, это десятки секунд. Популяция в 100 особей × 100 поколений = 10 000 запусков. Поэтому Sapienz — это в первую очередь распределённая ферма устройств и умный шедулер, и только во вторую очередь эволюционный алгоритм.
Эта картинка — самый практичный слайд всего трека. Стоимость одной оценки определяет вообще всё: размер популяции, выбор алгоритма, необходимость суррогатной модели, нужен ли вам кластер. Слева от миллисекунды можно позволить себе наивный GA. Правее секунды каждый вызов фитнеса надо защищать кэшом, фильтром и, по возможности, дешёвым приближением.
Финальный штрих, который часто забывают. Sapienz был бы бесполезен, если бы просто складывал краши в отчёт. Его встроили в процесс ревью: найденный краш автоматически превращается в задачу, привязанную к конкретному диффу, который его вызвал. Ценность SBSE-инструмента = найденное решение × вероятность, что человек с ним что-то сделает. Второй множитель — это UX и интеграция, не алгоритмы.
Genetic Improvement: улучшаем код, который уже работает
Отдельная ветка, которая ближе всего к «магии» и при этом хорошо изучена, — Genetic Improvement (GI): берём работающую программу и эволюционируем её исходник ради не-функционального свойства (скорость, память, энергопотребление), удерживая функциональность тестами.
Это генетическое программирование, применённое не к пустому месту, а к существующему коду: операторы работают на уровне строк/AST-узлов реальной программы — удалить строку, скопировать строку отсюда сюда, заменить оператор сравнения.
Классические результаты — у Уильяма Лэнгдона и Марка Хармана, «Optimizing Existing Software with Genetic Programming», IEEE TEC 2015: на реальных биоинформатических пакетах (Bowtie2, BarraCUDA) получены ускорения в разы и на порядки для специализированных версий — ценой отказа от общности, которая пользователю была не нужна.
Ключевая интуиция, почему это вообще возможно:
Реальный код содержит огромное количество «мягких» решений — общность, которая не используется, проверки, которые всегда истинны, параметры, подобранные наугад в 2011 году. Человек не станет их вычищать, потому что это скучно и рискованно. Поиск с тестами в роли страховки — станет.
Инструмент, с которого стоит начинать эксперименты, — Gin (Brownlee et al., GECCO 2019) для Java и PyGGI для нескольких языков.
где 90% времени ВыборГорячейЗоны --> ПостроениеПатча: мутация строк/AST
только внутри зоны ПостроениеПатча --> ФункциональныйФильтр: прогон тестов ФункциональныйФильтр --> ПостроениеПатча: тесты упали —
патч отброшен ФункциональныйФильтр --> ЗамерПроизводительности: тесты зелёные ЗамерПроизводительности --> ПостроениеПатча: не быстрее —
назад в поиск ЗамерПроизводительности --> РевьюЧеловеком: быстрее на X% РевьюЧеловеком --> [*]: принят в мастер РевьюЧеловеком --> ПостроениеПатча: непонятный код,
переискать
Заметьте роль последнего состояния: человек как финальный фильтр — часть алгоритма, а не вежливость. Патч, который ускоряет на 4 %, но который никто не может прочитать, в продакшене хуже, чем его отсутствие.
Часть 2. Как внедрить SBSE у себя
Где искать задачу: тест на пригодность из четырёх вопросов
Прежде чем писать хоть строчку кода, прогоните кандидата-задачу через четыре вопроса. Если хоть один ответ «нет» — не начинайте, вы потратите квартал.
- Вариантов действительно много? Если решений сотни — переберите их. Полный перебор, который отработал за ночь, всегда лучше эвристики, которая «наверное, нашла хорошее».
- Качество готового решения измеряется автоматически и быстро? Не «мы попросим экспертов оценить», а число, которое выдаёт скрипт. Это ключевая асимметрия SBSE: сделать трудно — проверить легко.
- Фитнес градуирован? Если функция выдаёт только 0 и 1 («тесты прошли / упали»), поиску не за что зацепиться, ландшафт — плато с иголками. Нужны промежуточные сигналы: branch distance, процент покрытия, число нарушенных ограничений.
- Есть бюджет на 10³–10⁶ оценок? Умножьте стоимость одной оценки на это число. Получилось «три недели на кластере ради экономии двух дней разработчика»? Закрывайте.
с большим числом вариантов"] --> B{"Качество решения
меряется скриптом?"} B -- нет --> X1["Не SBSE.
Сначала постройте метрику"] B -- да --> C{"Метрика градуирована,
а не 0/1?"} C -- нет --> C2["Добавьте промежуточные сигналы:
branch distance, штрафы,
частичное покрытие"] C2 --> D C -- да --> D{"Стоимость одной оценки
× 10⁴ влезает в бюджет?"} D -- нет --> E["Нужны: кэш, параллелизм,
суррогатная модель
или другая задача"] D -- да --> F["Запускаем случайный поиск
как baseline"] E --> F F --> G{"Случайный поиск
уже решает задачу?"} G -- да --> X2["Отлично. Оставьте случайный поиск,
это дёшево и понятно"] G -- нет --> H["Локальный поиск / GA / NSGA-II
по характеру задачи"] H --> I["Сравнение с baseline
на 30 запусках + статтест"] I --> J["Интеграция в CI и UX результата"]
Отдельно подчеркну шаг «случайный поиск как baseline». Это не формальность, а самая частая причина провала SBSE-проектов: команда полгода тюнит генетический алгоритм и не знает, что случайная генерация даёт 95 % того же результата за 1 % усилий. В статье про фитнес разбиралось, почему так бывает: если ландшафт плоский, ни один умный алгоритм не поможет — поможет только переделка представления.
Архитектура промышленного поискового сервиса
Реальный SBSE-инструмент в компании выглядит примерно так — и 80 % кода тут не про алгоритм.
новый дифф"] CRON["Ночной прогон
по всему репозиторию"] MAN["Ручной запуск
инженером"] end subgraph Core["Ядро поиска"] ORCH["Оркестратор:
бюджет, seed, рестарты"] ALG["Поисковый алгоритм
GA / NSGA-II / отжиг"] CACHE[("Кэш оценок
hash решения → фитнес")] end subgraph Eval["Ферма оценки"] W1["Воркер 1
sandbox"] W2["Воркер 2
sandbox"] WN["Воркер N
sandbox"] end subgraph Out["Выход"] FILT["Фильтры валидности:
компиляция, стабильность,
прирост метрики"] PR["Автоматический PR
с объяснением"] DASH["Дашборд качества:
тренд метрики по времени"] end CI --> ORCH CRON --> ORCH MAN --> ORCH ORCH --> ALG ALG <--> CACHE ALG --> W1 ALG --> W2 ALG --> WN W1 --> ALG W2 --> ALG WN --> ALG ALG --> FILT FILT --> PR FILT --> DASH
Что здесь критично и обычно недооценивается:
- Кэш оценок. Популяционные алгоритмы переоценивают одни и те же решения десятки раз (элита переживает поколения, кроссовер порождает дубликаты). Хеш решения → фитнес экономит 20–60 % бюджета бесплатно. Единственное требование — детерминированный фитнес, иначе кэш начнёт врать; при недетерминированном (flaky-тесты) кэшируйте усреднение по k прогонам.
- Sandbox для оценки. Если фитнес = запуск сгенерированного кода, вы запускаете враждебный код:
бесконечные циклы,
System.exit, запись в файлы, сетевые вызовы. Таймаут, отдельный процесс, ограничение прав — не опция. - Фиксированный seed и воспроизводимость. Инженер, которому прилетел странный патч, обязан уметь повторить прогон. Логируйте seed, версию инструмента, коммит.
- Бюджет как первоклассная сущность. Останов по времени/числу оценок, а не по «сходимости». В CI у вас есть 12 минут, и алгоритм обязан отдать лучшее найденное в момент истечения бюджета — все алгоритмы трека это умеют, они anytime.
Оценка результата: без статистики результата нет
Поисковые алгоритмы стохастические. «Мы запустили и получили 87 % покрытия» — не результат, а анекдот. Каноническое руководство — Arcuri & Briand, «A Hitchhiker’s Guide to Statistical Tests for Assessing Randomized Algorithms in Software Engineering», STVR 2014. Минимальный протокол:
- ≥ 30 независимых запусков каждой конфигурации с разными seed’ами.
- Тест Манна–Уитни (U-test) — непараметрический, не требует нормальности. Проверяем, что разница между вашим алгоритмом и baseline не случайна.
- Величина эффекта Варги–Дилейни $\hat{A}_ {12}$ — вероятность того, что случайный запуск A лучше случайного запуска B. $\hat{A}_ {12} = 0.5$ — алгоритмы неразличимы; 0.56 / 0.64 / 0.71 — малый / средний / большой эффект. p-value без величины эффекта бессмысленно: на 1000 запусках статистически значимым станет улучшение на 0.1 %.
- Одинаковый бюджет, измеренный в оценках фитнеса, а не в секундах — иначе вы сравниваете качество реализаций, а не алгоритмов.
"""Минимальный корректный протокол сравнения двух стохастических алгоритмов."""
from statistics import mean
from scipy.stats import mannwhitneyu
def a12(xs: list[float], ys: list[float]) -> float:
"""Величина эффекта Варги–Дилейни: P(x > y) + 0.5 * P(x == y).
Возвращает вероятность, что случайный результат из xs лучше случайного из ys
(в предположении «больше — лучше»). Сложность O(n*m); для n,m ~ 30 это ничто.
"""
more = sum(1 for x in xs for y in ys if x > y)
same = sum(1 for x in xs for y in ys if x == y)
return (more + 0.5 * same) / (len(xs) * len(ys))
def compare(name_a: str, xs: list[float], name_b: str, ys: list[float]) -> None:
stat, p = mannwhitneyu(xs, ys, alternative="two-sided")
effect = a12(xs, ys)
label = "пренебрежимая"
for threshold, text in ((0.71, "большая"), (0.64, "средняя"), (0.56, "малая")):
if abs(effect - 0.5) + 0.5 >= threshold:
label = text
break
print(f"{name_a}: медиана {sorted(xs)[len(xs) // 2]:.3f}, среднее {mean(xs):.3f}")
print(f"{name_b}: медиана {sorted(ys)[len(ys) // 2]:.3f}, среднее {mean(ys):.3f}")
print(f"p-value = {p:.5f}, A12 = {effect:.3f} ({label} разница)")
# 30 запусков — минимум, который позволяет говорить хоть что-то
# ga_runs = [run_ga(seed=s).coverage for s in range(30)]
# rand_runs = [run_random(seed=s).coverage for s in range(30)]
# compare("GA", ga_runs, "Random", rand_runs)
Практическое правило: если ваш алгоритм не бьёт случайный поиск с $\hat{A}_ {12} \geq 0.64$ при равном бюджете — у вас нет результата, есть шум и надежда.
Часть 3. Поиск и обучение: две парадигмы
Теперь — главная тема второй половины статьи. Чтобы говорить про LLM осмысленно, нужно чётко развести две вещи, которые часто валят в одну кучу под словом «ИИ».
| Поиск (SBSE) | Обучение (ML) | |
|---|---|---|
| Что дано | функция оценки качества | набор примеров |
| Что ищем | конкретное хорошее решение этой задачи | модель, обобщающая на новые задачи |
| Знание берётся из | вычислений в момент запуска | данных, собранных заранее |
| Результат | артефакт (тест, патч, расписание) | предиктор |
| Гарантии | ровно те, что проверяет фитнес | статистические, на распределении обучения |
| Стоимость | вычисления на каждую задачу | обучение один раз, вывод дёшев |
| Слабое место | плоский/обманчивый ландшафт | нет данных / сдвиг распределения |
Классический SBSE — чистый поиск: он ничего не знает про мир, кроме фитнес-функции, и каждый запуск начинает с нуля. Именно поэтому он универсален и одновременно туп: сколько бы раз EvoSuite ни генерировал тесты для Java-классов, в тысячный раз он не станет умнее, чем в первый.
Классический ML — чистое обучение: он не проверяет свои ответы, он их предсказывает. Поэтому он мгновенен и одновременно ненадёжен: правдоподобие ≠ правильность.
Все интересные системы последних лет живут посередине и устроены по одному и тому же принципу: обучение предлагает, поиск проверяет.
Правый нижний угол — то, чем чаще всего пользуются на практике «просто попросил модель написать тест». Правый верхний — то, что работает в проде. Разница между ними — ровно фитнес-функция, то есть содержание всего этого трека.
Как это выглядело исторически
Обратите внимание на структуру: каждая волна не отменяла предыдущую, а заменяла один блок в той же схеме «породить → оценить → отобрать». Сначала улучшали отбор, потом — оценку (суррогаты), теперь — порождение.
Часть 4. LLM как оператор мутации
Идея
Вернёмся к трём ингредиентам SBSE. Оператор мутации в генетическом программировании — это, по сути,
случайное изменение кода: заменить узел дерева, удалить строку, поменять < на <=.
Такая мутация семантически слепа: она не знает, что такое цикл, что такое null-проверка,
что вообще делает программа. Отсюда чудовищная неэффективность классического GP на реальном коде:
99.9 % потомков не компилируются или ломают всё.
LLM, обученная на миллиардах строк кода, — это оператор мутации с сильным априорным знанием о том, как выглядит правдоподобный код. Просим: «вот функция, вот её результат на бенчмарке, предложи изменённую версию» — получаем кандидата, который с высокой вероятностью хотя бы компилируется и делает что-то осмысленное.
Первая явная формулировка — «Evolution through Large Models» (Lehman et al., 2022, arXiv:2206.08896): LLM, дообученная на диффах, используется как оператор мутации в эволюционном цикле.
Дальше — FunSearch (Romera-Paredes et al., Nature, 2024): эволюционируются не решения задачи, а программы, которые строят решения; фитнес — объективный скоринг результата программы. Так были получены новые результаты в задаче cap set и в задаче об укладке контейнеров. Схема:
(островная популяция) participant S as Сэмплер промптов participant L as LLM participant E as Песочница-оценщик participant DB as Отбор P->>S: выбрать 2 программы
из одного острова S->>L: промпт: «вот v0 и v1 (лучше),
напиши v2» L-->>E: программа-кандидат E->>E: компиляция + запуск
с таймаутом alt упало / таймаут / хуже E-->>DB: отбросить else валидна E-->>DB: скор от объективной функции DB->>P: положить на остров
по кластеру сигнатуры end Note over P,DB: периодически: сброс слабых островов,
заселение копиями лучших
Здесь узнаётся всё, о чём был трек: островная модель для
поддержания разнообразия, элитизм, рестарты. Новое — только то, что вместо random.choice(operators)
стоит вызов модели.
AlphaEvolve (DeepMind, 2025) довёл идею до системы: ансамбль моделей, эволюция целых файлов, автоматически конструируемые промпты с историей улучшений, каскад оценщиков (быстрая грубая проверка → дорогая точная). Результаты — улучшенные алгоритмы умножения матриц и реальная экономия вычислительных ресурсов в дата-центрах Google.
Что меняется в дизайне алгоритма
Это самый практичный раздел статьи. Если вы заменяете мутацию на LLM, наивный GA ломается — меняется экономика.
| Параметр | Классический GP | GP с LLM-оператором |
|---|---|---|
| Стоимость одной мутации | ~1 мкс, бесплатно | 1–5 с, деньги за токены |
| Бюджет оценок | 10⁵–10⁷ | 10²–10⁴ |
| Размер популяции | 100–1000 | 5–50 |
| Доля валидных потомков | 1–20 % | 50–90 % |
| Главное узкое место | качество ландшафта | стоимость и латентность генерации |
| Что тюним | вероятности операторов | промпт, контекст, температура, ансамбль моделей |
| Риск | застревание в локальном оптимуме | схлопывание разнообразия (все ответы похожи) |
Три следствия, каждое из которых стоит инженеру недели, если понять его на своём опыте:
- Кэшируйте агрессивно. Одинаковый промпт → одинаковый ответ при температуре 0. Дедупликация кандидатов по нормализованному AST экономит больше, чем любой тюнинг.
- Разнообразие теперь дороже интенсификации. Классический GA борется с преждевременной сходимостью мутацией. LLM сходится ещё быстрее: она возвращает «самое вероятное» решение, и вся популяция становится вариациями одной идеи. Лечится островами, высокой температурой на части запросов, явным требованием «предложи принципиально другой подход» и MAP-Elites-подобным поддержанием разнообразия по признакам решения.
- Контекст — это тоже оператор. Что вы кладёте в промпт (только код? код + фитнес? код + история улучшений + провалившийся тест + трассировка?) влияет на качество сильнее, чем выбор между турнирной и рулеточной селекцией.
Рабочий пример: эволюционный цикл с подключаемым оператором
Ниже — компактный, но честный каркас: тот же GA, что в статье про генетические алгоритмы, но с абстрагированным оператором порождения, кэшем, бюджетом и островами. LLM-оператор — стаб с явным интерфейсом: подставьте свой клиент, логика вокруг не изменится.
"""Эволюция программ с подключаемым оператором порождения.
Одинаково работает со случайной мутацией и с LLM-оператором:
меняется только реализация Operator.propose.
"""
from __future__ import annotations
import hashlib
import random
from dataclasses import dataclass, field
from typing import Callable, Protocol
@dataclass(order=True)
class Individual:
score: float
code: str = field(compare=False)
parent: str | None = field(default=None, compare=False)
class Operator(Protocol):
"""Порождает нового кандидата из одного-двух родителей."""
def propose(self, parents: list[Individual]) -> str: ...
class RandomLineMutation:
"""Классический GI-оператор: удалить / продублировать / заменить строку.
Стоимость ~1 мкс, доля валидных потомков низкая — компенсируем количеством.
"""
def __init__(self, rng: random.Random) -> None:
self.rng = rng
def propose(self, parents: list[Individual]) -> str:
lines = parents[0].code.splitlines()
if len(lines) < 2:
return parents[0].code
i = self.rng.randrange(len(lines))
action = self.rng.choice(("delete", "duplicate", "swap"))
if action == "delete":
del lines[i]
elif action == "duplicate":
lines.insert(i, lines[i])
else:
j = self.rng.randrange(len(lines))
lines[i], lines[j] = lines[j], lines[i]
return "\n".join(lines)
class LLMMutation:
"""LLM как семантически осведомлённый оператор мутации.
Ключевые отличия от RandomLineMutation:
* дорого (секунды + деньги) — значит, вызовов на порядки меньше;
* в промпт кладём НЕ только код, но и обратную связь фитнеса —
модель должна видеть, куда двигаться;
* два родителя разного качества играют роль «кроссовера»:
показываем хуже/лучше и просим продолжить тренд.
"""
PROMPT = (
"Ниже две версии функции. Версия B получила оценку {sb:.4f}, "
"версия A — {sa:.4f} (больше — лучше).\n\n"
"### Версия A\n```python\n{a}\n```\n\n"
"### Версия B\n```python\n{b}\n```\n\n"
"Замечание оценщика по версии B: {feedback}\n\n"
"Напиши версию C: сохрани сигнатуру, улучши оценку. "
"Верни только код без пояснений."
)
def __init__(self, client: Callable[[str], str], feedback: Callable[[Individual], str]) -> None:
self.client = client # функция промпт -> текст ответа
self.feedback = feedback # человекочитаемая диагностика для промпта
def propose(self, parents: list[Individual]) -> str:
a, b = sorted(parents[:2], key=lambda ind: ind.score)[:2] if len(parents) > 1 else (parents[0], parents[0])
prompt = self.PROMPT.format(
a=a.code, b=b.code, sa=a.score, sb=b.score, feedback=self.feedback(b)
)
return extract_code_block(self.client(prompt))
def extract_code_block(text: str) -> str:
"""Достаём первый fenced-блок; если его нет — считаем ответ целиком кодом."""
if "```" not in text:
return text.strip()
body = text.split("```", 2)[1]
return body.split("\n", 1)[1].strip() if "\n" in body else ""
class CachedFitness:
"""Оценка с кэшем и жёстким учётом бюджета.
Кэш корректен только для детерминированного фитнеса. Для flaky-оценки
кэшируйте усреднение по k прогонам, иначе кэш начнёт закреплять случайную удачу.
"""
def __init__(self, evaluate: Callable[[str], float], budget: int) -> None:
self.evaluate = evaluate
self.budget = budget
self.used = 0
self.hits = 0
self._cache: dict[str, float] = {}
def __call__(self, code: str) -> float:
key = hashlib.sha256(code.encode("utf-8")).hexdigest()
if key in self._cache:
self.hits += 1
return self._cache[key]
if self.used >= self.budget:
raise BudgetExhausted
self.used += 1
score = self.evaluate(code) # компиляция + тесты + метрика, в песочнице
self._cache[key] = score
return score
class BudgetExhausted(Exception):
"""Бюджет исчерпан — возвращаем лучшее найденное (anytime-поведение)."""
def evolve(
seed_code: str,
fitness: CachedFitness,
operator: Operator,
islands: int = 4,
island_size: int = 8,
rng: random.Random | None = None,
) -> Individual:
"""Островная эволюция. Возвращает лучшую особь на момент исчерпания бюджета.
Сложность: O(B) вызовов фитнеса, где B — бюджет; память O(islands * island_size).
Всё остальное (селекция, сортировка) пренебрежимо на фоне стоимости оценки.
"""
rng = rng or random.Random(0)
base = Individual(fitness(seed_code), seed_code)
pops: list[list[Individual]] = [[base] for _ in range(islands)]
best = base
try:
while True:
for pop in pops:
# турнир из двух случайных: дёшево и устойчиво к шкале фитнеса
parents = [max(rng.sample(pop, min(2, len(pop))), key=lambda i: i.score)
for _ in range(2)]
child_code = operator.propose(parents)
try:
score = fitness(child_code) # валидность проверяет оценщик
except BudgetExhausted:
raise
except Exception:
continue # не скомпилировалось / упало — просто пропускаем
child = Individual(score, child_code, parent=parents[0].code)
pop.append(child)
pop.sort(key=lambda i: i.score, reverse=True)
del pop[island_size:] # усечение до размера острова
if score > best.score:
best = child
# миграция: раз в цикл лучший с острова i переезжает на остров i+1
for i, pop in enumerate(pops):
pops[(i + 1) % islands].append(pop[0])
except BudgetExhausted:
pass
return best
Что в этом коде важно заметить:
- Оператор — единственное, что отличает классический GI от LLM-эволюции. Вся остальная машинерия (бюджет, кэш, острова, элитизм, anytime-возврат) переиспользуется без изменений. Это и есть ответ на вопрос «устарел ли трек после появления LLM»: нет, устарела одна функция из десяти.
- Исключения при оценке проглатываются осознанно: невалидный потомок — это норма, а не баг.
- Миграция между островами — дешёвая страховка от схлопывания разнообразия, критичная именно для LLM-оператора.
- Бюджет считается в оценках фитнеса, а не в поколениях: только так можно честно сравнивать конфигурации между собой.
Assured LLM-based SE: почему воронка фильтров важнее модели
Meta сформулировала практический паттерн под названием Assured LLMSE (Alshahwan et al., 2024): LLM генерирует много кандидатов, а в продакшн проходят только те, кто пережил цепочку детерминированных, объективно проверяемых фильтров.
Опубликованные цифры TestGen-LLM на кодовой базе Instagram/Facebook: около 75 % сгенерированных тест-классов корректно собирались, ~57 % стабильно проходили, ~25 % давали измеримый прирост покрытия, а 73 % итоговых рекомендаций были приняты инженерами в продакшн.
Прочитайте эти числа ещё раз с точки зрения трека. Три четверти работы системы — это отбраковка. Именно фильтры превращают вероятностный генератор в инструмент, которому можно доверять: каждое дошедшее до ревью изменение сопровождается доказательством пользы («покрытие выросло на N строк»), а не обещанием модели.
Это ровно та же логика, что в автоматическом ремонте программ,
где патч, прошедший тесты, всё равно может быть мусором. Проблема overfitting к фитнесу никуда
не делась и в мире LLM обострилась: модель, которой показали падающий тест, охотно напишет
if (input == 42) return expected;. Единственная защита — фитнес, который сложно обмануть:
скрытые тесты, мутационное тестирование, метрики за пределами того, что видел генератор.
Это ровно тот же феномен, что в RL называют reward hacking, а в экономике — законом Гудхарта: как только метрика становится целью, она перестаёт быть хорошей метрикой. SBSE столкнулся с ним на 15 лет раньше и накопил конкретные противоядия: скрытые проверки, многокритериальность, регуляризация по простоте решения.
Часть 5. Типичные ошибки внедрения
Собрано из отчётов о неудачных проектах и собственных граблей.
- Начать с алгоритма, а не с фитнеса. Команда спорит две недели про GA vs. PSO, имея бинарную фитнес-функцию. Правильный порядок: фитнес → представление → baseline → и только потом алгоритм.
- Не измерить случайный поиск. Без baseline любые числа бессмысленны. Это дешевле всего сделать в первый день.
- Игнорировать стоимость оценки. Красивый прототип на игрушечных данных умирает при переходе на реальный код, потому что фитнес стал в 10 000 раз дороже. Оценивайте бюджет до написания кода.
- Один запуск вместо тридцати. Стохастический алгоритм, показанный один раз, — это лотерейный билет, а не результат.
- Забыть про читаемость выхода. Патч на 200 строк, который никто не понимает, не будет принят, даже если он идеален по метрике. Добавляйте минимизацию размера решения как отдельную цель — так и сделал Sapienz с длиной тестов.
- Отсутствие песочницы. Выполнение сгенерированного кода без изоляции однажды закончится удалённым каталогом или заDDoS-енным внутренним сервисом.
- Кэш поверх недетерминированного фитнеса. Flaky-тест, случайно прошедший один раз, навсегда закрепится в кэше как «хорошее решение» и отравит весь прогон.
- Вера в то, что LLM отменяет верификацию. Самая дорогая ошибка 2024–2026 годов. Генерация стала дешёвой — значит, весь дефицит переехал в проверку, и именно туда надо вкладывать инженерное время.
Часть 6. Куда двигаться дальше
Если вы дочитали трек и хотите продолжать, вот честная иерархия следующих шагов.
Ключевые источники, которые стоит держать под рукой:
- Harman, Mansouri, Zhang. «Search-Based Software Engineering: Trends, Techniques and Applications», ACM Computing Surveys, 2012 — до сих пор лучшая карта области.
- Arcuri & Briand, STVR 2014 — как честно мерить стохастические алгоритмы.
- Hoos. «Programming by Optimization», CACM 2012 — манифест «оставляйте выбор алгоритму, а не хардкодьте».
- Wolpert & Macready. «No Free Lunch Theorems for Optimization», 1997 — почему универсального лучшего алгоритма не существует.
- FunSearch, Nature 2024 и AlphaEvolve, 2025 — современное состояние гибридов.
- Ежегодные материалы конференции GECCO и воркшопа SBFT при ICSE — где появляются свежие результаты.
Мини-итог трека
Если из девяти статей нужно унести три мысли, то вот они.
Первая. SBSE — это не про алгоритмы, а про переформулировку. Умение увидеть в «мы не знаем, как правильно нарезать монолит» задачу оптимизации с измеримым качеством — и есть навык. Алгоритм после этого берётся с полки.
Вторая. Успех определяется представлением и фитнесом, а не выбором между отжигом и GA. Плоский или обманчивый ландшафт не спасёт никакая метаэвристика; хороший градуированный фитнес делает решаемой задачу, которая казалась безнадёжной.
Третья. Появление LLM сместило дефицит с генерации на верификацию. Раньше было трудно породить правдоподобного кандидата и легко его проверить. Теперь правдоподобных кандидатов бесконечно много, и вся ценность сосредоточилась в способности объективно отделить работающее от выглядящего работающим. Это ровно та мышца, которую тренирует SBSE, — и поэтому трек стал актуальнее, а не наоборот.
Что дальше
Трек «SBSE и поисковые алгоритмы» на этом заканчивается. Логичные продолжения на портале:
- Алгоритмы и структуры данных — фундамент, без которого разговор о сложности поиска и стоимости оценки остаётся поверхностным.
- Машинное обучение — вторая половина уравнения «обучение предлагает, поиск проверяет»: суррогатные модели, байесовская оптимизация, подбор гиперпараметров.
- Нейронные сети — как устроены модели, которые сегодня работают операторами мутации, и почему NAS сам по себе является задачей SBSE.
- Go — практичный язык для написания быстрых оценщиков и распределённых ферм фитнеса.
А если хочется сначала понять, куда двигаться по порталу целиком, — загляните в дорожную карту.
Ну и главное: возьмите завтра одну задачу из своего проекта, где вариантов слишком много, напишите к ней функцию оценки на 20 строк и запустите случайный поиск. Это займёт вечер и покажет о вашей задаче больше, чем месяц обсуждений.