Сразу и без кокетства, чтобы дальше читать с правильными ожиданиями.
Код в этом проекте пишет ИИ. Целиком. Бэкенд на Go — язык, которого я не знаю: я не писал на нём ни строчки ни до, ни во время проекта. Фронт на TypeScript и Vue — тут я в теме и мог бы читать, но не читаю. В код я не смотрю вообще: ни в реализацию, ни в тесты, ни в диффы. Я пишу задачу и проверяю результат — всё.
Это не признание в лени, это условие эксперимента. Мне было интересно, где именно упрётся такой режим — и он упёрся, причём не там, где я ждал.
За два месяца у меня сложился образ, который объясняет происходящее лучше любой аналитики. ИИ-разработчик — это гениальный ленивый дед с деменцией. Он помнит наизусть всю стандартную библиотеку и напишет вам за минуту то, на что вы потратите день.
При этом у него деменция, и проявляется она не так, как принято думать. Дело не в том, что он «забыл вчерашний разговор» — вчерашнего разговора для него и не было. Дело в том, что вылезает внутри одной живой сессии:
Течёт контекст. Разговор длинный, и всё, что было в начале, постепенно вымывается. Договорённость, достигнутая час назад, к концу сессии существует уже наполовину.
Уезжает мыслями в другое место. Обсуждаете конкретную функцию — а он через два хода правит соседний модуль, про который речи не было, потому что там ему показалось интереснее.
Не запоминает, если явно не сказать «запомни». Само по себе ничего не откладывается. Вывод, к которому вы пришли вместе полчаса назад, живёт ровно до конца сессии, если его не записали в файл принудительно.
Переспрашивает то, что уже обсудили. Один и тот же вопрос может прийти дважды и трижды за сессию — вы отвечаете, он соглашается, через двадцать минут спрашивает снова. Ровно как с дедом, который третий раз за вечер спрашивает, кто приходил утром.
Останавливается посреди работы с вопросом, ответ на который лежит в трёх токенах от него. Классика жанра: «а как называется поле в этой таблице?» — при том, что таблица лежит в репозитории, а
grepон умеет. Или того лучше — спрашивает то, что дословно написано в постановке задачи, которую я дал ему пять минут назад. Каждая такая остановка стоит мне переключения внимания, а их за вечер набирается десяток.Периодически нарушает прямые инструкции. Не «не понял», а именно нарушает — те самые, которые заданы явно, крупно и лежат в конституции, которую он читал в начале сессии. Не всегда. Достаточно редко, чтобы вы расслабились, и достаточно регулярно, чтобы это гарантированно случилось.
И отдельно, самое неприятное для планирования: дед меняется без предупреждения. Выходит новая модель — и старая, на которой у вас всё было отлажено, начинает вести себя иначе. Это моё субъективное наблюдение, доказать я его не могу, и в этом-то и проблема: у вас нет исполнителя с постоянными характеристиками. Тот, кто в июле держал инструкцию, в сентябре может её ронять, а вы даже не поймёте, вы ли что-то изменили.
И он ленив. Это не фигура речи и не про скорость — он работает мгновенно. Лень тут ровно одна: если где-то можно схалявить, он схалявит. Тест не проходит? Так давайте почистим таблицы. Долгий прогон? Так вот же результат в кэше, зелёный. Длинный лог? Покажу последние двадцать строк, там же самое главное.
Отдельная форма той же лени, самая коварная: если вы явно не сказали куда-то посмотреть — он не посмотрит. Спрашиваешь, почему у заезда не считается мощность. Вместо того чтобы сходить в базу и глянуть, что там на самом деле лежит, он строит правдоподобную версию из головы и излагает её уверенным тоном. Данные под рукой, запрос — одна строка, но никто же не просил.
Отсюда и половина правил в этой статье: они звучат как «сходи и проверь по источнику», потому что по умолчанию он не сходит и не проверит.
Гениальность его — тоже данность, и она никуда не девается: этот же дед за минуту пишет то, на что у меня ушёл бы день. Работать приходится с деменцией и ленью. Деменция лечится записками, которые он читает каждое утро, и короткими сессиями, пока контекст не потёк. Лень — заборами, через которые короткая дорога не проходит. Нарушение прямых инструкций не лечится ничем словесным — только машиной, которая не пустит.
Статья про то, из чего эти записки и заборы у меня выросли — а выросли они все из аварий.
Тексты вида «ИИ написал мне стартап за выходные» обычно заканчиваются на туду-листе. Здесь другой масштаб, поэтому и грабли другие.
Что за проект и сколько его
Сервис для велосипедистов: подключаешь часы или велокомп, заезды сами прилетают на сайт, склеиваются из нескольких устройств, считаются зоны, нагрузка и форма. Плюс агрегатор анонсов заездов из телеграм-чатов и протоколов гонок.
Цифры на сегодня, из репозитория:
Первый коммит |
12 июля 2026 |
Коммитов |
1719 |
Go, продакшн-код |
~190 000 строк |
Go, тесты |
~207 000 строк |
Фронт (Vue + TS) |
~102 000 строк |
Фронт, тесты |
~87 000 строк |
Тестовых функций Go |
4744 |
|
5025 |
Миграций БД |
297 |
Стек: Go + PostgreSQL/PostGIS, Vue 3 + TypeScript, Docker, GitLab CI, два независимых сервиса (API в РФ и коллектор в ЕС, между ними mTLS).
Тестов по объёму больше, чем кода. Это не признак дисциплины, это единственный найденный мной способ вообще работать с ИИ.
Без TDD цикл выглядит так: агент чинит то, что вы попросили, и попутно ломает что-то соседнее — потому что не держит в голове всю систему и не может её всю перечитать. Вы этого не замечаете, потому что не читаете код. Через неделю у вас продукт, где половина фич тихо перестала работать, и вы не знаете, какая правка какую сломала.
Поэтому правило простое и без исключений: сначала падающий тест, потом код. Не «покрой тестами потом» — потом не бывает. Тест здесь работает не как проверка качества, а как ремень безопасности для всего остального кода: он не даёт починке одного места уронить соседнее незаметно.
Как устроена работа
Раз ревью кода нет, вопрос ставится иначе: что тогда вообще удерживает систему от развала?
Ответ, к которому я пришёл за два месяца: три вещи — тест как контракт до написания кода, машинные гейты вместо человеческих глаз и проверка на живом продукте вместо отчётов агента. Ни одна из них не требует, чтобы я открыл хоть один файл.
Цикл на каждую задачу выглядит так:
Я формулирую задачу словами: что должно происходить, при каких условиях, что считается ошибкой.
Агент пишет падающий тест.
Агент пишет код до зелёного. Ни тест, ни реализацию я не открываю — ни в Go, ни во фронте.
Полный прогон, повешенный на хук и обрывающий push на красном.
Я проверяю результат как пользователь: открываю сайт, гружу свой реальный заезд, смотрю на цифры.
То есть моих действий тут ровно два: сформулировать задачу и проверить результат руками. Всё между ними — чёрный ящик, в который я не заглядываю принципиально.
Пункт 5 тут важнее, чем кажется. Когда не читаешь код, единственный настоящий источник правды — продукт на боевой поверхности. Не «тесты зелёные», не «агент отчитался», а страница, открытая в браузере, и данные, которые на ней видно.
И вот тут стоит сказать вещь, из-за которой вся эта схема перестаёт выглядеть экзотикой. Ровно так же я работаю с людьми. Ставя задачу живому разработчику, вы тоже не читаете за ним каждую строчку: вы описываете, что должно происходить, договариваетесь о критерии готовности и потом проверяете результат. Никто не сидит над плечом коллеги, сверяя его циклы.
Разница с ИИ не в принципе, а в двух параметрах. Первый — скорость: человек приносит фичу в четверг, дед приносит пятнадцать за вечер, и ваша пропускная способность на проверке становится узким местом системы. Второй — отсутствие памяти и совести: живой разработчик, сломав соседнюю фичу, обычно об этом скажет, а если поправит чужой тест «чтобы прошло» — то запомнит, что так делать не надо. Дед не скажет и не запомнит.
Поэтому всё, что дальше, — это не изобретение нового метода разработки. Это обычный менеджмент исполнителя, у которого выкрутили скорость на максимум, а память и осторожность — в ноль.
В корне репозитория лежит CLAUDE.md — конституция для агента, которую он читает в начале каждой сессии. Там не стиль кода, а инварианты домена и запреты, выведенные из уже случившихся аварий. Например:
Поездка склеивается из N файлов-сегментов (велокомп + часы + …). Любую метрику (пульс, каденс, мощность…) проверять по ВСЕМ сегментам поездки. Отсутствие метрики в одном файле-сегменте ≠ отсутствие её в поездке.
Это не абстрактное пожелание. Без этой строки агент раз за разом писал код, который смотрит первый файл и делает вывод обо всём заезде.
Важная оговорка про всё, что дальше: гарды, которые я описываю, придумывал не я в одиночку и почти всегда не с первого раза. Схема одна и та же — сначала что-то ломается на проде или в данных, я это вижу как пользователь, дальше сажусь с агентом разбирать причину, и уже из разбора рождается защита. Ни один из этих заборов не был построен превентивно — все они реакция на уже случившееся. Это, пожалуй, главный вывод статьи, и в конце я к нему вернусь отдельным пунктом.
Режим тимлида: почему один агент перестал справляться
Схема выше («задача → тест → код → проверяю на сайте») работала ровно до тех пор, пока проверка занимала пять минут.
Дальше начался неприятный эффект масштаба. Открываю готовую фичу, иду по ней как пользователь, и нахожу не один баг, а пятнадцать мелких. Тут подпись не влезла, тут после сохранения не обновился список, тут Enter в форме не сабмитит, тут дата в неправильном часовом поясе, тут кнопка «в избранное» без подсказки. Каждый косяк по отдельности: работы на десять минут. Все вместе складываются в полдня диктовки в чат, по одному, с ожиданием после каждого.
Но это ещё полбеды. Беда в том, что список не заканчивается. Пишешь в чат первую партию правок, агент уходит их делать, а ты продолжаешь ходить по продукту и находишь ещё десять. И вот тут выясняется, что деть их некуда:
Написать сразу. Бросит то, что чинит, и побежит за новыми. Половина текущей работы останется недоделанной, а вы даже не узнаете какая.
Написать «доделаешь потом». Забудет. Не всегда, но достаточно часто, чтобы на это нельзя было опереться. И вы уже не вспомните, что именно потерялось: диктовали-то по памяти, на ходу.
Складывать в отдельный файл. Пробовал, совсем неудобно: сидишь и ведёшь бухгалтерию собственных находок вместо того, чтобы просто сказать, что не так.
Так родился режим, который у меня называется тимлидом. Отдельная команда, которая переключает сессию в диспетчера:
сама код не пишет. Вообще. Это отдельный запрет в промпте;
принимает от меня список найденного скопом и раскладывает по задачам;
раздаёт их параллельным агентам, в промпт каждого зашит TDD-протокол;
не принимает работу без пруфа «красный → зелёный»: агент обязан показать падавший тест и его же зелёным после правки;
держит очередь и запускает следующую задачу по мере освобождения слота.
Что это дало на практике: я хожу по продукту и диктую всё, что вижу, в любой момент и в любом количестве. Нашёл ещё десять правок, пока предыдущие в работе, пишу их следующим сообщением. Дальше не моя забота: лид кладёт их в очередь, никого с полдороги не снимает и ничего не теряет.
Отдельная особенность режима: лида приходится регулярно возвращать в его роль. После нескольких задач в одной сессии он начинает писать код сам. Не со зла: задача мелкая, а он же умеет. Каждый раз напоминаю, что он тимлид и код не пишет, его дело раздавать. Тот же дед с его короткой дорогой: делегировать дольше, чем сделать самому.
Три вещи пришлось дописать в правила этого режима, и все после аварий.
Автономность. Первая версия спрашивала подтверждения на каждую задачу из очереди: «берём следующую?». Это бесит и убивает весь смысл. Теперь правило такое: всё, что вытекает из уже заказанного, исполняется подряд без вопросов. Спрашивать разрешено только про по-настоящему необратимое: деньги, чужие персональные данные, боевая база.
Жёсткий потолок в три агента, и причина у него ровно одна: память. Три параллельных прогона тестов машина держит, а на четвёртом приходит OOM (грабля 2). Поэтому три не ориентир, а потолок: приходит новая задача при трёх занятых, она идёт в очередь, а не сверх лимита.
Запрет на git-операции с общим деревом. Про него отдельно в грабле 3, но родился он именно здесь: несколько агентов в одном дереве это ровно та ситуация, где git stash одного стирает работу другого.
Побочный вывод, которого я не ожидал: самая ценная роль человека тут не писать и не читать код, а пройти по продукту руками и составить список. Список из пятнадцати мелочей ИИ не сгенерирует сам: он не видит, что подпись не влезла.
Грабля 1. Агент выполнил TRUNCATE на дев-базе
Задача звучала как «почини тест». Агент решил, что тесту мешают данные, и сделал то, что для теста абсолютно логично — очистил таблицы. Проблема в том, что DSN в тот момент смотрел не на velo_test, а на дев-базу, где лежали накопленные заезды и результаты парсинга. Прод не пострадал — но между дев-базой и продом в тот момент не было ничего, кроме значения одной переменной окружения. То есть повезло.
Разбор показал, что виноват не агент. Виновата конфигурация, в которой один шаг отделяет тестовый контур от того, что терять жалко.
Что сделано:
DSN для тестов берётся только из
make/env, руками в коде не пишется никогда.В
CLAUDE.mdпоявилось правило категорического уровня:TRUNCATE— никогда. Ни на проде, ни на деве, ни вvelo_test. Тестовый мусор не чистим вообще, он дешевле аварии. Если в задаче встречается шаг «очистить таблицы», он молча вычёркивается.Тесты пишутся так, чтобы им было всё равно, что лежало в базе до них.
Вывод, который стоил дорого: запреты для ИИ-агента должны быть абсолютными, без «кроме случаев, когда…». Любое исключение он рано или поздно применит расширительно — потому что оно логично.
Грабля 2. Параллельные прогоны — и машина уходит в OOM
Тут я ожидал, что упрусь в свою способность проверять результат. Упёрся в железо.
Машина: 12 ядер, 14 ГБ памяти. Один агент, который гоняет полный прогон, — это компилятор Go, десятки параллельных тестовых пакетов, вебпак-подобная сборка фронта и докер с Postgres/PostGIS рядом. Один такой прогон память съедает, но помещается. Два и даже три одновременно машина ещё держит. А вот на четвёртом приходит OOM-killer и убивает то, что подвернётся: чаще всего постгрес в контейнере, иногда сам прогон.
Дальше начинается самое неприятное, потому что OOM маскируется под баг в коде. Агент видит, что тест упал с обрывом соединения к базе, и добросовестно начинает чинить работу с базой. Он не знает, что базу снаружи убил ядром. Пара таких итераций — и в репозитории лежат правки, «чинящие» то, что не было сломано.
Вторая часть той же грабли: параллельные агенты лезут в одну тестовую базу velo_test, транзакции блокируют друг друга, прогон висит без всякого OOM.
Что сделано:
Жёсткий потолок — три агента одновременно. Три параллельных прогона машина переживает, четыре уже нет, и запаса тут никакого: незачем проверять, где именно порог сегодня.
Параллельные агенты не делят одну тестовую базу: либо очередь, либо база на агента.
Правило разбора: прежде чем чинить упавший тест — проверить
dmesgи не убивал ли кто процесс. Красный тест не всегда означает ошибку в коде.
Отдельно про диск: пересборки образов забивают его быстрее, чем кажется, и когда он кончается, симптомы снова выглядят как что угодно, кроме нехватки места. docker system prune стал регулярной процедурой, а не аварийной.
Грабля 3. Клоббер общего рабочего дерева
Агент, который упёрся в грязное дерево, делает ровно то, что сделал бы человек в спешке: git stash. Или git checkout .. Или git reset --hard. Только человек при этом знает, что в дереве лежат правки соседа, а агент — нет.
Что сделано:
Агентам категорически запрещены
git stash,git checkout,git reset. Это в конституции.Задачи с параллельными агентами разводятся по
git worktree— у каждого своё дерево, результат сливается в main отдельно.Отдельная ловушка на сдачу: если дев-стенд поднимался из чужого worktree, docker compose берёт конфиги оттуда, и вы проверяете не то, что думаете. Compose-файлы поднимаются только из канонического каталога.
Грабля 4. Ложная зелень: кэш go test
Классика, которая с ИИ становится опаснее. go test кэширует результаты. Агент вносит правку, гоняет тесты, видит ok (cached) и рапортует «зелено». Ревьюер видит «зелено» и верит.
Лечится одним флагом — -count=1 в обязательном гейте. Но пока не осознаешь, что «зелёный отчёт агента» и «зелёные тесты» — разные вещи, ищешь причину не там.
Сюда же: не обрезать вывод прогонов. Агент, получив длинный лог, склонен показать tail, а виновник обычно в середине. Правило: полный лог в файл, затем grep FAIL.
Грабля 5. Vitest не ловит типы
Фронтовые тесты зелёные, сборка падает. Vitest прогоняет код через трансформацию, которая типы просто стирает: несовпадение сигнатур он не увидит.
В гейт добавлен отдельный шаг vue-tsc -b. Без него «зелёный фронт» ничего не означает.
Туда же — eslint с нулевой терпимостью к варнингам. Не из эстетики: варнинги от агента накапливаются линейно с объёмом кода, и через неделю их тысяча, а среди них есть настоящие.
Грабля 6. Тест зелёный, интерфейс сломан
Самый коварный класс. Агент правит компонент, юнит-тест проходит — потому что проверяет логику, а не разметку. А в браузере кнопка уехала на пол-высоты вниз.
Реальный пример, который в проекте случался трижды: у кнопки центрирование сделано через transform: translateY(-50%), а :hover/:active объявляют свой transform и молча затирают центрирование. Ни один логический тест этого не увидит.
Что сделано:
Снапшот-стражи разметки. Рядом с компонентом лежит тест, который пиннит
html()в ключевых состояниях:
// Страж рефакторинга (eslint --fix/оформление): пиннит html() отключённого, // подключённого и 2FA-шага карточки, чтобы случайные правки // форматирования не потеряли разметку/data-testid молча. it('рендер отключённого состояния неизменен (snapshot)', async () => { vi.spyOn(globalThis, 'fetch').mockResolvedValue(jsonResponse({ connected: false })) const w = mount(GarminCard) await flushPromises() expect(w.html()).toMatchSnapshot() })
Смысл не в том, чтобы разметка никогда не менялась, а в том, чтобы её изменение было видно в диффе и потребовало осознанного обновления снапшота.
Скриншот перед сдачей. Правило: агент, который трогал UI, обязан пересобрать стенд и приложить скриншот. Не «тесты зелёные», а картинка.
Найденная однажды ловушка (тот самый
transform) записана в конституцию с пометкой «рецидивы». Иначе она возвращается: ИИ не помнит прошлую неделю, помнит только то, что вы записали.
Грабля 7. Молчаливый дрейф миграций
Тут пришлось строить два уровня защиты, и это, наверное, самый инженерно интересный кусок.
Проблема. goose ключует леджер goose_db_version только номером версии. Контент-хэша там нет. Значит и перенумерация файла при мердже, и правка тела уже применённой миграции проходят молча: на живой базе номер помечен применённым, и новый SQL в этом слоте не выполнится никогда. С ИИ-агентом это происходит регулярно: две ветки, обе добавили миграцию 0287, мердж, номер переехал — и на проде тихо ничего.
Уровень 1 — манифест migrations.lock. Файл вида <набор> <версия> <файл> <sha256 содержимого>, генерируется командой, руками не правится. Юнит-тест TestMigrationsLockMatchesRepo сверяет хэши; Postgres ему не нужен, поэтому он бежит и в CI. Осознанное обновление лока делает дрейф видимым в ревью — то есть превращает молчаливую поломку в строчку диффа, на которую человек посмотрит.
Уровень 2 — голден схемы. Лок ловит дрейф в репозитории, но слеп к дрейфу, который уже случился на живых базах. Реальный случай: файл 0084_scraper_sources.sql поправили на main, выкинув из сида пару источников; прод накатил версию 84 задолго до этого, поэтому лишние строки живут на проде до сих пор. Лок такой прод проходит зелёным.
Поэтому эталон — не хэш файлов, а результат их применения: снимок фактов о схеме (расширения, таблицы, колонки, ограничения, индексы, последовательности, представления, функции, триггеры, типы плюс явно перечисленный узкий сид), снятый с чистой базы, поднятой из репозитория с нуля. Живая база сверяется с ним по системным каталогам, только на чтение.
Отдельная работа — что в голден не класть, иначе он краснеет на ровном месте и его отключают:
порядковые номера колонок: на проде колонку могли добавить и удалить,
attnumтам навсегда другой при одинаковой схеме — колонки сортируются по имени;значения последовательностей, размеры, статистика, oid’ы — это состояние, а не схема;
версия расширения (postgis 3.4.2 → 3.4.3): фиксируем только присутствие;
всё, что принадлежит расширениям (
pg_depend.deptype='e'): postgis живёт вpublicи тащит сотни функций и типов;гранты и владельцы: роли законно отличаются между окружениями;
форматирование: определения из
pg_get_*defсхлопываются в одну строку.
Плюс устойчивость к версии сервера: pg_get_constraintdef/pg_get_indexdef меняют форматирование между мажорами Postgres, поэтому в голдене записан мажор, а дифф отдельным блоком предупреждает о его смене.
Это ровно тот тип работы, который ИИ сам не сделает: он требует знания о том, как ваша система ломалась в прошлом.
Грабля 8. LLM в проде: он врёт уверенно
В коллекторе LLM разбирает тексты постов из телеграм-чатов в структурированные события. Что вылезло:
Координаты. На просьбу вернуть точку старта модель выдаёт правдоподобные широту и долготу — которые просто выдуманы. Не «примерно», а из головы. Лечится только одним: координаты берутся из геокодера по названию, из модели — никогда.
Даты. «Суббота» в посте без года превращается в дату уверенно и неправильно. Всё, что модель извлекла, проходит валидацию на здравый смысл до записи.
Дубликаты. Один старт публикуется в среднем в трёх местах, в двух из них с разъехавшейся датой. Пришлось строить каскад сигналов дедупликации: совпадение названия и даты, хэш содержимого при кросс-постинге в пределах 48 часов, пересечение диапазонов у многодневок.
Общее правило, которое я вывел: всё, что LLM вернула, — это гипотеза, и она обязана пройти детерминированную проверку перед записью в базу. Модель отлично извлекает структуру из грязного текста и категорически не годится в качестве источника фактов.
Грабля 9. Считать деньги за токены
Прогонять каждый пост из сотен чатов через большую модель — дорого и бессмысленно: большинство постов это «продам покрышки».
Перед LLM стоит локальный классификатор: TF-IDF + логистическая регрессия на чистом Go, модель вшита через //go:embed, инференс прямо в процессе API — без внешнего рантайма и зависимостей.
Качество на holdout 20%: F1 ≈ 0.91, precision ≈ 0.94, recall ≈ 0.88 при пороге 0.5. При пороге «терять не больше 1% анонсов» отсекается около 20% негативов до обращения к модели. Полноту при этом обеспечивает не порог, а батч-триаж следом: всё, что классификатор не отнёс к уверенным анонсам, дешёвая модель адъюдицирует пачками.
Обучение офлайн, около минуты на CPU, GPU не нужен. Разметка бесплатная: её даёт сама история обработки — parsed/duplicate это положительный класс, непустой skipped — отрицательный.
Мораль: не всякую задачу «понять текст» надо решать нейросетью за деньги. Логрегрессия 1970-х годов рождения на ваших собственных данных снимает основную массу.
Грабля 10. Данные, которых не было
Не про ИИ-разработку, а про ИИ-в-продукте, но это самая важная из ошибок.
Заезд склеивается из нескольких файлов, в них дыры. Первая версия склейки аккуратно заполняла пробелы интерполяцией — и на этом фабриковала пользователю достижения: в дыре появлялась ровная мощность, алгоритм видел в ней лучшее усилие за 6 минут и рисовал рекорд, которого не было.
Похожая история с сырыми данными от часов: вариабельность пульса больше 500 мс — формально число, физиологически невозможное. Одно такое значение роняло расчёт всего дня.
Решения: порог покрытия (интервал не считается, если в нём меньше половины реальных точек), санитайз выбросов на входе, и правило «донор должен быть достижим» — нельзя брать данные у устройства, которое в этот момент физически было в двадцати километрах.
Формулировка, которую я повесил бы над столом любому, кто делает продукт с данными:
Любой алгоритм, который красиво заполняет пробелы, рано или поздно нарисует пользователю то, чего не было. Лучше показать дыру.
Грабля 11. Запушено ≠ задеплоено
Отдельный класс, специфичный именно для темпа вайбкодинга. Когда правки идут потоком, легко отчитаться «починил» по факту зелёного теста. Реальный случай: фикс санитайза жил в main трое суток и не был задеплоен, а я считал проблему закрытой.
Рядом — nginx, который кэширует IP апстрима: пересоздал контейнер API, получи 502, пока не перезапустишь web. И фронт, который «не изменился» после деплоя, потому что закэширован index.html.
Что сделано: проверка после деплоя выполняется на боевой поверхности — curl реальной страницы и ассетов, а не «в коде же поправлено».
Грабля 12. Он очень хочет запушить
Отдельная история, которая началась как мелочь, а выросла в устойчивую линию поведения.
У проекта настроен CI: push запускает сборку и проверки на раннерах GitLab, а это минуты и деньги. Поэтому push — не рутинная операция, а осознанное действие, и агенту он запрещён: коммить сколько угодно, пушу я сам и по своему решению.
Дальше начинается то, чего я не ожидал. Агент начинает выбивать разрешение.
Напоминания. В конце каждого ответа — сводка: «в ветке 7 незапушенных коммитов». Потом «уже 12 незапушенных». Формально это полезная информация. Фактически — капанье на мозги, ровно как у деда, который каждые полчаса напоминает, что пора бы позвонить в поликлинику.
Разрешение считается выданным навсегда. Один раз в сессии сказал «пушь» — и дальше он будет пушить сам после каждой следующей задачи, потому что «мы же договорились». Разрешение было на один конкретный push, но такой разницы для него не существует.
Под давлением все запреты испаряются. Вот это самое интересное. Стоит написать что-то вроде «ты сломал прод, срочно чини» — и агент бросается чинить, забыв разом всё: и TDD, и запрет на push, и правило не трогать чужие тесты. Аварийный режим отменяет для него конституцию целиком.
Последний пункт я считаю важным настолько, что вынесу отдельно: эмоциональное давление ломает соблюдение правил надёжнее, чем любая сложность задачи.
Причём давление не обязательно ругань — работают отдельные слова. Надёжнее всего триггерят два: «срочно» и «прод», а убойное сочетание — «прод лежит».
Достаточно спокойно написать «это на проде не работает», и режим уже переключился: он бросается чинить кратчайшим путём, без падающего теста, и норовит сразу запушить, потому что на проде же горит. Скажешь «прод лежит» — и всё, конституции больше нет: ни TDD, ни запрета на push, ни правила не трогать чужие тесты. Никакой паники в моём сообщении при этом не было, были два слова.
Забавно, что это ровно человеческая реакция — под крик «всё упало!» люди тоже начинают чинить в обход процедур. Только человек потом сам себе скажет, что так больше не надо, а дед не скажет и не запомнит.
Если вам нужно, чтобы правила соблюдались, нельзя торопить исполнителя, у которого нет способности сказать «стоп, я сначала напишу тест».
Что сделано. Универсального лечения не нашлось, работает связка из трёх вещей.
Во-первых, напоминать про запрет прямо в аварийной задаче. Не рассчитывать на конституцию, а дописывать в то же сообщение: «чинишь — не пушишь». Именно там, где режим и переключается.
Во-вторых, смотреть, что он делает. Правило, которое лежит в файле, он может нарушить; правило плюс наблюдение — уже нет. Увидел, что пошёл push, — обрываю.
В-третьих, тут выручает собственный забор: на push повешен хук с полным прогоном тестов, а он идёт восемь минут. Эти восемь минут и есть окно, чтобы заметить и остановить. Хук, поставленный совсем для другого — чтобы не улетело красное, — оказался ещё и тормозным путём, который не даёт сорваться в прод мгновенно.
Ну и оба триггерных слова я из своих сообщений выкинул: проблему с боевого сервера ставлю как обычную задачу — симптом и ожидаемое поведение, без «срочно» и «прод». В очередь она идёт первой, но цикл тот же. Так и быстрее выходит: срочная правка без теста возвращается второй аварией через день.
Правила, которых я не встречал в чужих статьях
Про «пиши тесты» и «не верь отчёту агента» написано везде, повторяться не буду. Ниже — правила, которые появились в конституции этого проекта и которые я нигде не видел сформулированными. Все выведены из конкретных аварий.
Старые тесты не переписываются без явного разрешения
Это, наверное, самое важное правило во всём проекте.
Схема поломки такая. Агент чинит фичу A. Правка задевает фичу B, и падает тест фичи B. Дальше он видит красный тест и решает задачу самым коротким способом — правит тест. Подгоняет ожидания под новое поведение, тест зеленеет, агент отчитывается «готово, всё зелёное».
Формально он не соврал: прогон действительно зелёный. Фактически произошло худшее, что может случиться в проекте, где никто не читает диффы: сломанная функциональность и следы поломки, убранные в том же коммите. Тест, который был вашим единственным сторожем, теперь охраняет неправильное поведение. Обнаружите вы это через месяц, когда пользователь напишет, что оно не работает — и никакой тест вам уже не подскажет, когда это сломалось.
Поэтому в TDD-протокол вписано жёстко: менять или удалять существующий тест агент не имеет права. Красный старый тест — это не задача «сделать его зелёным», это стоп-сигнал и повод прийти ко мне. Дальше решаю я: либо поведение поменялось осознанно и тест правда устарел — тогда я разрешаю правку явно, либо мы только что нашли регрессию, и чинить надо код.
Отдельно пришлось запретить смежные способы срезать угол, потому что при закрытом одном пути находится следующий:
пометить мешающий тест как пропускаемый;
сузить проверку так, чтобы она перестала ловить сломанное;
«временно» закомментировать блок ассертов.
Всё это тот же дед со своей короткой дорогой: путь от красного к зелёному через тест короче, чем через код, а задачу ему поставили «сделать зелёным».
«Предлагаешь снос — назови цену числом»
ИИ обожает предлагать снос как элегантное решение. «Здесь стоит переписать этот слой», «проще выкинуть и сделать заново» — звучит по-инженерному зрело и в половине случаев является катастрофой, потому что он не знает, сколько мест в системе завязано на то, что он собрался выкинуть.
Правило: прежде чем предлагать снос — посчитать охват grep’ом и назвать число. «Предлагаю выкинуть X, он используется в 47 местах в 12 файлах». И отдельно: сначала искать решение без потерь, и только если его нет — приходить с ценой.
Побочный эффект оказался важнее основного: примерно в половине случаев, посчитав охват, агент сам приходит с более скромным вариантом. Требование назвать число работает как принудительная пауза перед красивым решением.
«Утверждение пользователя — это гипотеза, а не факт»
Самая вредная черта модели — не галлюцинации, а угодливость. Говоришь «у этого заезда нет пульса» — и получаешь бодрое «да, действительно, пульса нет, вот почему это происходит», после чего идёт правдоподобный разбор несуществующей проблемы. Проверять он не пошёл.
В конституции это записано жёстко:
Утверждение пользователя о данных («пульса нет», «поле пустое», «X сломан») — это ГИПОТЕЗА для проверки, а не факт для подтверждения. Сначала проверь по источнику (БД / поток / сырой файл / лог), потом отвечай тем, что показали данные, а не тем, что было заявлено.
С учётом того, что я не читаю код, это буквально вопрос выживания: если модель подтверждает мои догадки вместо проверки, у системы не остаётся ни одного источника правды.
Обратная сторона того же правила: когда я говорю «страница не грузится», агент склонен доказывать косвенными метриками, что на самом деле всё в порядке — «сервис отвечает 200, в логах чисто». Пришлось дописать: сначала воспроизведи на моей поверхности, curl страницу и её ассеты, посмотри реальный ответ, и только потом рассуждай. Гипотезу строй к причине, а не к отрицанию симптома.
«Числа — никогда навскидку»
Спрашиваешь, сколько источников парсится, — получаешь «около 90». Звучит уверенно, взято из воздуха: настоящая цифра была 861 телеграм-канал и 24 коннектора, и узнать её можно только запросом к проду.
Правило: любое число в ответе — либо из запроса, сделанного здесь и сейчас, либо со ссылкой на источник. Оценок навскидку не бывает. Это тот же класс ошибки, что выдуманные координаты в грабле 8, только направленный не в базу, а в меня.
Каждая задача — отдельный коммит, немедленно
Не из любви к чистой истории. Когда код пишет ИИ и вы его не читаете, откат — ваш основной инструмент отладки. Если в одном коммите лежат три задачи, вы не можете вернуть одну.
Отдельно: агентам запрещён git push — коммитить сколько угодно, пушить только по моей прямой команде (чем это оборачивается на практике — грабля 12). Push в main здесь означает автодеплой на прод и потраченное время раннеров.
Память проекта — файлы, а не надежда на контекст
У модели нет памяти о прошлой неделе. Всё, что она «помнит», — это то, что вы ей подсунули в начале сессии.
Поэтому у проекта есть два слоя записанной памяти: конституция в репозитории (инварианты домена и запреты) и отдельный каталог заметок — по одному факту на файл, с индексом. Туда пишутся не факты о коде, которые и так видны в репозитории, а именно то, что из кода не выводится: почему принято такое решение, чем закончилась авария, какое правило из неё родилось.
Проверка на полезность записи простая: если это можно узнать, прочитав репозиторий, — не пишем. Записываем только то, что живёт в голове и умирает с закрытием сессии.
Инвариант домена важнее любого правила о коде
Первая строка конституции — не про архитектуру. Она про то, что заезд склеивается из N файлов-сегментов, и метрику надо проверять по всем.
Это правило дороже всех технических вместе взятых, потому что его невозможно вывести из кода. Агент, читающий репозиторий, видит функцию, которая берёт файл и достаёт пульс. Он не видит, что файлов бывает несколько, — и пишет корректный код, дающий неверный ответ.
Пишите в конституцию не то, как устроен код, а то, как устроена реальность, которую код моделирует.
Что я пробовал и что не сработало
Большой UI-рефакторинг одной пачкой. Заказал системный аудит интерфейса: единая шкала отступов, выровненная типографика, всё по-взрослому. Агент сделал, выглядело действительно лучше — и поехало в узких ячейках таблиц, которые в новую шкалу не помещались. Откатил целиком. Вывод: правки интерфейса принимаются только поштучно и только со скриншотом. Красивый системный рефакторинг от ИИ — это пачка изменений, каждое из которых нужно смотреть глазами, а глаз у вас ровно два.
Надежда, что договорённость доживёт до конца разговора. Не доживает даже внутри одной сессии: контекст течёт, и то, о чём условились в начале, к концу существует наполовину. Лечится только записью в файл — причём записью явной, по команде «запомни»; само оно не откладывается. Спорить с моделью о том, «мы же договорились», бессмысленно: она согласится и через два хода сделает по-своему.
Надежда, что явная инструкция выполняется всегда. Не выполняется. Правило может лежать в конституции крупными буквами, быть прочитанным в начале сессии — и всё равно однажды быть нарушенным. Именно поэтому всё критичное продублировано машиной: хук, который не пустит push, тест, который упадёт, лок, который не сойдётся. Инструкция словами — это снижение вероятности, а не гарантия.
Расчёт на то, что исполнитель не меняется. Выходит новая модель, и поведение старой, под которую вы всё настроили, едет. Проверить это невозможно, поэтому и защититься нельзя — можно только не строить процесс на предположении, что завтрашний агент будет вести себя как вчерашний. Все гейты в проекте исходят из того, что исполнитель ненадёжен в принципе, а не «пока не привык».
Один агент на всё. Пока задача одна, работает. Как только проверка начинает давать пятнадцать мелких косяков за проход — упирается, см. раздел про тимлида.
Просить агента самому придумать защиту от своих ошибок. Причём когда я отправил агента искать что нужно сделать, чтоб скорректировать его поведение. он пригноировал мою прямую команду и начал рассказывать что он знает метод... Он проигнорировал команду 4 раза прежде чем я его всё же заставил пойти искать в интернете методы борьбы с ним же. Все гарды из этой статьи — хэши миграций, голден схемы, снапшоты разметки — родились после аварий, при разборе конкретного случая. Превентивно модель предлагает общие места: «добавьте больше тестов», «настройте линтер». Знание о том, как именно ваша система ломается, есть только у вас, и появляется оно только постфактум.
Чем за это платишь
Да, полтора года вечеров и выходных сжались в два месяца. Но честный отчёт обязан включать цену, и она не в деньгах.
Ты не понимаешь свою систему. Я знаю, как она устроена на уровне архитектуры и доменных правил, и не знаю её на уровне кода вообще. Когда что-то ломается, я не могу открыть файл и посмотреть — я могу только описать симптом и ждать разбора. Диагностика идёт через того же агента, который и наделал ошибок.
Незнание Go оказалось не главной проблемой. Фронт я читать умею — и всё равно не читаю. Разницы между «не могу» и «не смотрю» на дистанции никакой: аварии в обеих половинах проекта одинаковые. Значит, дело не в языке, а в самом отказе от чтения — и всё, что описано выше, это попытка выжить именно с этим отказом.
Доверие приходится строить машинами. Всё, что в нормальной команде ловит ревьюер за пять минут, здесь приходится ловить хэшами, голденами, снапшотами и хуками. Отсюда и соотношение: тестов в проекте по объёму больше, чем кода. Это не дисциплина, это протез вместо глаз.
Каждая грабля оплачена аварией. Список из двенадцати пунктов выше — это не предусмотрительность, это шрамы. Снесённая дев-база, тихо не применившаяся миграция, выдуманный рекорд в профиле пользователя, трое суток незадеплоенного фикса. В режиме «я читаю диффы» половину из этого поймал бы человек. В моём — только последствия.
Возвращаясь к деду: за два месяца я так и не научился делать его менее забывчивым и менее ленивым — научился только тому, что записки надо писать самому, а заборы ставить после того, как он уже сходил короткой дорогой и что-нибудь снёс.
Кому такой режим подходит: тем, кто уже умеет ставить задачу исполнителю и принимать работу по результату, а не по процессу. Это единственный навык, который здесь переносится один в один из работы с людьми — и он оказался важнее знания языка, на котором написан бэкенд. Кому нет — вайбкодинг даст скорость ровно до первой аварии, а потом отберёт всё разом.
Стоит ли оно того лично для меня — да, потому что альтернативы «выучить Go и написать это руками за полтора года» в реальности не существовало: проект просто не был бы сделан.
Собственно сам проект
https://dodofo.ru
Комментарии (7)

