- BrainTools - https://www.braintools.ru -
Это первая статья из цикла «Хроники Agent Driven Development трансформации». В цикле я рассказываю, как постепенно перевожу реальный продакшен-проект на рельсы agent-driven development — когда LLM-агенты становятся полноценными участниками разработки, а не просто подсказчиками в автокомплите.
В нулевой статье [1] я рассказал, как ускорил прогон ~800 тестов в 6 раз — с 10 минут до 101 секунды. Это было необходимой подготовкой: если agent feedback loop занимает 10 минут на каждый цикл «сгенерировал тест → скомпилировал → запустил → получил результат», то никакой agent-driven development не взлетит.
Два подхода к генерации тестов — sprint-driven и coverage-driven — и когда какой применять
Шестиуровневый pipeline верификации, отсеивающий бесполезные тесты
Двухагентная архитектура Writer + Reviewer с feedback loop
Как ускорение компиляции напрямую влияет на эффективность agent-driven разработки
Конкретные приёмы экономии токенов при массовой генерации
Что сказали разработчики на code review — и почему 13% тестов были отклонены

|
|
|
|---|---|
|
68 тестовых файлов |
сгенерировано LLM-агентом |
|
86.8% acceptance rate |
при ревью живыми разработчиками |
|
~6% прирост branch coverage |
на чистых тестах для непокрытого кода |
|
30-50% экономия токенов |
за счёт оптимизации feedback loop |
Совсем кратко о себе: разрабатываю высоконагруженные сервисы, веду канал о разработке в стартапах в TG [2] и в канале Max [3], где делюсь своим опытом [4].
Контекст
Идея: LLM как тестировщик
Verification Pipeline: 6 gate’ов
Двухагентная архитектура: Writer + Reviewer
Подход 1: Sprint-driven генерация
Подход 2: Coverage-driven генерация
Ускорение компиляции: agent feedback loop
Экономия токенов: agent efficiency
Ревью разработчиками: human-in-the-loop
Что дальше: LLM-тесты как часть спринта
Что я понял
Стек: Scala, Akka, SBT, ScalaTest, ScalaMock, PostgreSQL, Testcontainers. Бэкенд-монолит — ядро системы. Тесты — смесь unit и интеграционных. Интеграционные работают через собственный фреймворк поверх Testcontainers с миграцией БД через Liquibase.
Исходная картина (после оптимизации скорости тестов [1]):
Прогон ~800 тестов за 101 секунду — CI больше не узкое место
Низкое покрытие — и statement, и branch coverage далеки от целевых значений
Пороги покрытия установлены в билде — ниже падать нельзя, но до целевых 90% как до Луны
Тесты пишутся вручную, и их хронически не хватает — продуктовые фичи всегда важнее
Задача: быстро нарастить покрытие, не тратя человеко-месяцы на написание тестов вручную.
Генерация тестов через LLM — тема не новая. Но исследования (Meta TestGen-LLM, MUTGEN 2025) показывают неприятную правду:
|
Проблема |
Описание |
|---|---|
|
Тесты-попугаи |
Тестируют реализацию, ломаются при рефакторинге |
|
Тесты-пустышки |
Покрывают строки, но не ловят баги. MUTGEN показал: 100% line coverage → 4% mutation score |
|
Test smells |
Assertion Roulette, Magic Numbers, Thread.sleep, мокирование SUT |
Просто попросить LLM «напиши тесты для этого класса» — получишь строки покрытия, но не безопасность релиза. Нужна система контроля качества. Как именно — дальше.
Я спроектировал многоуровневый pipeline, через который проходит каждый сгенерированный тест:

Шесть ступеней — от банальной компиляции до семантического ревью вторым LLM-агентом. Каждый gate отсеивает часть тестов: компиляция — ~20-25%, зелёный прогон — ещё ~15-20%, mutation testing — самый ценный, проверяет не что тест работает, а что он нужен.
Банальный, но отсеивающий ~20-25% первых попыток. LLM часто путает импорты, использует несуществующие методы или неправильные типы. При reject агент получает полный вывод компилятора и пытается исправить.
Тест должен проходить. Ещё ~15-20% отсеивается: неправильные assertions, неверные expected values, проблемы с тестовым окружением.
Запуск 3-5 раз. Любое расхождение → reject без права на апелляцию. Flaky тесты — это race conditions и shared state, которые LLM плохо чинит. Лучше сразу отбросить.
Ключевой gate. Stryker4s вносит мутации в целевой файл (замена > на >=, true на false, удаление вызовов) и проверяет, падает ли тест. Если не падает — тест бесполезен.
Пороги mutation score:
Чистые функции, модели: ≥ 50%
Сервисы, бизнес-логика: ≥ 40%
HTTP routes, DAO, акторы: ≥ 30%
При reject LLM получает конкретику:
Мутант выжил: строка 42, замена `if (count > 0)` → `if (count >= 0)`.
Ни один тест не обнаружил эту мутацию.
Добавь тест-кейс для граничного случая count == 0.
Автоматическая проверка антипаттернов:
|
Smell |
Правило |
|---|---|
|
No assertions |
Тест без единого assert → REJECT |
|
Assertion Roulette |
> 5 assertions без описания → REJECT |
|
Excessive Mocking |
> 6 mock-объектов → REJECT |
|
Thread.sleep |
В unit-тестах → WARNING |
|
Empty test |
Пустое тело → REJECT |
Самый интересный. Отдельный LLM-агент проверяет то, что не ловят автоматические gate’ы:
Тавтологичность — expected value вычисляется тем же кодом, что и SUT
Соответствие задаче — тест реально проверяет то, что описано в задаче трекера
Полнота сценариев — есть happy path, граничные случаи, ошибки [5]
Ценность — тест не тривиален (не проверяет getter или copy() data-класса)
Ключевой инсайт: Gate 4 (Mutation Testing) — самый ценный из шести. Остальные gate’ы проверяют, что тест работает. Mutation testing проверяет, что тест нужен — ловит ли он реальные баги при изменении кода.

