Когда-то мы считали, что перевести нашу внутреннюю админку с Python 2 на Python 3 — задача на грани возможностей. В команде даже ходила легенда, что два разработчика уже пробовали это сделать и после этого увольнялись.
Задача воспринималась примерно как экспедиция на Эверест. Наверное, можно. Но зачем рисковать жизнью? Рисковать своей жизнью не пришлось! У нас появился LLM, и я смело отдал миграцию ему. Через неделю у меня была рабочая админка на Python 3 и MR на 600 файлов. До прода было ещё далеко.
Расскажу про все этапы своего подхода к невозможному. Про новую «рутину» работы с агентами, про важность формализации и как вижу ближайшее развитие агентской разработки.
Привет! Меня зовут Илья, я тимлид backend-команды B2B Операционная в Иви. Мы разрабатываем внутренние сервисы, в том числе ту самую админку, о которой пойдёт речь.
Итак, начнём.
Древний монолит, который никто не хотел трогать
Наша внутренняя админка — довольно большой Django-монолит. Примерно 350 тысяч строк кода. Покрытие тестами — около 33%. Разрабатывать её начали ещё в 2010 году. Django 1, Python 2. Потом через проект прошло много команд и разработчиков. Что-то добавляли, что-то переписывали, что-то просто старались лишний раз не трогать.
Так она и прожила много лет.
Постепенно старый стек начал мешать вообще всему. Пакеты устарели. Новые библиотеки уже невозможно было нормально подключать. Например, когда мы делали авторизацию через ADFS, подходящей библиотеки под наш стек не оказалось. В итоге пришлось частично копировать код из более новой библиотеки и адаптировать его вручную. И над всем этим висел Python 2. Переписать хотелось давно. Но каждый раз задача выглядела слишком большой, слишком рискованной, и совершенно непонятно было, когда она окупится. Ну и легенда про двух разработчиков тоже не помогала.
А что если просто отдать это нейронке?
В какой-то момент мы начали активно использовать LLM в разработке. И у меня появилась довольно простая мысль: а что если вообще не пытаться оценить стоимость миграции? Просто запустить агента и посмотреть, что он сможет сделать. Если получится — отлично. Если через пару дней всё развалится — ну и ладно. Денег я на эксперимент почти не трачу, своё время тоже особо не инвестирую.
Так всё и началось.
Первым вопросом было: а на какой Python вообще переезжать? Можно было сказать «давайте сразу на 3.14», но тогда выяснилось бы, что половина наших библиотек давно умерла, другую половину нужно менять, Django надо обновлять, а вслед за ним наверняка полезет ещё несколько слоёв проблем.
Мне хотелось поменять как можно меньше вещей одновременно. Поэтому я поставил ограничение: Django пока не трогаем. Сначала просто избавимся от Python 2. Попросил нейронку проанализировать все зависимости и найти минимальную версию Python 3, на которой этот зоопарк сможет жить. Она покопалась в зависимостях, совместимости пакетов и предложила Python 3.6. Да, эта версия Python давно снята с поддержки. Но здесь была важна не современность стека. Цель была гораздо скромнее: сделать один большой шаг, разорвать зависимость от Python 2 и при этом не переписывать половину приложения. Обновление Django и дальнейший подъём Python — это уже другие задачи.
Критерии успеха тоже были максимально примитивными:
Docker-образ собирается;
приложение запускается;
тесты зелёные.
Всё.
Мне хотелось, чтобы дальше агент сам находил очередную проблему, исправлял её, проверял результат и двигался к следующей. Оставался маленький вопрос.
Как заставить его работать, пока я сплю?
«Продолжай»