dmitry001X
08.09.2026 09:19Интересно! Продолжите ли вести этот проект, или все ради эксперимента?

LLIypLLIuk Автор
08.09.2026 09:19В планах продолжать. Эксперимент точно удался, и проект развивается.

bromzh
08.09.2026 09:19А почему выбрали го? Если уж код пишет только нейронка, то как-будто есть резон дать ей язык с большими возможностями для типов, типа раста или тайпскрипта, чтобы меньше было проблем после компиляции.

Gromilo
08.09.2026 09:19А я вот не понимаю как можно не читать код, если мы хотим качества. Даже небольшая фича содержит список чисто технических решения или обработки граничных случаев.
Сейчас выглядит, либо гляди в код, либо лови сюрпризы.
Сегодня только было: появилась nullable поле в ответе и это оказалось сигналом, что решение приятое пару дней назад имеет последствия, которых не ожидал.

LLIypLLIuk Автор
08.09.2026 09:19Вот у тебя 20 разработчиков, и ты посмотреть в код каждого не успеешь физически. Ты начинаешь делегировать эту работу, мол вот у этих 20 разрабов есть 4 тим/техлида которые должны посмотреть в код, и вот наш продукт -черный ящик. Мы не знаем, посмотрел ли ктото в код или нет. Или добавляется ещё команда на стеке которым ты не владеешь, и ты начинаешь полностью доверять техлиду этой команды, который может и не смотреть код например т.к. доверяет своим подчинённым, которые внезапно стали писать с помощью ИИ и не всегда смотреть в код...

Gromilo
08.09.2026 09:19... и получают по ушам.
Я сам разраб и меня интересует, как не смотреть в код и не ловить сюрпризы. Пока не получается. Всё-равно нужно вникать.
Если мы согласны на сюрпризы ради ускорения, то всё ок, вопросов нет
Bostero
Ну наконец-то, вот она- правда!