- BrainTools - https://www.braintools.ru -

Хроники Agent Driven Development трансформации .1: улучшаем agent feedback loop

Это первая статья из цикла «Хроники Agent Driven Development трансформации». В цикле я рассказываю, как постепенно перевожу реальный продакшен-проект на рельсы agent-driven development — когда LLM-агенты становятся полноценными участниками разработки, а не просто подсказчиками в автокомплите.

В нулевой статье [1] я рассказал, как ускорил прогон ~800 тестов в 6 раз — с 10 минут до 101 секунды. Это было необходимой подготовкой: если agent feedback loop занимает 10 минут на каждый цикл «сгенерировал тест → скомпилировал → запустил → получил результат», то никакой agent-driven development не взлетит.

Что вы узнаете из этой статьи

  1. Два подхода к генерации тестов — sprint-driven и coverage-driven — и когда какой применять

  2. Шестиуровневый pipeline верификации, отсеивающий бесполезные тесты

  3. Двухагентная архитектура Writer + Reviewer с feedback loop

  4. Как ускорение компиляции напрямую влияет на эффективность agent-driven разработки

  5. Конкретные приёмы экономии токенов при массовой генерации

  6. Что сказали разработчики на code review — и почему 13% тестов были отклонены

    Хроники Agent Driven Development трансформации .1: улучшаем agent feedback loop - 1

Результаты в цифрах

68 тестовых файлов

сгенерировано LLM-агентом

86.8% acceptance rate

при ревью живыми разработчиками

~6% прирост branch coverage

на чистых тестах для непокрытого кода

30-50% экономия токенов

за счёт оптимизации feedback loop

Совсем кратко о себе: разрабатываю высоконагруженные сервисы, веду канал о разработке в стартапах в TG [2] и в канале Max [3], где делюсь своим опытом [4].

Оглавление

  1. Контекст

  2. Идея: LLM как тестировщик

  3. Verification Pipeline: 6 gate’ов

  4. Двухагентная архитектура: Writer + Reviewer

  5. Подход 1: Sprint-driven генерация

  6. Подход 2: Coverage-driven генерация

  7. Ускорение компиляции: agent feedback loop

  8. Экономия токенов: agent efficiency

  9. Ревью разработчиками: human-in-the-loop

  10. Что дальше: LLM-тесты как часть спринта

  11. Что я понял


1. Контекст

Стек: Scala, Akka, SBT, ScalaTest, ScalaMock, PostgreSQL, Testcontainers. Бэкенд-монолит — ядро системы. Тесты — смесь unit и интеграционных. Интеграционные работают через собственный фреймворк поверх Testcontainers с миграцией БД через Liquibase.

Исходная картина (после оптимизации скорости тестов [1]):

  • Прогон ~800 тестов за 101 секунду — CI больше не узкое место

  • Низкое покрытие — и statement, и branch coverage далеки от целевых значений

  • Пороги покрытия установлены в билде — ниже падать нельзя, но до целевых 90% как до Луны

  • Тесты пишутся вручную, и их хронически не хватает — продуктовые фичи всегда важнее

Задача: быстро нарастить покрытие, не тратя человеко-месяцы на написание тестов вручную.

2. Идея: LLM как тестировщик

Генерация тестов через LLM — тема не новая. Но исследования (Meta TestGen-LLM, MUTGEN 2025) показывают неприятную правду:

Проблема

Описание

Тесты-попугаи

Тестируют реализацию, ломаются при рефакторинге

Тесты-пустышки

Покрывают строки, но не ловят баги. MUTGEN показал: 100% line coverage → 4% mutation score

Test smells

Assertion Roulette, Magic Numbers, Thread.sleep, мокирование SUT

Просто попросить LLM «напиши тесты для этого класса» — получишь строки покрытия, но не безопасность релиза. Нужна система контроля качества. Как именно — дальше.

3. Verification Pipeline: 6 gate’ов

Я спроектировал многоуровневый pipeline, через который проходит каждый сгенерированный тест:

Хроники Agent Driven Development трансформации .1: улучшаем agent feedback loop - 2

Шесть ступеней — от банальной компиляции до семантического ревью вторым LLM-агентом. Каждый gate отсеивает часть тестов: компиляция — ~20-25%, зелёный прогон — ещё ~15-20%, mutation testing — самый ценный, проверяет не что тест работает, а что он нужен.

Подробнее: описание каждого gate’а

Gate 1: Компиляция

Банальный, но отсеивающий ~20-25% первых попыток. LLM часто путает импорты, использует несуществующие методы или неправильные типы. При reject агент получает полный вывод компилятора и пытается исправить.

Gate 2: Зелёный тест

Тест должен проходить. Ещё ~15-20% отсеивается: неправильные assertions, неверные expected values, проблемы с тестовым окружением.

Gate 3: Стабильность (anti-flaky)