Сейчас такой подход называют Ralph Loop. У меня все было максимально примитивно. Я написал скрипт, который запускал OpenCode несколько раз подряд.
cat run_agent.sh
#!/bin/bash iterations=1 model=ivi/gpt-5.2-codex while getopts ":i:" opt; do case "$opt" in i) iterations="$OPTARG" ;; *) echo "Usage: $0 [-i iterations]" >&2 exit 2 ;; esac done if ! [[ "$iterations" =~ ^[0-9]+$ ]] || [ "$iterations" -lt 1 ]; then echo "Error: iterations must be a positive integer" >&2 exit 2 fi for ((i=1; i<=iterations; i++)); do opencode run --model "$model" --agent build "продолжай" --print-logs --log-level INFO 2>&1 \ | tee ".agent/runs/$(date +%Y-%m-%d_%H:%M:%S)_${i}.log" done say "Yeah, Science!"
Каждый новый запуск получал один и тот же промпт: «Продолжай».
Всё.
Конечно, если просто написать нейронке «Продолжай», она ничего полезного не сделает. Нужно было каким-то образом передавать состояние между запусками. Для этого был AGENTS.md. Там лежало всё самое важное:
какая вообще цель проекта;
как собирать Docker-образ;
как запускать тесты;
что можно менять;
что нельзя менять;
как устроено окружение;
куда смотреть, если что-то сломалось.
Отдельно лежали задачи. Просто markdown-файлы. Агент запускался в новом контексте, смотрел, что уже сделано, какая задача сейчас в работе, и продолжал оттуда.
Структура файлов
AGENTS.md .agent/ ├── tasks/ # задачи ├── knows/ # знания по задачам ├── runs/ # логи запусков агентов └── STATE.md
cat AGENTS.md
- Ты запущен на MacOS - Бери любую задачу в статусе IN_PROGRESS на выполнение, если таких нет, бери любую задачу в статусе READY - Задачи хранятся в папке .agent/tasks - Создавай новые задачи при необходимости, дроби их на под задачи как тебе удобно - В имени задачи есть ее уникальный номер и статус, пример: .agent/tasks/T-001-READY.md - Формат файла задачи произвольный, но важно указывать блокеры обязательно со ссылкой на задачи - Когда меняешь статус задачи, отрази это в имени файла - Знания по задачам находятся в папке .agent/knows, в имени файла указывай номер задачи, пример: .agent/knows/T-001.md - Логи запуска агентов находятся в папке .agent/runs. - Состояние проекта находится в файл .agent/STATE.md, обновляй его по мере изменения проекта - Можешь произвольно менять данный файл для улучшения процесса разработки - Запускай скрипты с выводом логов в файл, читай его малыми порциями при необходимости, чтоб не засорить контекст - После завершения работы над задачи, делай git commit всех файлов проекта, в тексте коммита пиши кратко что сделал и номер задачи - Собрать докерфайл можно командой - docker compose -f docker_compose/docker-compose.yml build webadmin_dev - Полезные команды в файле justfile, можешь их дополнить ## Модель статусов задач READY → IN_PROGRESS → DONE READY → BLOCKED → READY READY | IN_PROGRESS → NEEDS_HUMAN | CANCEL
Идея мне тогда казалась прекрасной. Нейронка сейчас сама разобьёт большую задачу на маленькие. Сама их создаст. Сама выполнит. Я утром приду и посмотрю результат.
Не получилось.
Нейронка тоже умеет тупить
Периодически агент просто приходил в тупик. Причём неприятность была даже не в самом тупике. Я утром открываю проект и вижу: ничего не работает. А что он делал последние несколько часов — загадка. Попытался исправить одну ошибку, породил другую, потом пошёл куда-то ещё и в конце остановился.
Сначала я пытался вручную разбираться с последствиями. Потом понял: нужны логи.
Я начал сохранять каждый запуск агента. Теперь утром можно было открыть последнюю сессию и посмотреть весь путь: какую ошибку получил, что решил поменять, почему это не сработало и в какой момент уехал куда-то не туда.
Мы вместе с нейронкой разбирали очередной провал и меняли инструкции так, чтобы в следующий раз она смогла выбраться самостоятельно. И через какое-то время случилась забавная вещь.
Она действительно начала решать проблемы сама.
С созданием новых задач получилось хуже. Я надеялся, что агент сам хорошо декомпозирует миграцию. В итоге все восемь задач создал я ручками.
Это был один из первых важных уроков: большую мутную проблему агент пока может ковырять очень долго. Если человек заранее разрезал её на понятные куски — всё становится заметно лучше.
Первые шишки
Вообще это был мой первый большой эксперимент с автономными агентами, поэтому странных проблем хватало.
Контекст закончился
Если долго гонять одну сессию, контекст в какой-то момент переполняется. Агент начинает хуже понимать происходящее, потом совсем разваливается. На первых запусках я просто ловил это постфактум. К счастью, Ralph Loop частично спасал ситуацию сам собой: одна сессия умирала, следующая запускалась с чистым контекстом, читала состояние из файлов и продолжала. Потом я уже начал осознанно ограничивать размер сессий. Это оказалось очень важным. Контекст — тоже ресурс. Его можно загадить так же, как оперативную память в системе.
Docker не хотел собираться
Дальше началась классика. Пакет не ставится. Меняем версию. Теперь не ставится другой. Обновляем его. Третий пакет считает, что сегодня 2015 год и ничего нового после него существовать не должно.
Какое-то время сборку пришлось чинить довольно плотно вместе с нейронкой. Это был, наверное, самый ручной кусок всей миграции.
Тесты идут полчаса
Ещё выяснилось, что наш обычный CI для такой работы практически бесполезен. Полный пайплайн занимает около получаса. Представим цикл агента: поменял одну строчку → полчаса ждём → ошибка → поменял ещё одну → опять полчаса.
От такой автономности можно состариться.
Пришлось сделать возможность быстро локально запускать один конкретный тест или маленькую группу тестов. После этого скорость итерации стала совсем другой.И вот это уже гораздо интереснее самого промпта.
Если агент может за минуту сделать изменение и получить понятный ответ «да/нет» — он довольно хорошо движется сам. Если обратная связь приходит через полчаса и представляет собой 20 экранов CI-логов — начинается веселье.
Через неделю оно заработало
Примерно неделю я запускал эту штуку по вечерам и оставлял работать ночью. Утром смотрел, что произошло. Что-то подкручивал. Вечером запускал снова. И в какой-то момент Docker-образ собрался. Потом позеленели тесты. Я запустил админку локально. Потыкал несколько разделов.
Работает!
Это был очень странный момент. Задача, которую годами воспринимали почти как неподъёмную, внезапно просто запустилась у меня на ноуте на Python 3. Я был очень доволен.
Потом открыл merge request.
И перестал быть довольным.
600 файлов