Главный инсайт: один LLM-агент не может одновременно и генерировать, и критиковать свой код. Нужно разделение ролей.
Writer Agent — пишет тесты. Получает задачу из трекера, анализирует SUT (system under test), генерирует тест-кейсы, пишет код.
Reviewer Agent — ревьюит результаты. Работает по чеклисту из 12 пунктов, знает антипаттерны проекта, имеет примеры хороших и плохих тестов из существующей кодовой базы.
Когда Reviewer отклоняет тест, Writer получает конкретный feedback:
REVISE: тест "should process message" тавтологичен.
Expected value `SUT.convert(input)` совпадает с вызовом SUT.
Замени на литерал, рассчитанный вручную.
Writer дорабатывает (не переписывает с нуля!), тест проходит быстрый re-check (Gate 1-2) и возвращается к Reviewer. Максимум 2 итерации.
Ключевой инсайт: feedback loop Writer → Reviewer даёт +21-32% к качеству assertions (по данным исследования Self-Refining LLM Unit Testers). Два агента по отдельности слабее, чем один цикл ревью между ними.
Архитектура описана — теперь два конкретных подхода, как её применять.
Генерировать тесты «на весь проект» — утопия. Нет контекста, непонятно что важно. Первый подход — идти от задач трекера.
Python-скрипт загружает спринты и задачи из трекера по API, классифицирует их, и для каждой core-задачи Writer Agent генерирует тесты через pipeline.
Шаг 1. Python-скрипт загружает спринты и задачи из трекера по API.
Шаг 2. Скрипт классификации автоматически распределяет задачи:
|
Категория |
Критерий |
Пример |
|---|---|---|
|
core |
Изменения в бэкенд-логике |
Новая бизнес-фича, исправление бага |
|
frontend |
Только фронт |
Правка CSS, новый компонент |
|
adapters |
Интеграции |
Новый канал, webhook |
|
technical |
Инфраструктура |
Миграция БД, обновление зависимостей |
|
incidents |
SRE |
Расследование инцидента |
Шаг 3. Для каждой core-задачи Writer Agent:
Анализирует коммиты (какие файлы менялись)
Находит SUT в коде
Генерирует тест-кейсы в JSON
Пишет тесты
Прогоняет через pipeline
Каждый тест помечается тегами для трассировки:
it should "корректно обработать пустой ввод (TASK-1234)" taggedAs(LlmGenerated, Sprint("2.4.1")) in {
// ...
}
LlmGenerated — помечает, что тест написан LLM (а не человеком). Sprint("2.4.1") — привязка к спринту. Так можно отфильтровать все LLM-тесты или все тесты конкретного спринта.
|
Серия спринтов |
Спринтов обработано |
Тестов написано |
Тип |
|---|---|---|---|
|
Ранние спринты (3 серии) |
8 |
12 |
integration |
|
Средние спринты (3 серии) |
23 |
119 |
unit + integration |
|
Поздние спринты (2 серии) |
14 |
155 |
unit + integration |
|
Итого |
45 |
286 |
|
286 тестов в 40 файлах. Покрытие выросло, хотя до целевых порогов ещё далеко. Но главная ценность — не в цифрах. Тесты находят реальные баги при рефакторинге: каждый привязан к конкретной задаче, и при падении сразу понятно, какое бизнес-поведение сломалось.
Хорошее начало — но sprint-driven имеет ограничение. О нём — в следующем подходе.
Sprint-driven подход дал 286 тестов — отличный старт. Но он генерирует тесты для задач, а не для кода. Если файл уже хорошо покрыт — тратим время на дубли. Если файл никогда не фигурировал в задачах — он остаётся непокрытым.
Следующий шаг — тестировать непокрытый код. Но не весь, а только тот, для которого можем гарантировать качество контекста.
Отчёт покрытия с разбивкой по ~50 пакетам. Инструмент — scoverage. Ключевая метрика — branch coverage, а не statement (она честнее: показывает, какие ветки if/else/match реально проверены).
LLM пишет хорошие тесты только при хорошем контексте. Если wiki-страница не обновлялась два года — контракты поменялись, а документация врёт. Тесты по устаревшей документации будут проверять поведение [6], которого уже нет.
Я проанализировал ~150 страниц внутренней wiki:
|
Категория |
Критерий |
Количество |
|---|---|---|
|
Свежие |
Обновлены в последние 6 месяцев |
~60 страниц |
|
Устаревшие |
Не обновлялись более 6 месяцев |
~90 страниц |
Правило: генерируем тесты только для пакетов, у которых есть свежая документация. Остальные — в «долг», пока люди не обновят доки.
На пересечении — идеальные кандидаты:
Модели и конвертеры внешних API (покрытие 5-15%, доки свежие)
Утилиты и хелперы (покрытие 10-25%, доки свежие)
Протоколы сериализации (покрытие 0-20%, доки свежие)
Конфигурационные модели (покрытие 15-30%, доки свежие)
Работа шла итеративными раундами — по 10-50 тестов за раунд:
|
Раунд |
Что покрывали |
Тестов |
Результат |
|---|---|---|---|
|
1–6 |
Модели таймеров, конвертеры, утилиты, настройки |
~170 |
Все зелёные |
|
7–8 |
Протоколы фильтрации, статусы пользователей |
~50 |
Все зелёные |
|
9–13 |
Фильтры, конфиги платформ, модели сообщений |
~65 |
62 зелёных, 3 удалены |
Branch coverage вырос на ~6% относительно исходного значения — не headline-цифра, но это прирост на «чистых» тестах для непокрытого кода, а не на дублировании уже покрытых путей.
Ключевой инсайт: coverage-driven подход нацелен точно на непокрытые ветки. А фильтр по свежести документации отсекает пакеты, где LLM сгенерирует мусор из-за устаревшего контекста.
Оба подхода дали тесты. Но насколько быстро агент может итерироваться — зависит от скорости компиляции и обратной связи. Об этом — следующие два раздела.
Agent-driven development — это цикл: агент пишет код → компилирует → запускает тесты → анализирует результат → исправляет → повторяет. Каждая итерация стоит времени и токенов. Чем быстрее цикл — тем эффективнее агент.
|
Что |
Было |
Стало |
Эффект |
|---|---|---|---|
|
Полная компиляция модуля |
~90с |
~60с |
–33% |
|
Вывод компиляции |
~2000 строк |
~50 строк (только warnings) |
–97% шума |
|
Запуск одного теста |
~15с (startup + test) |
~12с |
–20% |
Для агента, который делает 5-10 итераций compile-test на один тестовый файл, это экономит 3-5 минут на файл.
Scala-компилятор по умолчанию компилирует backend (генерацию байткода) в один поток. Флаг -Ybackend-parallelism распараллеливает эту фазу:
scalacOptions += "-Ybackend-parallelism" -> "4"
На 4-ядерной машине это даёт ощутимое ускорение полной компиляции. Для инкрементальной (когда поменялся один файл) эффект меньше, но всё равно заметен.
По умолчанию SBT выводит INFO-логи для каждого скомпилированного файла. При 500+ файлах в модуле — это тысячи строк, которые агент честно читает и тратит на них токены. Решение:
Compile / logLevel := Level.Warn
Теперь в вывод попадают только предупреждения и ошибки — то, что реально нужно для принятия решений.
Scala-компилятор — тяжёлое JVM-приложение. Дефолтные настройки памяти [7] для него недостаточны:
-Xms3048m
-Xmx3048m
-XX:ReservedCodeCacheSize=256m
-XX:MaxMetaspaceSize=512m
Увеличенный heap и code cache снижают частоту сборки мусора, что критично при компиляции больших модулей. Отдельно — отключение логирования макросов ORM-фреймворка (-Dquill.macro.log=false), которое генерировало тысячи строк debug-вывода при компиляции DAO-слоя.
Ключевой инсайт: –97% шума компиляции — это не только экономия токенов. Это чище контекстное окно агента: меньше мусора → точнее анализ ошибок → меньше итераций.