Запуск 3-5 раз. Любое расхождение → reject без права на апелляцию. Flaky тесты — это race conditions и shared state, которые LLM плохо чинит. Лучше сразу отбросить.

Gate 4: Mutation Testing

Ключевой gate. Stryker4s вносит мутации в целевой файл (замена > на >=, true на false, удаление вызовов) и проверяет, падает ли тест. Если не падает — тест бесполезен.

Пороги mutation score:

  • Чистые функции, модели: ≥ 50%

  • Сервисы, бизнес-логика: ≥ 40%

  • HTTP routes, DAO, акторы: ≥ 30%

При reject LLM получает конкретику:

Мутант выжил: строка 42, замена `if (count > 0)` → `if (count >= 0)`.
Ни один тест не обнаружил эту мутацию.
Добавь тест-кейс для граничного случая count == 0.

Gate 5: Smell Analysis

Автоматическая проверка антипаттернов:

Smell

Правило

No assertions

Тест без единого assert → REJECT

Assertion Roulette

> 5 assertions без описания → REJECT

Excessive Mocking

> 6 mock-объектов → REJECT

Thread.sleep

В unit-тестах → WARNING

Empty test

Пустое тело → REJECT

Gate 6: Семантическое ревью (Reviewer Agent)

Самый интересный. Отдельный LLM-агент проверяет то, что не ловят автоматические gate’ы:

  • Тавтологичность — expected value вычисляется тем же кодом, что и SUT

  • Соответствие задаче — тест реально проверяет то, что описано в задаче трекера

  • Полнота сценариев — есть happy path, граничные случаи, ошибки [5]

  • Ценность — тест не тривиален (не проверяет getter или copy() data-класса)

Ключевой инсайт: Gate 4 (Mutation Testing) — самый ценный из шести. Остальные gate’ы проверяют, что тест работает. Mutation testing проверяет, что тест нужен — ловит ли он реальные баги при изменении кода.

4. Двухагентная архитектура: Writer + Reviewer

Хроники Agent Driven Development трансформации .1: улучшаем agent feedback loop - 3

Главный инсайт: один 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). Два агента по отдельности слабее, чем один цикл ревью между ними.

Архитектура описана — теперь два конкретных подхода, как её применять.

5. Подход 1: Sprint-driven генерация

Генерировать тесты «на весь проект» — утопия. Нет контекста, непонятно что важно. Первый подход — идти от задач трекера.

Python-скрипт загружает спринты и задачи из трекера по API, классифицирует их, и для каждой core-задачи Writer Agent генерирует тесты через pipeline.

Подробнее: классификация и шаги генерации

Шаг 1. Python-скрипт загружает спринты и задачи из трекера по API.

Шаг 2. Скрипт классификации автоматически распределяет задачи:

Категория

Критерий

Пример

core

Изменения в бэкенд-логике

Новая бизнес-фича, исправление бага

frontend

Только фронт

Правка CSS, новый компонент

adapters

Интеграции

Новый канал, webhook

technical

Инфраструктура

Миграция БД, обновление зависимостей

incidents

SRE

Расследование инцидента

Шаг 3. Для каждой core-задачи Writer Agent:

  1. Анализирует коммиты (какие файлы менялись)

  2. Находит SUT в коде

  3. Генерирует тест-кейсы в JSON

  4. Пишет тесты

  5. Прогоняет через pipeline

Каждый тест помечается тегами для трассировки:

it should "корректно обработать пустой ввод (TASK-1234)" taggedAs(LlmGenerated, Sprint("2.4.1")) in {
  // ...
}

LlmGenerated — помечает, что тест написан LLM (а не человеком). Sprint("2.4.1") — привязка к спринту. Так можно отфильтровать все LLM-тесты или все тесты конкретного спринта.

Результаты sprint-driven

Серия спринтов

Спринтов обработано

Тестов написано

Тип

Ранние спринты (3 серии)

8

12

integration

Средние спринты (3 серии)

23

119

unit + integration

Поздние спринты (2 серии)

14

155

unit + integration

Итого

45

286

286 тестов в 40 файлах. Покрытие выросло, хотя до целевых порогов ещё далеко. Но главная ценность — не в цифрах. Тесты находят реальные баги при рефакторинге: каждый привязан к конкретной задаче, и при падении сразу понятно, какое бизнес-поведение сломалось.

Хорошее начало — но sprint-driven имеет ограничение. О нём — в следующем подходе.

6. Подход 2: Coverage-driven генерация

Sprint-driven подход дал 286 тестов — отличный старт. Но он генерирует тесты для задач, а не для кода. Если файл уже хорошо покрыт — тратим время на дубли. Если файл никогда не фигурировал в задачах — он остаётся непокрытым.

Следующий шаг — тестировать непокрытый код. Но не весь, а только тот, для которого можем гарантировать качество контекста.

