Как нам в Домклик LLM рефакторинг делала

Привет! Меня зовут Сергей, я Android-разработчик команды Новостроек в Домклик. Довольно давно наблюдаю за «сценой» ИИ в разработке, надо вовремя сменить профессию на электрика, эксперта в электрификации ИИ ЦОДов.
Я постоянно вижу, как кто‑то навайбкодил новый (хорошо забытый) SaaS, или автоматизировал разработку новых фич, а может быть даже вообще заменил всю команду разработчиков в своём стартапе несколькими агентами. С другой стороны, так же часто я вижу и посты «Что делать — агент снёс мою продовую БД?!» и «Ищу опытного разработчика, сам я уже не в силах дорабатывать вайбкод».
На мой взгляд, это связано с недостатком процесса рецензирования. Ведь LLM способна генерировать код с такой скоростью, что его сможет проверить только… другая LLM! Особенно если вы стартап или у вас мало разработчиков.
Я лично ещё не готов доверить моделям проверку кода и принятие окончательного решения о его качестве. Роль помощника в процессе проверки — да, но не финальное слово.
Сам я пробовал писать фичи с помощью агентов, писать модульные и UI-тесты. Переносил одну большую фичу с Android на iOS; опыт, кстати, оказался положительным. С тестами, когда их немного, модель справляется неплохо, но при разработке целой фичи всё равно требует «ручного ввода». Пока у меня не получилось вот так сходу получить код, который я готов «as is» отдать на ревью. Так или иначе, шёл и допиливал руками. Это однозначно ускоряло мою разработку, но на «автоматизацию» не тянуло.
Я пробовал даже разрабатывать целые проекты с нуля, используя только агенты. Спортивный интерес. При хорошо описанной архитектуре и поставленной задаче получалось вполне неплохо, но с ростом проекта становилось всё сложнее поддерживать это всё и добавлять новые фичи (именно то, на что жалуются другие люди).
Так или иначе, я думал, что же можно автоматизировать с помощью агентов… У меня есть гипотеза, что качественного результата от LLM, можно добиться несколькими слагаемыми:
-
качественным DoD;
-
технической возможностью проверки результата выполнения задачи;
-
циклом обратной связи с моделью, который может донести результат проверки.
Имея эти три составляющие, мы, возможно, получим хороший результат. Свой подход я буду проверять на наиболее рутинных задачах, за которые неохотно берутся кожаные мешки живые разработчики: исправление ошибок линта и рефакторинг тестов.
Экспозиция
Хочется сделать небольшое отступление и рассказать вам про наш проект. Мы разрабатываем Android-приложение Домклик. Оно большое (250 Gradle-модулей), написано на Kotlin и корутинах, использует, в основном, MVVM с состоянием и Compose (не считая legacy-частей). И у нас, как у взрослой команды, есть свои правила: несколько стандартов, релевантных статье.
Мы пишем модульные тесты для бизнес-кода. Покрываем репозитории, сценарии использования и View-модели. Тесты мы пишем на JUnit4 и используем MockK. Раньше у нас был Mockito, но когда переезжали с Java на Kotlin, то перешли с Mockito на MockK.
Также у нас есть свои правила для линта. Они написаны по мотивам наших стандартов разработки, и мы стараемся не добавлять новых ошибок и исправлять существующие. Процесс этот проходит в рамках технического долга, не быстро.
Рефакторинг тестов
Задача довольно простая: переписать тесты с Mockito на MockK. Этот проект у нас шёл уже давно, но оставалась немного тестов (около 100 тестовых классов), у которых не было владельцев, и всё никак не доходили руки их переписать.
Однако простой промпт вроде «Найди все тесты на Mockito, перепиши их на MockK, не совершай ошибок» работал не очень хорошо:

Думаю, причин, почему он работал не очень хорошо, две: у нас большой проект и много тестов. Тут видны все признаки Context Rot.
Context Rot — это снижение производительности модели с ростом контекста. Модель может перестать справляться даже с самыми простыми задачами.
Также стоит держать в уме особенность Lost in the Middle: информация в середине контекста обрабатывается хуже, чем в конце и в начале.
Как мне быть? Оказывается, есть утилита Ralph (или Open Ralph, если нужна поддержка большего количества агентов). Эти утилиты представляют из себя workflow loop, который умеет запускать агент с нужным промптом и задачей. Причём каждый раз агент запускается с чистым контекстом.
Подробнее про Ralph
Часто задачи LLM‑программирования решаются запуском CLI Сlaude Сode, OpenCode или аналогичным инструментов в headless-режиме. В таком подходе нет ничего плохого, однако когда надо запустить много задач автономно, возникают проблемы: что делать, если агент завис? как после первой задачи запустить следующую? Решить их помогает Ralph.
В режиме задач Ralph будет брать задачи из файла .ralph/ralph_tasks.md и для каждой из них по очереди запускать агент. Во время прогона задачи Ralph будет мониторить stdout агента на признак «стоп-слова» (completion promise). Если агент закончил выполнение, но не ответил заданной фразой, то Ralph перезапустит его с задачей. Когда задача будет выполнена, Ralph перейдёт к следующей.
Пример запуска:
ralph —prompt-file workflow/code_review.md —tasks —task-promise «READY_FOR_NEXT_TASK» —max-iterations 200 —model qwen3dot6-coder-35b-A3b
Задача переписать тест — атомарная: агенту не нужно помнить, что было в прошлый раз, ему нужны только правила. В итоге у меня получился промпт, который работал стабильно и выдавал ожидаемый результат.
Промпт агента для рефакторинга тестовYou will receive ONE test file path as your current task. The file is the path from project root. The project is located in Bench dir (append Bench/ in front of tests path you receive). Use tasks/unit_tests/knowledge.md for knowledge from previous iterations. ## Folder structure You are in the parent folder of the android project. This folder contains all AI related files, tasks, docs etc. Android project is located in Bench dir, which means that if you need to run build, or make a commit you need to do it inside the Bench folder. ## WorkFlow 1. Rewrite the test with mockk instead of mockito 2. If tests is already using mockk make sure it does not have any mockito imports (Also remove @RunWith(MockitoJUnitRunner::class) it is unnecessary for mockk) 3. Run the specific test class and make sure it passes 4. Commit changes 5. If you found any useful knowledge for next iterations (e.g. something that you tried first, but it didn’t work) write it in file tasks/unit_tests/knowledge.md. Make it short, but useful. ## Rules 1. If test fails after rewrite — try to fix it (max 5 attempts), then skip and output: <promise>READY_FOR_NEXT_TASK</promise> with a note 2. Only replace Mockito mechanics with MockK equivalents. Do not change test logic, assertions, or test structure. 3. Commit message format: «PPSM-4988: переписал [TestClassName] на mockk» 4. Run only the specific test class being changed 5. <IMPORTANT> DO NOT USE mockk(relaxed = true), relaxUnitFun is permitted 6. <IMPORTANT> DO NOT USE @MockK ## Docs For reference see docs/unit-test-guide.md and When done (pass or skip), output: <promise>READY_FOR_NEXT_TASK</promise> ## Migration cheatsheet 1. MockitoKt.argThat { it is Success } -> match { it is Success } (No need to import it, it will work in mockk scopes as is)
В промте есть несколько интересных пунктов:
-
knowledge — я заметил, что в каждой итерации агент повторяет одни и те же действия, поэтому я сказал ему, чтобы он записывал свой опыт. Это помогло.
-
READY_FOR_NEXT_TASK — тот самый completion promise, агент должен вывести его в конце, чтобы Ralph понял, что тот закончил.
-
Rules — это правила которые агент не должен нарушать, они закрывают как основы его работы, так и нюансы. Например, мы не хотим использовать MockK-аннотации.