Когда агент генерирует тесты массово, расход токенов становится заметной статьёй бюджета. Я нашёл четыре источника бессмысленных трат и устранил их.
Суммарный эффект: 30-50% экономия токенов за раунд генерации. На масштабе 68 тестовых файлов — разница между «укладываемся в бюджет» и «нужно в два раза больше».
Типичный запуск sbt compile на Scala-проекте — это 2000+ строк вывода: разрешение зависимостей, компиляция каждого файла, загрузка плагинов. Агент читает всё это, тратит токены, а полезной информации там — exit code и, может быть, 5 строк ошибок.
Решение: logLevel := Level.Warn для компиляции + чтение результатов тестов из XML-отчётов (target/test-reports/*.xml) вместо парсинга консольного вывода. XML содержит структурированные данные: имя теста, статус, время выполнения, stacktrace при ошибке — всё, что нужно агенту для принятия решений.
В начале я запускал измерение покрытия после каждого раунда генерации (10-15 тестов). Каждое измерение — это полная пересборка с инструментацией + запуск всех 800+ тестов + генерация отчёта. 10-15 минут и несколько тысяч токенов на парсинг отчёта.
Решение: измерять покрытие только в конце, после завершения всех раундов генерации. Промежуточный прогресс оценивать по количеству покрытых веток в целевых файлах — это видно из testOnly без полного прогона.
Pipeline изначально требовал для каждой задачи создавать JSON-файл с тест-кейсами, потом сами тесты, потом отчёт. Три файла вместо одного. Агент читал и писал каждый из них, тратя токены на форматирование и парсинг.
Решение: при массовой генерации тестов для покрытия — сразу писать тесты, без промежуточного JSON. Артефакт нужен для трассировки до задач трекера (sprint-driven), но при coverage-driven подходе задачи трекера не задействованы.
Агент читал целые файлы по 500-1000 строк, хотя для генерации теста нужна была одна функция на 20 строк. Остальные 980 строк — чистый расход токенов.
Решение: читать файлы точечно — с указанием offset и limit. Если нужна сигнатура метода — читаем 30 строк вокруг. Если нужен весь класс — тогда да, весь файл. Но осознанно.
Компиляция ускорена, токены сэкономлены. Но финальный вердикт — за живыми разработчиками.
Самый важный этап — ревью живыми разработчиками. Никакой pipeline не заменит человека, который знает кодовую базу и понимает бизнес-контекст.
Все 68 LLM-сгенерированных тестовых файлов были отданы на ревью команде. Вердикт:
«В целом качество тестов хорошее, но некоторые тесты я бы всё-таки убрал.»
|
Метрика |
Значение |
|---|---|
|
Всего тестовых файлов на ревью |
68 |
|
Принято (good quality) |
59 |
|
Рекомендовано к удалению |
9 |
|
Acceptance rate |
86.8% |
Паттерн отклонённых тестов — тесты на тривиальное поведение [8]:
Простые маппинги enum → string — компилятор и так гарантирует, что маппинг работает. Тест проверяет, что StatusA.toString == "StatusA" — бесполезно.
Тесты геттеров data-классов — проверка, что entity.field возвращает то, что было передано в конструктор. Это не тест, это проверка, что Scala работает.
Тесты тривиальных конвертеров — когда конвертация сводится к case A => B; case C => D без бизнес-логики.
Интересно, что эти 9 файлов — как раз тот случай, когда Gate 6 (семантическое ревью) должен был их отсечь. Pipeline пропустил их, потому что формально тесты были корректны: компилировались, проходили, имели assertions. Но ценность этих тестов — околонулевая.
86.8% acceptance rate — хороший результат для LLM-генерации. Но 13% мусора — тоже важная цифра. Без человеческого ревью эти 9 файлов остались бы навсегда, создавая ложное чувство безопасности.
Pipeline не ловит тривиальность. Все 6 gate’ов проверяют корректность и стабильность. Но вопрос «а стоит ли вообще это тестировать?» — пока только для человека.
Ревью LLM-тестов быстрее, чем ревью LLM-кода. Тесты — это контракт на поведение. Разработчику проще оценить «имеет ли смысл этот контракт?», чем вникать в реализацию.
Ключевой инсайт: ревью LLM-тестов занимает минуты, а не часы. Тест — это спецификация поведения, и оценить «нужна ли эта спецификация?» гораздо быстрее, чем писать её с нуля.
Всё описанное выше — ретроспективная работа: мы генерировали тесты для кода, который уже давно в проде. Но главная ценность выстроенного процесса — возможность встроить его в текущую разработку.
Идея: каждый спринт заканчивается не только релизом, но и автоматической генерацией тестов для задач этого спринта. Разработчик пишет код и свои тесты, а LLM-агент параллельно генерирует дополнительные — на граничные случаи, негативные сценарии, ветки, которые человек мог пропустить.
Как это выглядит на практике:
Спринт закрыт, задачи смержены в основную ветку
Агент получает список core-задач спринта из трекера
Для каждой задачи — анализирует изменённые файлы, находит непокрытые ветки
Генерирует тесты, прогоняет через pipeline
Результат — merge request с новыми тестами, готовый к ревью
Разработчику остаётся только посмотреть MR и принять или отклонить. По нашему опыту, 86.8% тестов принимаются без правок — это 10-15 минут ревью вместо нескольких часов написания.
LLM-тесты не заменяют тесты разработчика, а дополняют их. Разработчик лучше понимает бизнес-контекст и пишет тесты на критические сценарии. LLM лучше перебирает комбинации и граничные случаи — ту рутину, на которую у людей вечно не хватает времени.
Не качество модели, не количество токенов, а скорость цикла «написал → скомпилировал → запустил → получил результат». Если цикл занимает 10 минут — агент делает 6 итераций в час. Если 2 минуты — 30 итераций. Разница в 5 раз при тех же затратах.
Sprint-driven даёт привязку к бизнесу: каждый тест связан с конкретной задачей. Coverage-driven даёт точность: тесты генерируются именно для непокрытых веток. Первый — для начала, второй — для наращивания.
Генерировать тесты по устаревшей документации — создавать тесты, которые проверяют поведение двухлетней давности. Анализ свежести wiki занял полдня, но сэкономил десятки часов на ревью и переделке.
При массовой генерации тестов разница между «агент читает 2000 строк SBT-лога» и «агент читает 50 строк ошибок» — это 30-50% бюджета. Оптимизация вывода сборки, точечное чтение файлов, отложенное измерение покрытия — каждое по отдельности мелочь, вместе — существенная экономия.
13% отклонённых тестов — это тесты тривиального поведения, которые pipeline не отсёк. Нужен дополнительный gate: проверка ценности теста. Добавить в Reviewer Agent правило «если SUT — чистый маппинг без бизнес-логики, пропускай».
Writer + Reviewer с feedback loop даёт +21-32% к качеству assertions. Но 9 файлов из 68 всё равно прошли pipeline и были отклонены человеком. Агент-ревьюер пока не умеет оценивать «бизнес-ценность» теста — только его техническую корректность.
68 тестовых файлов, 86.8% acceptance rate при ревью разработчиками, branch coverage вырос на ~6%.
Цифры скромные? Возможно. Но за ними — выстроенный процесс, который масштабируется. Sprint-driven генерация + coverage-driven наращивание + оп��имизированный feedback loop — это машина, которая каждую неделю добавляет в проект десятки тестов без участия людей в написании кода.
Главное, что я вынес из этого этапа: agent-driven development — это не про «дай LLM написать код». Это про инженерию процесса. Ускорение компиляции, подавление шума, анализ покрытия, фильтр документации, pipeline верификации — каждый из этих элементов по отдельности тривиален. Вместе они превращают LLM из игрушки в инструмент.
В своем канале в Telegram [2] и в канале Max [3] о разработке в стартапах рассказываю ещё больше интересного и делюсь опытом, заходите, буду рад!
Всем добра и тихих релизов!
Автор: rurikovich
Источник [9]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/27217
URLs in this post:
[1] нулевой статье: https://habr.com/ru/articles/1003592/
[2] канал о разработке в стартапах в TG: https://t.me/tech_lead_rst
[3] в канале Max: https://max.ru/join/3g33rHRy936aspuXPILwvsfk7x6b4GUjqmW5hWVww_8
[4] опытом: http://www.braintools.ru/article/6952
[5] ошибки: http://www.braintools.ru/article/4192
[6] поведение: http://www.braintools.ru/article/9372
[7] памяти: http://www.braintools.ru/article/4140
[8] поведение: http://www.braintools.ru/article/5593
[9] Источник: https://habr.com/ru/articles/1010148/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1010148
Нажмите здесь для печати.