Шаг 1: Анализ покрытия по пакетам

Отчёт покрытия с разбивкой по ~50 пакетам. Инструмент — scoverage. Ключевая метрика — branch coverage, а не statement (она честнее: показывает, какие ветки if/else/match реально проверены).

Шаг 2: Анализ свежести документации

LLM пишет хорошие тесты только при хорошем контексте. Если wiki-страница не обновлялась два года — контракты поменялись, а документация врёт. Тесты по устаревшей документации будут проверять поведение [6], которого уже нет.

Я проанализировал ~150 страниц внутренней wiki:

Категория

Критерий

Количество

Свежие

Обновлены в последние 6 месяцев

~60 страниц

Устаревшие

Не обновлялись более 6 месяцев

~90 страниц

Правило: генерируем тесты только для пакетов, у которых есть свежая документация. Остальные — в «долг», пока люди не обновят доки.

Шаг 3: Пересечение — низкое покрытие + свежие доки

На пересечении — идеальные кандидаты:

  • Модели и конвертеры внешних 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 сгенерирует мусор из-за устаревшего контекста.

Оба подхода дали тесты. Но насколько быстро агент может итерироваться — зависит от скорости компиляции и обратной связи. Об этом — следующие два раздела.

7. Ускорение компиляции: agent feedback loop

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

Теперь в вывод попадают только предупреждения и ошибки — то, что реально нужно для принятия решений.

JVM-тюнинг для сборки

Scala-компилятор — тяжёлое JVM-приложение. Дефолтные настройки памяти [7] для него недостаточны:

-Xms3048m
-Xmx3048m
-XX:ReservedCodeCacheSize=256m
-XX:MaxMetaspaceSize=512m

Увеличенный heap и code cache снижают частоту сборки мусора, что критично при компиляции больших модулей. Отдельно — отключение логирования макросов ORM-фреймворка (-Dquill.macro.log=false), которое генерировало тысячи строк debug-вывода при компиляции DAO-слоя.

Ключевой инсайт: –97% шума компиляции — это не только экономия токенов. Это чище контекстное окно агента: меньше мусора → точнее анализ ошибок → меньше итераций.

8. Экономия токенов: agent efficiency

Хроники Agent Driven Development трансформации .1: улучшаем agent feedback loop - 4

Когда агент генерирует тесты массово, расход токенов становится заметной статьёй бюджета. Я нашёл четыре источника бессмысленных трат и устранил их.

Суммарный эффект: 30-50% экономия токенов за раунд генерации. На масштабе 68 тестовых файлов — разница между «укладываемся в бюджет» и «нужно в два раза больше».

Подробнее: 4 проблемы и решения

Проблема 1: SBT-вывод затапливает контекстное окно

Типичный запуск sbt compile на Scala-проекте — это 2000+ строк вывода: разрешение зависимостей, компиляция каждого файла, загрузка плагинов. Агент читает всё это, тратит токены, а полезной информации там — exit code и, может быть, 5 строк ошибок.