За ночь такой агент переписал более 100 тестов с Mockito на MockK. Утром мне оставалось только открыть ПР и ждать отзыв от коллег. На ревью мы нашли места, где агент «схалтурил», и тут у меня появился выбор: можно либо доработать промпт, либо научить модель работать с code review. Выбор был очевиден.
Измени промт после ревью, и модель сделает одну задачу. Научи её читать комментарии рецензентов, и она будет делать задачи всю жизнь
Промежуточный итог: чётко поставленная задача и чёткое условие приёмки. Если тест пройден, то задача выполнена, если нет — нужно исправлять.
LLM в код ревью
Я быстренько написал (нагенерировал) скрипт, который ходил в Bitbucket, забирал оттуда по API задачи и выдавал их в формате MD. Что‑то типа такого:
— [ ] Исправить комментарий по code review ID: 567824 File: app/src/test/java/me/srggrch/fakeapplication/feature/domain/VeryCoolUseCaseImplTest.kt Line: 21 + P1: Запрещено использовать `relaxed = true`
Я написал промпт для исправления review. Сразу показываю финальный вариант, было много итераций.
Промпт агента для исправления комментариев code reviewYou will receive code review comments in this format: fix code review comment ID: <id> File: <file path relative to project root> Line: <line number> P1: <reviewer comment> Your task is to fix each comment and check other files from PR for similar problems. Project is located in **Bench/** dir (prepend `Bench/` to all file paths). — ## IRON LAWS — never violated, no exceptions 1. NEVER use `mockk(relaxed = true)` — `relaxUnitFun` is allowed 2. NEVER use `@MockK` annotation 3. NEVER use any Mockito classes 4. NEVER ship failing tests — fix before commit (max 3 attempts) 5. ALWAYS run tests before finishing — concrete terminal output required as evidence 6. ALWAYS check ALL changed files in the branch — not just the file in the comment 7. NEVER refactor unrelated code — only what the comment asks for 8. NEVER edit files inside .ralph dir If any Iron Law is violated — stop and correct immediately before continuing. — ## WorkFlow ### Step 1 — Read context Open the file. Read ±20 lines around the specified line number to understand context. ### Step 2 — Clarify if needed If the comment is unclear, append to `tasks/review/feedback.md`: «` * <ID> — <your question> «` Then make the most reasonable interpretation and proceed. ### Step 3 — Apply fix Fix the issue in the specified file. Follow Iron Laws. ### Step 4 — Scan all PR files for the same issue «`bash cd Bench && git diff origin/develop…HEAD —name-only «` Search every changed file for the same pattern. Fix all occurrences. ### Step 5 — Run targeted tests Determine affected modules from changed files. If the changed file is a test class: «`bash cd Bench && ./gradlew <module>:testDomclickCommonDebugUnitTest —tests <FullyQualifiedTestClassName> «` If the changed file is not a test — find related tests first: «`bash grep -r «ClassName» Bench —include=»*Test*.kt» -l «` Then run those specific test classes. For multi-module changes: «`bash cd Bench && ./gradlew <mod1>:testDomclickCommonDebugUnitTest <mod2>:testDomclickCommonDebugUnitTest «` Ignore the result only if the error is exactly: `No tests found for given includes:` ### Step 6 — Build check (non-test files only) «`bash cd Bench && ./gradlew assembleDebug «` ### Step 7 — Run verify_review_fix subagent Pass the original comment verbatim. — If subagent returns OK → proceed — If subagent returns a file list → restart from Step 1 for each file ### Step 8 — Run verify_build subagent — If returns OK → proceed — If returns failing tests → fix them, then re-run verify_build ### Step 9 — Knowledge capture If you learned something useful, append to `tasks/review/knowledge.md` (1–2 lines, practical). ### Step 10 — Commit «`bash cd Bench && git add -A && git commit -m «PPSM-4988: code review fix <short description>» «` — ## VERIFICATION-BEFORE-COMPLETION You CANNOT output READY_FOR_NEXT_TASK without providing all three: 1. The exact `./gradlew` command you ran 2. The actual terminal output (last 20 lines minimum) 3. Explicit confirmation that no test failures appear in the output If tests were skipped due to `No tests found` — quote that exact error line. If no tests exist for the change — show the grep result proving no test files reference the changed class. Required format before READY_FOR_NEXT_TASK: «` ## Verification Evidence Command: ./gradlew <module>:testDomclickCommonDebugUnitTest —tests com.example.FooTest Output: <paste terminal output here> Result: PASS / No tests found (reason: …) «` — ## Docs For reference see `docs/unit-test-guide.md` When done, output: READY_FOR_NEXT_TASK
Когда я писал этот агент, я уже ориентировался на опыт других людей и обратил внимание на репозитрий ECC, который содержит много разных навыков и агентов для Claude Code и других подобных инструментов. На что тут стоит обратить внимание
-
IRON LAWS — более структурированная эволюция Rules из прошлого промпта.
-
Шаги теперь расписаны точнее. Это вообще хорошее правило, если вы что‑то не сказали модели, то она это не сделает.
-
VERIFICATION‑BEFORE‑COMPLETION — отдельный пункт про верификацию перед окончанием работы агента.
И вот тут начались проблемы.
Наша относительно небольшая модель отказывалась искать ошибки в файлах кроме того, что указан в комментарии, а писать 16 раз задачи в разных файлах никто не будет. Я добавил агент, который искал задачи во всём диффе. Стало лучше, но теперь агент отказывался прогонять тесты.
Мне пришла идея: мы же хотим иметь детерминированные условия приёмки, и они тут есть. Понять что комментарий исправлен сложно, но тесты проверить мы ещё как можем!
Как запустить тесты после прогона агента и условно перезапустить его с новыми входными данными? Написать свой цикл наподобие Ralph, но под конкретные задачи. Код я прилагать не буду, расскажу сам алгоритм. Идея простая, как пять копеек:
-
Мы идем в Bitbucket и забираем оттуда список задач.
-
Скрипт запускает агент по их нарезке. Этот агент получает задачу и ищет в диффе все файлы где есть проблема.
-
Дальше запускается Ralph с задачами на исправление ошибок.
-
После исправления скрипт запускает тесты для всех модулей, затронутых диффом. Тут обязательно надо использовать ‑‑continue в задаче Graddle, чтобы собрать ВСЕ тесты, даже если где‑то ошибка компиляции.
-
И если у нас есть тесты, которые падают, то скрипт создаёт для Ralph задачи на исправление тестов и перезапускает его.
Не с первой итерации, но такой подход заработал. Он стабильно выдавал результат, где все проблемы были исправлены и тесты проходили. Будет ли этот подход работать на более комплексных комментариях? Честно сказать, не знаю, нам это только предстоит выяснить.
Ещё один промежуточный итог:
ПР, содержащий рефакторинг всех оставшихся тестов на Mockito, около 100 штук, был влит после того, как LLM сама исправила правки.
Исправляем ошибки линта при помощи LLM
Процесс для тестов мне очень понравился, и я начал думать над тем, какие ещё задачи у нас есть. Исправление линтов! Это же отличная задача, в которой есть чёткий DoD. На основе цикла из предыдущего сценария я быстро накидал следующее:
-
Обнови baseline lint’a и получи все проблемы по всем модулям по конкретной проблеме.
-
Создай задачи для каждого модуля.
-
Запусти Ralph.
-
Проверь, что все линты исправлены, если нет, то снова запусти Ralph.
Первый пункт — это отдельный скрипт, так мы можем написать навык, который научит нашу модель проверять линты во время работы. Скрипт ещё умеет отдавать список проблем в модуле. Я поставил этот процесс выполняться на ночь, и вот результаты:
Время работы 10 часов, за это время агент исправил 1700 ошибок линта.
Это, как по мне, отличный результат! Но есть, как в том анекдоте, нюанс. Невозможно вменяемо посмотреть ПР на 500 файлов, даже если там тривиальные изменения. И вторая проблема заключается в том, что сразу появились конфликты. Я доработал скрипт так, что он берёт батчи по 100 файлов и создаёт по три таких ветки за раз. Такие ПРы уже можно смотреть.
В процессе я доработал скрипт, научив его создавать задачи в Jira по API, у нас нельзя пушить ветку без привязанной задачи.
Идеи для развития
Задачей этого небольшого исследования не был полет на Луну, рефакторинг тестов и исправление ошибок линта задачи не сложные, но и останавливаться на этом я не хочу. Линтов много разных, пока я не верю, что наша модель вытянет рефакторинг RX → корутины. Но править такие небольшие ошибки линта вполне себе, а это уже автоматизация и экономия ресурса, так как делать кому‑то это нужно будет.
Так вот, точки роста, которые я вижу:
-
Попробовать развернуть этот скрипт на отдельной машине, чтобы он мог работать 24/7. Задач и хотелок у нас много, да и кодовая база немаленькая.
-
Доработать финальную версию цикла. Он должен быть более универсальным, сейчас для каждой новой задачи приходится его допинывать
-
Понять, как расширить узкое место в виде ревью кода. Как я писал в начале, ИИ может генерировать гораздо больше кода, чем мы в состоянии посмотреть.
Практическое применение
Процесс доказал свою жизнеспособность, и при дальнейшем развитии сможет снять с команды разработки задачи по простому рефакторингу, миграциям и исправлению техдолга. Так как скрипт не человек и может работать круглые сутки, то задачи будут выполняться быстрее, а разработчики смогут заняться более интересными и важными задачами.
Вывод
Я не люблю термин «вайбкодинг» и образованные от него слова вроде «навайбкодил». Когда я их слышу, у меня сразу возникает в голове образ человека, который не разбирается в коде. Этот человек зашёл в ChatGpt и попросил сделать ему новый Facebook и не делать ошибок.
LLM можно использовать для решения задач, где вместо «вайбов» будет понятный, а главное, ожидаемый результат. Разработка и настройка этих процессов у меня заняла около пяти дней. Вы скажете, что за это время можно было и руками всё это сделать, но процесс и опыт можно переиспользовать. Если опустить фазу исследования, то результат приятный:
100 отрефакторенных тестов за ночь и 1700 исправленных линтов за ещё одну ночь.
Update
Пока я писал эту статью, появилась новость, что Anthropic тихо — а когда вы читаете, может быть, и не тихо, — раскатили фичу /workflow. Судя по тому, что пишут, фича позволяет внутри Claude Code сделать что‑то вроде Python-скриптов из статьи, которые будут оркестрировать ИИ-агентов, но на JavaScript.
По сути. эту новость можно считать подтверждением жизнеспособности описанного подхода. В Anthropic тоже поняли, что для оркестрации LMM скрипт с чёткими условиями подходит больше, чем недетерминированная модель.
Источник: habr.com

Добавить комментарий
Для отправки комментария вам необходимо авторизоваться.