В MR было около 600 изменённых файлов. Примерно 40 тысяч строк diff.
Я немного посмотрел на это.
Закрыл.
Открыл ещё раз.
Нет, меньше не стало.
Человеческое ревью такого объёма было практически бессмысленным. Конечно, можно посадить нескольких разработчиков и несколько дней листать изменения. Только уже через пару тысяч строк внимание закончится и дальше получится ритуал, а не ревью.
Сам diff тоже был очень разный: механические правки синтаксиса; изменения зависимостей; несовместимое поведение Python 2 и Python 3. Где-то агенту реально пришлось разбираться в коде.
Я начал рассказывать про результат команде. Сначала многие решили, что я шучу.
Потом посмотрели.
Нет, вроде правда работает.
А дальше возник уже совсем не смешной вопрос: кто будет это ревьюить и тестировать? Ответа не нашлось. Проект заморозили.
Четыре месяца ничего не происходило
Я уже мысленно похоронил миграцию. Эксперимент получился прикольный. Я доказал хотя бы себе, что такую задачу агент в принципе способен протащить. Ну и ладно.
Прошло примерно четыре месяца. И тут менеджер сообщает: мы всё-таки переезжаем на Python 3.
У меня примерно: «А. Хорошо.»
К этому моменту и сами инструменты уже немного изменились, и появилось больше примеров больших миграций с помощью LLM. Мы решили использовать нейронку ещё раз. Только теперь не как разработчика. А как первый уровень ревью.
Как ревьюить 40 тысяч строк
Идея была довольно простая. Человеку нет смысла одинаково внимательно смотреть весь diff. Нужно сначала понять, где вообще находятся опасные места.
Поэтому я попросил LLM пройтись по изменениям и выделить то, что действительно требует человеческого внимания. Механические изменения можно почти не смотреть. А вот изменение логики, сериализации, работы со строками, байтами, датами, кодировками и другими местами, где Python 2 и Python 3 ведут себя по-разному, уже интересно.
Нейронка выделила набор мест для ручной проверки. Например алгоритмы шифрования, обработка файлов, ключи кэша, правки в Dockerfile.
Мы их посмотрели. Ничего критического не нашли.
Это, конечно, не означало, что всё хорошо. 33% покрытия тестами никуда не делись. Оставалось проверить остальные 67% старым надёжным способом.
Руками.
Две недели людей против админки
Админкой пользовалось много команд. У каждой были свои разделы, свои процессы и какие-то совершенно уникальные сценарии, про которые ответственные разработчики самой админки могли вообще не знать. Поэтому решили собрать QA со всех причастных команд. Каждый тестировал свою часть.
Следующие две недели люди просто ходили по админке и проверяли, что всё ещё работает. Я ожидал катастрофы. Но багов оказалось неожиданно мало. Где-то двадцать за всё тестирование.
Для 40 тысяч изменённых строк результат выглядел подозрительно хорошо.
В какой-то момент решили: хватит. Можно катить на прод. Раскатились. И снова баги.
Первый релиз был недолгим
После релиза довольно быстро выяснилось, что кое-что мы всё-таки пропустили. Появились ошибки. Их было немного, но критичны для пользователей. Мы откатились.
Вот здесь нейронка снова оказалась особенно полезной. В основном баги были одного типа: ошибка проявилась в конкретном месте, но по характеру было понятно, что такой же паттерн может быть разбросан ещё по десяткам файлов. Человеку пришлось бы идти grep'ать проект, смотреть каждый случай и решать, относится он к проблеме или нет. Агенту можно было показать один баг и сказать примерно: «Вот причина. Найди все места, где мы сделали то же самое». Он довольно быстро находил повторения и правил их пачкой.
Пример необычного бага (угадай, что сломалось здесь):
e = None try: 1/0 except Exception as e: logger.warn(e) if e: messages.error(request, "Ошибка: %s" % e)
В итоге быстро поправили все. Снова выкатили. На этот раз админка осталась в проде.
Всё-таки переехали