Решение: logLevel := Level.Warn для компиляции + чтение результатов тестов из XML-отчётов (target/test-reports/*.xml) вместо парсинга консольного вывода. XML содержит структурированные данные: имя теста, статус, время выполнения, stacktrace при ошибке — всё, что нужно агенту для принятия решений.

Проблема 2: Избыточные измерения покрытия

В начале я запускал измерение покрытия после каждого раунда генерации (10-15 тестов). Каждое измерение — это полная пересборка с инструментацией + запуск всех 800+ тестов + генерация отчёта. 10-15 минут и несколько тысяч токенов на парсинг отчёта.

Решение: измерять покрытие только в конце, после завершения всех раундов генерации. Промежуточный прогресс оценивать по количеству покрытых веток в целевых файлах — это видно из testOnly без полного прогона.

Проблема 3: Промежуточные артефакты

Pipeline изначально требовал для каждой задачи создавать JSON-файл с тест-кейсами, потом сами тесты, потом отчёт. Три файла вместо одного. Агент читал и писал каждый из них, тратя токены на форматирование и парсинг.

Решение: при массовой генерации тестов для покрытия — сразу писать тесты, без промежуточного JSON. Артефакт нужен для трассировки до задач трекера (sprint-driven), но при coverage-driven подходе задачи трекера не задействованы.

Проблема 4: Чтение полных исходных файлов

Агент читал целые файлы по 500-1000 строк, хотя для генерации теста нужна была одна функция на 20 строк. Остальные 980 строк — чистый расход токенов.

Решение: читать файлы точечно — с указанием offset и limit. Если нужна сигнатура метода — читаем 30 строк вокруг. Если нужен весь класс — тогда да, весь файл. Но осознанно.

Компиляция ускорена, токены сэкономлены. Но финальный вердикт — за живыми разработчиками.

9. Ревью разработчиками: human-in-the-loop

Самый важный этап — ревью живыми разработчиками. Никакой pipeline не заменит человека, который знает кодовую базу и понимает бизнес-контекст.

Все 68 LLM-сгенерированных тестовых файлов были отданы на ревью команде. Вердикт:

«В целом качество тестов хорошее, но некоторые тесты я бы всё-таки убрал.»

Статистика

Метрика

Значение

Всего тестовых файлов на ревью

68

Принято (good quality)

59

Рекомендовано к удалению

9

Acceptance rate

86.8%

Подробнее: почему 9 файлов отклонены

Паттерн отклонённых тестов — тесты на тривиальное поведение [8]:

  • Простые маппинги enum → string — компилятор и так гарантирует, что маппинг работает. Тест проверяет, что StatusA.toString == "StatusA" — бесполезно.

  • Тесты геттеров data-классов — проверка, что entity.field возвращает то, что было передано в конструктор. Это не тест, это проверка, что Scala работает.

  • Тесты тривиальных конвертеров — когда конвертация сводится к case A => B; case C => D без бизнес-логики.

Интересно, что эти 9 файлов — как раз тот случай, когда Gate 6 (семантическое ревью) должен был их отсечь. Pipeline пропустил их, потому что формально тесты были корректны: компилировались, проходили, имели assertions. Но ценность этих тестов — околонулевая.

Уроки из ревью

  1. 86.8% acceptance rate — хороший результат для LLM-генерации. Но 13% мусора — тоже важная цифра. Без человеческого ревью эти 9 файлов остались бы навсегда, создавая ложное чувство безопасности.

  2. Pipeline не ловит тривиальность. Все 6 gate’ов проверяют корректность и стабильность. Но вопрос «а стоит ли вообще это тестировать?» — пока только для человека.

  3. Ревью LLM-тестов быстрее, чем ревью LLM-кода. Тесты — это контракт на поведение. Разработчику проще оценить «имеет ли смысл этот контракт?», чем вникать в реализацию.

Ключевой инсайт: ревью LLM-тестов занимает минуты, а не часы. Тест — это спецификация поведения, и оценить «нужна ли эта спецификация?» гораздо быстрее, чем писать её с нуля.

10. Что дальше: LLM-тесты как часть спринта

Всё описанное выше — ретроспективная работа: мы генерировали тесты для кода, который уже давно в проде. Но главная ценность выстроенного процесса — возможность встроить его в текущую разработку.

Идея: каждый спринт заканчивается не только релизом, но и автоматической генерацией тестов для задач этого спринта. Разработчик пишет код и свои тесты, а LLM-агент параллельно генерирует дополнительные — на граничные случаи, негативные сценарии, ветки, которые человек мог пропустить.

Как это выглядит на практике:

  1. Спринт закрыт, задачи смержены в основную ветку

  2. Агент получает список core-задач спринта из трекера

  3. Для каждой задачи — анализирует изменённые файлы, находит непокрытые ветки

  4. Генерирует тесты, прогоняет через pipeline

  5. Результат — merge request с новыми тестами, готовый к ревью

Разработчику остаётся только посмотреть MR и принять или отклонить. По нашему опыту, 86.8% тестов принимаются без правок — это 10-15 минут ревью вместо нескольких часов написания.

LLM-тесты не заменяют тесты разработчика, а дополняют их. Разработчик лучше понимает бизнес-контекст и пишет тесты на критические сценарии. LLM лучше перебирает комбинации и граничные случаи — ту рутину, на которую у людей вечно не хватает времени.

11. Что я понял

Agent feedback loop — главный bottleneck

Не качество модели, не количество токенов, а скорость цикла «написал → скомпилировал → запустил → получил результат». Если цикл занимает 10 минут — агент делает 6 итераций в час. Если 2 минуты — 30 итераций. Разница в 5 раз при тех же затратах.

Два подхода дополняют друг друга

Sprint-driven даёт привязку к бизнесу: каждый тест связан с конкретной задачей. Coverage-driven даёт точность: тесты генерируются именно для непокрытых веток. Первый — для начала, второй — для наращивания.

Свежесть документации — фильтр качества

Генерировать тесты по устаревшей документации — создавать тесты, которые проверяют поведение двухлетней давности. Анализ свежести wiki занял полдня, но сэкономил десятки часов на ревью и переделке.

Экономия токенов — не мелочь

При массовой генерации тестов разница между «агент читает 2000 строк SBT-лога» и «агент читает 50 строк ошибок» — это 30-50% бюджета. Оптимизация вывода сборки, точечное чтение файлов, отложенное измерение покрытия — каждое по отдельности мелочь, вместе — существенная экономия.

86.8% acceptance rate — хороший, но не идеальный результат

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

www.BrainTools.ru

Rambler's Top100