Сейчас админка работает на Python 3. Если считать только активную работу, миграция заняла где-то месяц-полтора. Но между первой рабочей версией и реальным продолжением проекта была четырёхмесячная заморозка, поэтому календарно всё растянулось примерно на полгода.
Для меня главное даже не это. До эксперимента миграция выглядела как задача, за которую никто особо не хочет браться. После эксперимента вопрос изменился. Не «можно ли это вообще сделать?», а «как это проверить и довезти до прода?». Для старого legacy-проекта это очень большая разница.
А теперь самое интересное
После этой истории я довольно сильно изменил представление о том, что вообще значит «агент умеет программировать».
Поначалу мне казалось, что самое важное — модель. Нужна модель поумнее. Промпт получше. Контекст побольше.
На практике большая часть успеха оказалась вообще не там.
Тесты — это суперсила агента
Агент хорошо работает там, где мир может быстро сказать ему: получилось или нет.
Собрался Docker? Да или нет.
Запустился сервис? Да или нет.
Прошёл тест? Да или нет.
Это почти идеальная среда. Агент может сделать изменение, получить обратную связь, скорректировать себя и попробовать снова.
Поэтому сейчас я воспринимаю тесты немного иначе. Раньше тесты в основном защищали разработчиков от регрессий. Теперь они ещё и дают агенту возможность работать самостоятельно.
Чем больше вещей в проекте можно автоматически проверить, тем дальше можно отпустить нейронку без человека. Из этого у нас получился ещё один эффект: разработчики стали гораздо реже писать тесты руками.
Мы задаём правила, структуру и требования. Агент генерирует сами тесты, а человек уже смотрит результат. Раньше тест часто не писали просто потому, что лень. Теперь эта причина полностью исчезла.
Автономность — это не промпт
В начале эксперимента мне хотелось найти правильный промпт. Потом выяснилось, что «Продолжай» вполне достаточно. Главное — всё остальное. Файлы с состоянием. Задачи. Логи. Команды. Быстрые тесты. Правила работы. Возможность начать новый контекст и восстановить происходящее.
То есть автономность агента оказалась свойством не модели, а системы вокруг неё. Если агент может потерять контекст и через минуту снова понять, где он находится, — это работает. Если вся память проекта существует только внутри одного огромного чата — рано или поздно всё закончится плохо.
Оказалось, писать код — не главный тормоз
После этого мы начали гораздо активнее использовать LLM в обычной разработке. Сейчас процесс разработки выглядит так:
В режиме планирования пишу агенту: давай сделаем задачу 2255.
Агент идет в трекер задач, изучает требования, задает уточняющие вопросы.
Перевожу агента в режим разработки и пишу: делай.
Когда все готово, проверяю руками новый функционал.
Создаю новую сессию, в режиме планирования прошу агента провести ревью на требования и архитектурные требования.
Получаю список замечаний, по каждому задаю уточняющие вопросы в форкнутой ветке, там же прошу их исправить.
Прошу агента создать МР и заполнить тикет. Все!
Вы великолепны.
Теперь код разработчик пишет меньше. Казалось бы, тикеты должны начать летать.
Не начали.
Написание кода — это только один кусок длинной цепочки жизненного цикла задачи. Есть ещё: груминг, ревью, тестирование, релиз. Мы сильно ускорили только разработку. После неё тикет, как и раньше, ждёт ревью, ждёт тестирования. Потом выясняется, что задача была описана не до конца. Разработчик идёт задавать вопрос менеджеру. Менеджер идёт к заказчику. И так по кругу.
Внезапно выяснилось, что быстрое написания кода почти не изменило скорость всей системы. Сейчас мне кажется, что следующий большой выигрыш находится как раз не в генерации кода. А в автоматизации груминга, ревью, тестирования и формализации.
Задачи пришлось научиться нормально описывать
Есть ещё неприятная особенность. Разработчик читает мутный тикет и думает: «Что-то здесь странно». Потом идёт к менеджеру и спрашивает.
Агент иногда читает мутный тикет и думает: «Понятно». И начинает писать код. Через несколько минут получается вполне работающая реализация совершенно не того, что имелось в виду.
Поэтому с агентами качество постановки задачи стало гораздо важнее. Неясность никуда не исчезает. Просто раньше её замечал разработчик до написания кода, а теперь она может приехать уже на тестирование.
Получился неожиданный эффект: чем больше мы автоматизируем разработку, тем больше приходится формализовывать всё вокруг неё.
Что в итоге
Если бы мне в начале этой истории сказали, что главным промптом для миграции 350 тысяч строк legacy будет слово «Продолжай», я бы не поверил. Но примерно так и получилось.
Конечно, сама нейронка ничего магически не решила. Я создавал задачи. Настраивал окружение. Чинил проблемы, из которых агент не мог выбраться. Команда делала ревью. QA две недели ходили по админке. После первого релиза мы вообще откатились.
Но всё это почему-то не кажется главным. Главное изменение произошло в другом. Задача перестала выглядеть неподъёмной. LLM не убрала инженерную работу. Она просто сдвинула её в другое место. Меньше времени ушло на механическое изменение тысяч файлов. Больше на то, чтобы поставить понятную цель, построить обратную связь, разбить работу на части и проверить, что получилось.
Именно в этом сейчас самая интересная часть работы с агентами. Не придумать волшебный промпт. А построить такую среду, где им можно сказать:
«Продолжай».
И уйти спать.

