ВЫБЕРИ, КАК ЧИТАТЬ
Выпуск идёт в двух версиях. Общая часть — для всех, кто строит продукт: разбор шести ролей в команде и как у них рассыпается работа, если за неё взялся только ИИ-агент. Инженерная часть — детальный технический разбор 56 инцидентов из моих проектов, для тех, кто чинит инфраструктуру руками.
РОЛИ · СЕТЬ · СУБД · БРАУЗЕР · API · СЕРВЕР · ПРОМПТЫ АУДИТА
006ИЛЛЮЗИЯ БЫЧЬЕГО РЫНКА НА ИИ-РАЗРАБОТКЕ
«У меня в руках ИИ, сейчас соберу свой SAP или CRM на тысячи пользователей.» Шесть ролей в продуктовой команде, которые ИИ-агент не закрывает, и 56 инфраструктурных инцидентов из моих проектов — с разметкой, какие из них валят боевой сервис в один момент.
БЛОК 1. Откуда взялась иллюзия бычьего рынка ИИ
На бирже «бычьим рынком» называют время, когда всё безостановочно растёт. Любой портфель в плюсе, любой инвестор чувствует себя гением. Но когда тренд разворачивается, внезапно выясняется, что половина проектов вообще ничего не зарабатывала — их просто тащил вверх общий хайп.
В ИИ-разработке сейчас происходит ровно то же самое. Любой менеджер покупает подписку на Cursor или Claude Code, накидывает промпты и за выходные собирает рабочий прототип. Через неделю он уже убеждает инвестора: «Я сам сделаю свою CRM, зачем мне разработчики, нейросеть пишет код со скоростью пулемёта».
И в этом кроется главная иллюзия. Нам кажется, что если код пишется быстро, то продукт уже готов к запуску. И действительно — на личном ноутбуке всё летает. Первые пару месяцев все счастливы. А потом случается первый сбой в боевой среде (на проде), и выясняется страшное: в команде нет ни одного человека, который понимает, как эта штука вообще работает под капотом.
За последний год я собрал коллекцию из 56 таких инцидентов на собственных проектах. Каждый стоил мне дня-двух жесткой отладки. Ни один из этих багов не появился бы, если бы я писал код руками. Но я доверил это ИИ-агенту. Полный список с разбором лежит в блоке 4.
Реальность такова: в полноценной продуктовой команде шесть ролей. ИИ-агент способен частично закрыть только одну из них — написание кода. Менеджер с подпиской искренне верит, что заменил весь отдел, но по факту он просто нанял джуниор-разработчика, который очень быстро печатает. Пять остальных ролей провисают, продукт стагнирует, и осознание этого приходит слишком поздно — обычно через полгода-год.
Инциденты, которые валят сервис в один момент
Из 56 кейсов в блоке 4 эти семь — про мгновенное падение. Если знаете о них заранее, добавляете строку в чеклист перед каждым релизом и обходите. Если не знаете — узнаете в первую субботу пиковой нагрузки.
- Инцидент №4. Забытый PROXY-protocol в Nginx — антифрод видит у всех пользователей один и тот же IP балансировщика, считает это ботнетом и банит подсеть провайдера. Симптом: массовый ложный бан клиентов из России.
- Инцидент №15. Долгий внешний вызов внутри транзакции — соединения с базой удерживаются по 45 секунд каждое, пул заканчивается на пятом параллельном пользователе. Симптом: сервер перестаёт отвечать, ошибка
QueuePool overflow. - Инцидент №19. Конкурентное списание балансов без блокировки строки — два процесса считывают одну сумму, оба обновляют, одно списание затирает другое. Симптом: баланс уходит в минус, СУБД сыпет deadlock-ами.
- Инцидент №25. Hydration mismatch на динамическом времени — на серверной и клиентской версии страницы расходится «1 минуту назад» и «2 минуты назад», React тихо отключает обработчики всех кликов. Симптом: страница выглядит нормально, ни одна кнопка не работает на iPhone.
- Инцидент №50. OOM-watchdog убивает здоровый сервис — скрипт мониторинга читает глобальный лог ядра, видит OOM от соседнего контейнера и перезапускает чужой сервис. Симптом: чат обрывает стриминг каждые пять минут без причины в собственных логах.
- Инцидент №54. Деплой в момент длинной задачи — CI убивает контейнер без graceful shutdown, активные генерации обрываются на 90%. Симптом: долгие задачи у пользователей не доходят до результата при каждом релизе фронтенда.
- Инцидент №56. Логи без ротации — приложение пишет CORS-ошибки в файл годами, файл забивает диск, на полном диске падает PostgreSQL. Симптом: сервис вдруг ложится, и причина не в коде, а в свободном месте.
Эти семь инцидентов — нижняя планка инженерной грамотности. Если ИИ-агент пишет код на проде, и вы не проверяете его автоматически перед каждым сохранением, то падение продукта — это вопрос времени. В блоке 5 лежат четыре промпта, которые ловят 80% таких ловушек ещё до того, как они попадут в боевой код.
Все тексты ниже анонимизированы: имена сервисов, IP-адреса, ключи и домены заменены. Но сами инциденты — настоящие.
БЛОК 2. Шесть ролей в продуктовой команде
В любой продуктовой команде есть шесть ключевых ролей. ИИ-агент может взять на себя лишь одну из них (и то не до конца). Остальные пять он немного ускоряет, но точно не заменяет. Давайте разберём, кто за что отвечает и где именно нейросеть пасует.
Продакт-менеджер: находит реальную боль клиента
Продакт — это тот, кто слушает людей. Он сидит на кастдевах, разгребает тикеты в саппорте и пытается вытащить из этого хаоса одну настоящую боль клиента. И формулирует её не сухим языком из учебника, а живыми словами, которыми ругается пользователь. Именно продакт решает, какую фичу мы пилим сегодня, а какую выбрасываем в корзину.
Что делает ИИ-агент? Он уверенно выплёвывает красивые списки фич и абстрактные формулировки ценности. Проблема в том, что эти формулировки — средняя температура по больнице. Они одинаковые у всех, кто вбил такой же промпт. Нейросеть не сидела на часовом созвоне с клиентом и не слышала его интонацию. Выбирать между двумя хорошими идеями на основе интуиции и эмпатии ИИ пока не умеет.
Если продакт ошибается в самом начале, дальше сыпется всё: аналитик меряет не то, дизайнер рисует не то, маркетолог приводит не тех. Каждый делает свою работу идеально, но продукт всё равно никому не нужен.
Аналитик: выбирает метрики и считает деньги
Аналитик следит за тем, чтобы команда не обманывала сама себя. Сколько новых пользователей возвращается через неделю (удержание)? Сколько мы тратим на привлечение одного клиента, и сколько он нам приносит (юнит-экономика)? На каком шаге люди массово отваливаются (воронка)? Аналитик решает, на какие цифры нужно смотреть каждый день, а какие можно игнорировать.
ИИ-агент шикарно пишет SQL-запросы. То, на что раньше уходил час, теперь делается за минуту. Но вот решить, под каким углом смотреть на данные, ИИ не может. Красивый зелёный график в среднем может скрывать катастрофический отток по одному конкретному каналу рекламы. Нейросеть не догадается копнуть именно туда без вашей подсказки.
Если в команде нет аналитика, дашборды могут годами гореть зелёным, а потом внезапно выясняется, что деньги кончились. Цифры считались правильно, просто вы смотрели не на те цифры.
UX/UI-дизайнер: делает так, чтобы было удобно конкретно вашим людям
Давайте сразу разделим: UI — это про красоту (цвета, шрифты, отступы). UX — про логику и удобство (как человек переходит с экрана на экран). ИИ отлично справляется с UI: он выдаст вам современный, красивый экран. Но с UX он проваливается с треском.
Живой дизайнер проектирует интерфейс под конкретную аудиторию. Он знает, что 80% ваших людей заходят с телефона, поэтому делает кнопки крупнее. Нейросеть же всегда рисует идеальный "золотой путь" — когда всё хорошо. Но в реальности у пользователя отваливается интернет в середине оплаты, он забывает пароль или у него слишком длинное имя, которое ломает вёрстку. ИИ-агент эти краевые сценарии (edge cases) просто игнорирует, а ведь именно на них теряются клиенты.
Если продукт собирает ИИ без дизайнера, он выглядит как конфетка, всё кликается. Но люди почему-то не возвращаются. Внешне придраться не к чему, но пара неудачных решений в логике убивают весь опыт использования.
Инженер: делает так, чтобы всё это не рухнуло под нагрузкой
Хороший инженер держит в голове всю архитектуру продукта: как данные текут от базы к пользователю, где узкие места, что отвалится, если на сайт разом зайдут десять тысяч человек. Это глубокое, контекстное знание конкретно вашей системы.
ИИ-агент пишет код быстро и почти без ошибок. Но только локально, в идеальном вакууме. На проде выясняется, что нейросеть понятия не имела о том, что у вас Nginx стоит за облачным прокси (сюрприз!), или что Safari молча обрывает WebSocket-соединение при неактивности. Те самые 56 инцидентов из блока 4 — это хроника того, чего ИИ не знал о реальном мире.
Инженерные провалы всегда очень громкие: ложится сервер, не проходят платежи, разрывается саппорт. Поэтому их чинят первыми. Но эта "громкость" отвлекает от того, что три предыдущие роли (продакт, аналитик, дизайнер) проседают тихо и незаметно.
DevOps (специалист по инфраструктуре): следит за серверами
Инженер думает о коде, DevOps — о сервере, на котором этот код крутится. Базы данных, балансировщики, кэш, мониторинг — это его вотчина.
ИИ-агент отлично генерирует стандартные конфиги. Попросишь настроить Nginx — выдаст рабочий вариант. Чего он не умеет, так это планировать ресурсы. Он не знает, что в пятницу вечером у вас пиковая нагрузка, и сервер нужно масштабировать. А ещё ИИ не проснётся в три часа ночи от алерта, чтобы зайти в консоль, убить зависшие процессы и поднять упавшую базу.
Когда падает серверная часть, парализуется вся работа. Реклама крутится, люди приходят, видят ошибку 502 и уходят навсегда. Команда в панике ищет баги в коде, а проблема — в закончившемся месте на диске.
Маркетинг и дистрибуция: приводит тех, кому продукт реально нужен
Самая недооценённая разработчиками роль. Идеальный код бесполезен, если им никто не пользуется. Маркетинг отвечает за то, чтобы найти вашу аудиторию, зацепить её правильным посылом и превратить в лояльных фанатов.
ИИ-агент легко напишет рекламный пост или текст для лендинга. Но он не скажет вам, где именно обитает ваша аудитория. Нейросеть выдаст усреднённые советы по индустрии — идите в соцсети, запускайте контекст. Но там уже все ваши конкуренты, и это дорого. Найти неочевидный канал привлечения ИИ не способен.
Если маркетинг проваливается, к вам приходят случайные люди. Они странно пользуются продуктом, ничего не покупают и пишут плохие отзывы. Аналитик бьёт тревогу («удержание падает!»), и вы начинаете судорожно переделывать продукт. Хотя с продуктом всё нормально — просто вы привели не тех людей.
Резюмируем
Иллюзия ИИ-пузыря — это наивная вера в то, что нейросеть заменяет всех. Да, ИИ круто ускоряет инженера. Если у вас уже есть слаженная команда из шести человек, с ИИ вы будете двигаться в два раза быстрее. Но если команды нет, вы просто быстро напишете красивый продукт, который развалится при первой же реальной нагрузке. Поймёте вы это не сразу, а где-то через полгода, когда закончатся деньги.
А теперь давайте зароемся в ту самую одну роль, которую ИИ вроде как заменяет — в инженерную. В блоке 3 расскажу, как настроить автоматический аудит ИИ-кода. А в блоке 4 — тот самый атлас 56 инфраструктурных провалов из первых рук. В блоке 5 — четыре промпта, которые спасут вас от большинства этих проблем.
БЛОК 3. Как поставить инженерный аудит у себя
Этот блок — про шестую роль из шести. Если в вашей команде есть инженер, который понимает, как код работает в боевой среде, дальше — для него. Если такого человека нет, ниже — карта, по которой можно поставить хотя бы автоматическую проверку ИИ-агента самостоятельно. Это не заменит инженера, но снимет 70-80% самых громких провалов из 56 в блоке 4.
Сразу расшифрую несколько слов, которые встретятся в шагах. Pre-commit hook — это автоматическая проверка, которая запускается на компьютере каждый раз, когда программист сохраняет код в общий репозиторий. До разговора с этой проверкой код в общий код не попадает. Через эту дверь можно пропустить ИИ-агента вашими собственными правилами — «не сохранять, если код выглядит подозрительно вот в этом конкретном смысле». KNOWN_ISSUES.md — это просто файл с известными проблемами. Технический дневник проекта. У меня в каждом репозитории такой лежит, ИИ-агент читает его при старте сессии и не наступает на старые грабли.
Шесть шагов
- Соберите свой список известных проблем. Пройдите по последним 50-100 изменениям в коде проекта, выпишите все случаи, когда после разворачивания в боевую работу что-то пошло не так. Симптом — причина — решение. Если такого файла ещё нет, это первый артефакт у вас на руках. У меня он растёт второй год, и каждая новая запись стоила одного-двух дней разбирательств в прошлом.
- Разбейте их на пять технических слоёв. Путь от пользователя до приложения через прокси-серверы (сеть), хранение и изменение данных (база данных), поведение страницы в браузере у клиента (фронтенд и гидратация), вызовы внешних сервисов (API оплаты, генерации, рассылки), обслуживание сервера и реакция на инциденты (серверная часть). Эта раскладка — не теоретическая, она вышла из моих 56 инцидентов: всё кладётся в один из пяти слоёв без остатка.
- Напишите по одному промпту на каждый слой. Один промпт берёт изменения в коде (это называется diff — разница между старой и новой версией), читает один-два файла за раз, ищет паттерны конкретного слоя, выдаёт отчёт. Готовые шаблоны промптов P01-P04 — в блоке 5 этого выпуска. Их можно копировать и адаптировать под себя.
- Поставьте эти промпты в автоматическую проверку перед сохранением кода. Дешёвая модель (Anthropic Haiku, Google Gemini Flash, китайский Kimi K2.6 — это разные нейросети, которые умеют выполнять промпты быстро и дёшево) проходит по изменениям за секунды и копейки. При обнаружении подозрительного паттерна — блокирует сохранение и говорит, что именно ей не нравится. Программист (или ИИ-агент в кресле программиста) либо исправляет, либо обосновывает.
- Раз в неделю — полный аудит всего кода. Тот же набор промптов, но не на изменениях, а на всём коде проекта. Через GLM, Gemini или Kimi — там, где у вас лучше цена за объём. На выходе получаете список «потенциальных мест, на которые стоит посмотреть», который инженер-человек просматривает за час. Это вторая линия защиты — она ловит то, что протиснулось через первую.
- Обновляйте файл известных проблем после каждого нового инцидента. Это и есть память команды. ИИ-агент, который читает этот файл в начале сессии, перестаёт совершать те же ошибки. В моих проектах он лежит как раздел «Known Issues» внутри файла CLAUDE.md (это специальный файл, который ИИ-агент Claude Code читает автоматически перед началом работы). У других ИИ-агентов аналогичные механизмы есть тоже — почитайте документацию вашего.
Что НЕ работает
Не работает «один универсальный промпт, который ловит всё». Это самая частая ошибка тех, кто только начинает. ИИ-агент сильно слабеет, когда задача становится размытой. В пятислойном промпте он сконцентрируется на первом слое и пропустит остальные четыре. По одному промпту на слой — это не бюрократия, это техническая необходимость. Так у каждого промпта есть чёткий фокус.
Не работает «прогон промпта вручную раз в месяц». Через месяц у вас уже 30 изменений в коде, и найденный паттерн придётся искать в истории. К тому моменту он уже месяц жил в боевой работе и, возможно, успел стоить вам пользователей. В автоматической проверке перед сохранением паттерн ловится в моменте, до того, как код попадёт куда-либо.
Не работает «один сеньор пишет промпты для всех». Промпт под чужой проект плохо ловит, потому что промпт должен знать конкретные пути в вашем коде, конкретные внешние сервисы, которые вы используете, конкретные паттерны вашего проекта. Передавать другой команде нужно методологию — пять слоёв — и шаблон, а не готовый промпт. Промпт каждая команда докручивает под себя за день работы.
БЛОК 4. Атлас 56 инцидентов
Пять глав по техническим слоям. Каждая глава — схема + 12 (или 8 в последней главе) инцидентов с симптомом, корнем и решением. После всех глав — четыре упражнения на разбор кода, по одному на слой.
Главы написаны в техническом тоне для инженерной аудитории. После каждой главы — короткое резюме «что это значит человеческим языком» для тех, кто читает этот выпуск в режиме «продукт и команда».
Глава 1. Сетевой хаос, Nginx и web-топология (кейсы 1-12)
Инцидент 1. ActionExecutor застрял на старом коде воркера в RAM
- Проект: Backend ассистента.
- Симптом: Повторяющиеся падения интеграции с биллинговым партнёрским API с ошибками
counter_id undefined. Деплои проходили зелёными, но рантайм-логи выдавали старые трассировки. - Корень: Воркер запускался как systemd-юнит (
app-ads-worker.service). Скрипт деплоя выполнял blue/green-переключение фронтенда, но не перезапускал фоновые службы. ИИ-агент не подозревал о существовании долгоживущих системных процессов и бесконечно патчил код на диске, пока процесс в RAM исполнял скомпилированный код трёхдневной давности. - Решение: Добавить
systemctl restart app-ads-workerв хук успешного завершения деплоя.
Инцидент 2. Бесконечные SSO-петли редиректов при basePath-изоляции
- Проект: Self-hosted чат-фронтенд.
- Симптом: Белый экран при попытке входа на мобильных устройствах, бесконечная перезагрузка по адресу
/chat/api/bridge?return=.... - Корень: SPA-ассеты при сборке использовали конструктор
new URL('/api/bridge', appEnv.APP_URL). ПриAPP_URL=https://example.com/chatконструктор отсекал subpath/chatи генерировал путь, который вёл в корень основного домена и возвращал 404. Middleware ловило 404, считало сессию сбойной и инициировало повторный редирект — петля. - Решение: Глобальный рефакторинг сборщика URL с принудительным инжектированием
basePathво все клиентские API-вызовы.
Инцидент 3. CDN апстрим-фреймворка жёстко захардкожен в SPA
- Проект: Сервис медиа-генерации.
- Симптом: При открытии чата с мобильного Safari выдавал 404 на шрифты и иконки.
- Корень: При сборке форка SPA-фреймворка агент скопировал дефолтный
next.config.js, гдеassetPrefixуказывал на официальный CDN вендора. Все ассеты нашего кастомного билда уходили на сторонний CDN, который их не знал. - Решение: Удалить
assetPrefix, перенести статику в локальную директорию/_spa/.
Инцидент 4. Забытый PROXY-protocol снимет real-IP и ломает антифрод
- Проект: Сервис медиа-генерации.
- Симптом: Массовый ложный бан российских пользователей с ошибкой 403 от антифрода.
- Корень: Домен работал через облачный reverse proxy (
192.0.2.1), передающий реальный IP клиента через TCP-уровневый PROXY protocol. Nginx поддерживал параметрproxy_protocolна портах прослушивания, но агент забыл подключить файл разбора заголовка (snippets/proxy-protocol.confс директивойreal_ip_header proxy_protocol). Для бэкенда IP-адресом всех пользователей стал IP балансировщика. Антифрод-сервис партнёра зафиксировал 10 регистраций с одного IP за две минуты, счёл это ботнет-атакой и забанил весь прокси-пул провайдера. - Решение:
include /etc/nginx/snippets/proxy-protocol.conf;во все блокиserver.
Инцидент 5. Таймауты Telegram-webhook'ов из-за сетевых фильтров
- Проект: Self-hosted чат-фронтенд.
- Симптом: Бот в Telegram перестал отвечать на сообщения, исходящие запросы в логах проходили без ошибок.
- Корень: Серверы Telegram отправляли POST-запросы на основной домен. Маршрутизация через облачный балансировщик блокировала входящие соединения от Telegram на сетевом уровне, приводя к таймаутам.
- Решение: Выделить технический субдомен без внешнего проксирования, резолвящийся напрямую на IP сервера, и пробрасывать webhook через локальный Nginx.
Инцидент 6. Пустые 403 от S3-хранилища на presigned URL
- Проект: Сервис медиа-генерации.
- Симптом: Сгенерированные медиафайлы спорадически не прогружались. Sentry фиксировал
TypeError: Failed to fetch. - Корень: WAF облачного S3 ложно детектировал частые presigned-запросы как атаку и молча сбрасывал соединение, возвращая HTTP 403 с нулевым телом, без XML-ошибки AccessDenied. Агент не анализировал сырые HTTP-заголовки и впадал в бесконечный цикл ретраев.
- Решение: Fallback-маршрут проксирования скачиваний через свой бэкенд при падении прямого S3-запроса.
Инцидент 7. DNS-перегрузка Undici в Node.js на пике
- Проект: Self-hosted чат-фронтенд.
- Симптом: При пиковых нагрузках генерации прерывались с
fetch failed: Connect Timeout. - Корень: Стандартный HTTP-клиент Node.js при частых асинхронных вызовах перегружал DNS-резолвер сервера, вызывая кратковременные сбои разрешения доменных имён LLM-провайдеров. Агент классифицировал это как фатальную ошибку авторизации и сбрасывал сессии.
- Решение: Локальное DNS-кэширование (адаптер на
dns.lookup) и принудительный keep-alive пул.
Инцидент 8. Тихий 403 в API внешней платформы из-за неполного OAuth scope
- Проект: HR-парсер.
- Симптом: Скрипт успешно авторизовался по OAuth, но при отправке сопроводительных писем падал с 403.
- Корень: Агент запросил базовый scope чтения. По недокументированному правилу платформы, для исходящих действий требуется расширенный scope записи. Модель не читала тело ошибки и просто пересоздавала базовый токен в цикле.
- Решение: Явно указать расширенный scope в URL авторизационного запроса. Залогировать тело ответа 403, прежде чем переавторизоваться.
Инцидент 9. iframe Clickjacking блокировки на партнёрских платформах
- Проект: Образовательная платформа.
- Симптом: Интерактивные виджеты не загружались внутри iframe-фреймов обучающих платформ партнёров.
- Корень: Агент применил жёсткую политику
X-Frame-Options: DENYво всех заголовках Nginx. Это заблокировало легитимное встраивание песочниц в iframe. - Решение: Заменить заголовок на гибкий
Content-Security-Policy: frame-ancestors 'self' https://*.example.com;.
Инцидент 10. Превышение лимита Nginx client_max_body_size на голосовых
- Проект: Аналитический бот.
- Симптом: Голосовые сообщения длительностью более трёх минут не транскрибировались, бот возвращал пустой ответ без ошибок в логах бэкенда.
- Корень: Nginx блокировал загрузку аудиофайлов размером более 2 MB на уровне прокси, возвращая HTTP 413. До бэкенда запрос не долетал, поэтому в логах Python-приложения была тишина.
- Решение:
client_max_body_size 50M;в конфигурации Nginx.
Инцидент 11. Недокументированный 429 от внешнего LLM-роутера при конкурентных запросах
- Проект: Сервис медиа-генерации.
- Симптом: При одновременной генерации промптов для 5 пользователей бэкенд падал с 429.
- Корень: Провайдер заявляет лимит в 20 запросов в секунду, но его WAF выдаёт 429 уже на 4-м конкурентном запросе с одного IP. Агент не использовал очереди, надеясь на заявленные лимиты.
- Решение: Локальный семафор в asyncio, ограничивающий конкурентность тремя запросами в секунду.
Инцидент 12. CORS preflight на Safari при запросах к S3
- Проект: Сервис медиа-генерации.
- Симптом: В Safari на iOS генерации падали на шаге сохранения холста.
- Корень: Safari требует точного совпадения
Access-Control-Allow-Originи блокирует wildcard*при запросах с включёнными credentials. Агент прописал дефолтныйCORS *в настройках бакета. - Решение: Явное перечисление разрешённых доменов в CORS-конфигурации S3-бакета.
Что это значит человеческим языком. Между клиентом и вашим приложением живут не один сервер, а четыре независимых слоя. Сначала запрос проходит через CDN — это сеть серверов вашего облачного провайдера по всему миру, она ускоряет загрузку и защищает от атак. Потом — через Nginx, программу, которая принимает запрос на ваш сервер и распределяет по приложениям. Потом — через фреймворк (это библиотека для построения веб-приложения, например Next.js или Astro), который собирает страницу. И только потом — до настоящего API, где работает ваша логика.
На каждом из этих четырёх стыков ИИ-агент допускает типовые ошибки: неправильно настраивает передачу настоящего IP-адреса клиента (и антифрод банит за то, что у всех ваших пользователей один и тот же IP), забывает увеличить лимит на размер аудиофайлов (и голосовые длиннее трёх минут просто исчезают, без следа), оставляет дефолтный CORS-параметр * (и Safari блокирует запросы из-за этого). 12 кейсов в этой главе — ровно про эти четыре слоя по три инцидента на каждом.
Глава 2. СУБД, транзакции и конкурентность (кейсы 13-24)
Инцидент 13. Затирание данных при PATCH-запросах через схему с .default
- Проект: Backend ассистента.
- Симптом: При обновлении заголовка занятия (PATCH) формат встречи самовольно переключался с «онлайн» на «офлайн», стирая адрес.
- Корень: Агент применил метод
.default('offline')к опциональному полю в общей схеме валидации. Клиент при PATCH передавал только изменённое поле; валидатор видел отсутствиеeventAttendanceModeв payload, автоматически подставлял дефолт и отправлял на перезапись. - Решение: Запрет дефолтных значений во всех схемах валидации PATCH-запросов.
Инцидент 14. FastAPI get_db() rollback стирал аудит-логи фрода
- Проект: Сервис медиа-генерации.
- Симптом: Система безопасности фиксировала блокировки спамеров, но в БД в таблице
fraud_logsотсутствовали записи о банах. - Корень: Вся бизнес-логика использовала единую сессию из
get_db()через FastAPI dependency injection. При обнаружении фрода бэкенд записывал лог в сессию и выбрасывалHTTPException(403). FastAPI перехватывал исключение и автоматически вызывалdb.rollback(), который стирал только что записанные строки. - Решение: Перевод функций аудита фрода на выделенный пул соединений с независимой сессией через
AsyncSessionLocal()и принудительныйcommit()до выброса исключения.
Инцидент 15. Исчерпание SQLAlchemy QueuePool при долгих внешних вызовах
- Проект: Сервис медиа-генерации.
- Симптом: Бэкенд внезапно переставал отвечать, выдавая
QueuePool limit of size 5 overflow. - Корень: Внутри HTTP-обработчика генерации агент открывал транзакцию (
async with db.begin():) и внутри неё делал долгий асинхронный запрос к внешнему API генератора видео, который занимал до 45 секунд. Всё это время коннект к СУБД удерживался открытым, мгновенно исчерпывая пул при нескольких параллельных запросах. - Решение: Жёсткое правило — внешние сетевые вызовы выполняются вне транзакционных контекстов.
Инцидент 16. Postgres TEXT vs DATE — неявное сравнение ломает запросы
- Проект: Backend ассистента.
- Симптом: При фильтрации расписания по дате база выбрасывала
operator does not exist: text = date. - Корень: Агент сохранял даты как строки (
VARCHAR), а при фильтрации пытался сравнивать их напрямую с типомDateиз Python. PostgreSQL не делает неявный кастинг. - Решение: Миграция колонок на
DATEи строгая валидация типов на уровне ORM.
Инцидент 17. Race condition в симметричных связях таблиц
- Проект: Backend ассистента.
- Симптом: При одновременном взаимном подписке двух преподавателей друг на друга СУБД уходила в deadlock.
- Корень: При параллельных вставках
a -> bиb -> aСУБД блокировала строки в разном порядке, вызывая перекрёстный deadlock. Агент не использовал упорядочивание ID при создании связей. - Решение: Принудительное сортировка ID до блокировки:
min_id, max_id = sorted([a, b]).
Инцидент 18. Нарушение FK-ограничений из-за пустых строк вместо NULL
- Проект: Backend ассистента.
- Симптом: База отклоняла миграции legacy-колонок с
ForeignKeyViolationError. - Корень: При очистке связей агент записывал в поле FK пустую строку
""вместоNULL. Postgres расценивал пустую строку как попытку сослаться на запись с ID"", которой не было в родительской таблице. - Решение: Middleware-преобразователь пустых строк в
Noneна этапе валидации входящих схем.
Инцидент 19. Deadlock при конкурентном списании балансов
- Проект: Сервис медиа-генерации.
- Симптом: При частых асинхронных генерациях баланс пользователя уходил в минус, СУБД сыпала ошибками блокировки транзакций.
- Корень: Агент обновлял баланс
SELECTс последующимUPDATE. При конкурентных запросах два процесса считывали один и тот же баланс одновременно, после чего оба выполняли обновление, затирая списания друг друга. - Решение: Атомарные апдейты с блокировкой строки:
UPDATE users SET balance = balance - :amount WHERE id = :id AND balance >= :amount RETURNING balance.
Инцидент 20. INTEGER out of range на ID мессенджера
- Проект: Аналитический бот.
- Симптом: Бот упал и перестал записывать историю сообщений с
integer out of range. - Корень: Для ID сообщений из мессенджера агент использовал
INTEGERв PostgreSQL (лимит ~2.1 млрд). Мессенджер выдаёт ID выходящие за лимит. - Решение: Изменение типа колонки на
BIGINTчерез миграцию.
Инцидент 21. Утечка файловых дескрипторов на незакрытых курсорах
- Проект: HR-парсер.
- Симптом: Через четыре часа непрерывной работы парсер падал с
OSError: [Errno 24] Too many open files. - Корень: Агент выполнял сырые SQL-запросы через
await db.execute()без закрытия результирующего курсора. Каждый запрос удерживал открытый дескриптор соединения. - Решение: Обязательное использование контекстных менеджеров
async with db.execute(...) as result:.
Инцидент 22. Lost update в Redis из-за отсутствия блокировок
- Проект: Сервис медиа-генерации.
- Симптом: Пользователи видели неактуальный статус генерации видео, хотя бэкенд уже завершил обработку.
- Корень: Агент считывал статус из Redis, изменял локально в Python и записывал обратно. При конкурентных запросах происходила потеря обновлений.
- Решение: Атомарные операции Redis (
HSET,SETNX) или Redlock.
Инцидент 23. Ошибка кодировки соединения PostgreSQL на эмодзи
- Проект: Аналитический бот.
- Симптом: Сообщения с эмодзи вызывали
OperationalError: Incorrect string valueи падение записи. - Корень: Агент инициализировал базу с локалью по умолчанию (
SQL_ASCII/LATIN1) вместоUTF8, либо строка подключения SQLAlchemy не форсировалаclient_encoding. - Решение: Миграция базы в
UTF8с правильным collation и форсированиеclient_encoding='utf8'в строке подключения.
Инцидент 24. Slow queries из-за отсутствия составных индексов
- Проект: Backend ассистента.
- Симптом: Страница поиска преподавателей грузилась более шести секунд при пиковом трафике.
- Корень: Агент сгенерировал SQL с фильтрацией по трём полям одновременно (
track_id,is_active,rating), но не создал составной индекс. База выполняла полное сканирование. - Решение:
CREATE INDEX idx_teachers_filter ON teachers(track_id, is_active, rating DESC);
Что это значит человеческим языком. База данных — это место, где живут все факты о вашем продукте: кто пользователь, что он купил, сколько у него денег на счету, на какое занятие записан. Каждое действие пользователя — это разговор приложения с базой через так называемую транзакцию (это короткая сессия общения с базой, в течение которой база гарантирует, что все изменения произойдут вместе или не произойдут вообще). Транзакция — как закрытая комната, в которой база работает с одним конкретным пользователем; пока комната занята, другие пользователи ждут.
ИИ-агенты халатно относятся к этим комнатам. Они легко могут открыть транзакцию, потом уйти делать долгий запрос к внешнему сервису (например, генерации видео, которая занимает 45 секунд), и всё это время держать комнату закрытой. Другие пользователи в очереди не могут попасть. На пятом одновременном пользователе вся база умирает. Это инцидент №15. Похожих в этой главе — 12 штук: тихое затирание данных при частичном обновлении, два одновременных пользователя списывают деньги с одного баланса (и оба успешно — балансу больно), эмодзи в сообщении ломает всю запись.
Глава 3. Фреймворки, hydration и мобильный UX (кейсы 25-36)
Инцидент 25. Hydration mismatch на динамическом времени
- Проект: Сервис медиа-генерации.
- Симптом: На странице отзывов клики и Stories переставали работать на мобильных. В консоли — hydration-warning.
- Корень: Компонент рендерил относительное время (
timeAgo()) и на сервере, и на клиенте. Между SSR и гидратацией проходило 200-400 мс, и значение менялось. React видел расхождение и переставал навешивать обработчики событий во всём поддереве. - Решение: На сервере — абсолютная дата, на клиенте — относительное время после
useEffectи mounted-state.
Инцидент 26. iOS Safari обрывает WebSocket через 25 секунд тишины
- Проект: Self-hosted чат-фронтенд.
- Симптом: На iPhone пользователь не получает следующий стримящийся токен примерно через 25 секунд после старта генерации.
- Корень: Safari агрессивно закрывает «бездействующие» сокеты на мобильных, чтобы экономить батарею. Агент не вставил периодический keep-alive на бэкенде.
- Решение: Пинг каждые 15 секунд с бэкенда (пустой
ping-фрейм или служебное событие), браузер видит активность, сокет жив.
Инцидент 27. Race condition в SWR-кэше при быстрой смене страниц
- Проект: Образовательная платформа.
- Симптом: При быстром переключении между уроками иногда отображались чужие комментарии — другого урока.
- Корень: Агент использовал SWR без передачи ключа кэша на смену маршрута. Промис старого запроса возвращался позже нового и переписывал данные.
- Решение: Ключ кэша SWR должен включать ID урока. И
useEffectна cleanup, который отменяет старый запрос на размонтировании.
Инцидент 28. Виртуализация списка ломала клавиатурную навигацию
- Проект: Образовательная платформа.
- Симптом: Tab-навигация по элементам каталога перепрыгивала элементы за пределами viewport.
- Корень: Агент применил
react-windowк каталогу из 800 уроков. Элементы вне viewport физически не существовали в DOM. Tab их не достигал. - Решение: Для списков <1000 элементов — без виртуализации. Для длинных — кнопка «загрузить ещё», а не infinite scroll.
Инцидент 29. CSS @container не работал в Safari определённой минорной версии
- Проект: Сервис медиа-генерации.
- Симптом: На устаревших iOS-устройствах разрушалась адаптивная сетка генератора.
- Корень: Агент применил container queries без фолбэка на media queries.
- Решение: Дублирующая media-query как фолбэк, container-query — поверх для современных браузеров.
Инцидент 30. localStorage переполнился на iOS, страница перестала открываться
- Проект: Образовательная платформа.
- Симптом: На некоторых iPhone сайт открывался белым экраном без ошибок в консоли разработчика.
- Корень: Агент кешировал визиты, прогресс уроков и чат в
localStorage. Safari на iOS режет квоту до 5 MB и при превышении бросаетQuotaExceededErrorсинхронно на запись. Код не оборачивал запись в try/catch и падал. - Решение: Try/catch на каждой записи в localStorage. Старые ключи удалять по LRU.
Инцидент 31. Intersection Observer не срабатывал на iframe-страницах
- Проект: Образовательная платформа.
- Симптом: Lazy-загрузка изображений и видео не срабатывала на встроенных уроках.
- Корень: Агент использовал
IntersectionObserverбез передачиroot. Внутри iframe root по умолчанию — окно iframe, а не родительская страница. - Решение: Явно передавать
rootили фолбэк наscroll-обработчик в iframe-контексте.
Инцидент 32. Tailwind не отрабатывал на динамически собранных классах
- Проект: Образовательная платформа.
- Симптом: Классы вида
bg-${color}не применялись в проде, хотя в dev работали. - Корень: Tailwind в проде вырезает неиспользуемые классы статическим анализом. Динамическая конкатенация не видна анализатору.
- Решение: Полные имена классов в коде или
safelistвtailwind.config.js.
Инцидент 33. Серверный fetch в Next.js не пробрасывал cookies
- Проект: Self-hosted чат-фронтенд.
- Симптом: Server Component получал 401 от собственного API, хотя браузер был залогинен.
- Корень: В Next.js App Router серверный
fetchне наследует cookies от входящего запроса. Агент об этом не знал. - Решение: Явно читать
cookies()и передавать в заголовок:fetch(url, { headers: { cookie: cookies().toString() } }).
Инцидент 34. Astro hot-reload терял состояние React-острова
- Проект: Образовательная платформа.
- Симптом: При редактировании MDX-урока в dev-режиме форма комментариев сбрасывалась.
- Корень: Astro перерисовывает страницу целиком при изменении MDX, React-острова монтируются заново. Не баг, фича — но агент рассчитывал на сохранение состояния.
- Решение: Состояние формы — в localStorage с восстановлением на mount. Это и для пользователя полезно.
Инцидент 35. Mobile keyboard скрывал поле ввода на iOS
- Проект: Self-hosted чат-фронтенд.
- Симптом: При фокусе на input chat-окна клавиатура поднималась и закрывала собой поле ввода.
- Корень: iOS-Safari при появлении клавиатуры не сжимает viewport, а смещает контент. Агент использовал
position: fixed; bottom: 0;— фиксированный элемент уходил под клавиатуру. - Решение: Использовать
env(safe-area-inset-bottom)иvisualViewport APIдля отслеживания реальной высоты viewport.
Инцидент 36. Lighthouse выдавал 100/100 — а реальный TTI был 8 секунд
- Проект: Образовательная платформа.
- Симптом: Lighthouse — 95+ по всем метрикам. Реальные пользователи жаловались на тормоза первого открытия.
- Корень: Lighthouse запускается в искусственной среде с пустым кэшем браузера, но идеальной сетью. Агент оптимизировал под Lighthouse, а не под реальный 3G мобильного оператора.
- Решение: Тестирование на «throttled fast 3G» — параметр в DevTools и в Lighthouse CLI.
Что это значит человеческим языком. Современная веб-страница не просто загружается одним куском HTML — она собирается дважды. Сначала сервер быстро рисует первый вариант страницы и отправляет его в браузер (это называется server-side rendering, SSR — рисование на стороне сервера). Пользователь видит экран. Потом в течение 300-400 миллисекунд браузер «оживляет» эту страницу — пересобирает её заново уже на стороне клиента и навешивает обработчики кликов (это и есть hydration — гидратация, оживление).
Если между первой версией страницы (с сервера) и второй версией (с клиента) появилось расхождение хоть в одном символе — браузер пугается и тихо отключает все клики во всём поддереве. Самый частый источник расхождения — динамическое время «1 минуту назад / 2 минуты назад» рядом с записями: сервер посчитал «1 минуту», за 300 миллисекунд время сдвинулось и клиент посчитал «2 минуты». Страница выглядит нормально, выглядит интерактивно — но ни одна кнопка не работает. Это инцидент №25, и таких ловушек в современном вебе много. 12 кейсов этой главы — про то, как реальные устройства (телефоны, разные браузеры, плохой интернет) ломают идеальный код, написанный ИИ на ноутбуке с быстрым Wi-Fi.
Глава 4. Внешние API, токены и лимиты (кейсы 37-48)
Инцидент 37. Региональная квота вендора видео-генерации
- Проект: Сервис медиа-генерации.
- Симптом: Параллельный запуск двух задач генерации видео вызывал 429 на второй, хотя дневная квота была не выработана.
- Корень: У провайдера для нашего региона лимит на concurrency был не 5, как в общей документации, а 1. Агент про это не знал.
- Решение: Очередь с конкурентностью 1 на стороне нашего бэкенда. Перед прод-релизом — обязательно тест на 2 параллельных запроса.
Инцидент 38. Скрытые reasoning-токены съедают весь бюджет
- Проект: Аналитический бот.
- Симптом: Биллинг за месяц превысил план в три раза при том же числе пользователей.
- Корень: Модель с reasoning по умолчанию генерирует «невидимые» токены рассуждения, которые тарифицируются как обычный output. Агент не выставил
reasoning_effort: lowилиthinking: off. - Решение: На production-эндпоинтах reasoning явно выключать или ограничивать. На сложных задачах — включать только под флагом.
Инцидент 39. Whisper API обрезал тишину в начале файла
- Проект: Аналитический бот.
- Симптом: При расшифровке аудиозаписей таймкоды субтитров сдвигались вперёд на 5-10 секунд.
- Корень: Whisper по умолчанию обрезает тишину в начале аудио. Агент не передал
temperature=0и начальный prompt-ориентир, из-за чего пауза в начале считалась шумом. - Решение: Передавать параметры калибровки тишины в API-запрос.
Инцидент 40. Webhook вендора генерации картинок упирался в JWT-авторизацию
- Проект: Сервис медиа-генерации.
- Симптом: Картинки генерировались успешно у вендора, но пользователи не видели их в интерфейсе часами.
- Корень: Вендор присылает результат через webhook POST-запрос. Агент настроил приём webhook'ов на эндпоинт, который требовал JWT авторизации пользователя. Сервер вендора получал 401 Unauthorized и прекращал доставку.
- Решение: Вывести webhook-эндпоинт из-под JWT, добавить проверку подписи запроса от вендора.
Инцидент 41. Утечка API-ключа в лог при verbose-режиме
- Проект: Self-hosted чат-фронтенд.
- Симптом: В Sentry-логе обнаружились полные authorization-заголовки запросов.
- Корень: При debug-логировании HTTP-клиента (включённом в проде по ошибке) логи писали целиком запросы с заголовками.
- Решение: Дефолтный фильтр на
authorization/x-api-keyна уровне логгера. И запрет debug-уровня в проде через env-переменную.
Инцидент 42. Повторная отправка webhook'ов без идемпотентности
- Проект: Сервис медиа-генерации.
- Симптом: Один и тот же результат генерации записывался в БД дважды-трижды.
- Корень: Вендор при недоставленном webhook'е (наш бэкенд ответил 500) ретрит до пяти раз. Агент не сделал идемпотентность по
request_id. - Решение: Уникальный индекс на
(vendor, request_id)в таблице результатов +ON CONFLICT DO NOTHING.
Инцидент 43. Streaming-ответ обрывался на буфере nginx
- Проект: Self-hosted чат-фронтенд.
- Симптом: Стриминг ответа от LLM работал в curl напрямую, но в браузере приходил кусками с задержками.
- Корень: Nginx по умолчанию буферизует ответы прокси. Агент не выключил буферизацию для эндпоинта streaming.
- Решение:
proxy_buffering off;иproxy_cache off;для streaming-маршрутов.
Инцидент 44. Timeout в gateway убивал длинные генерации
- Проект: Сервис медиа-генерации.
- Симптом: Генерации длиннее 60 секунд возвращали 504 Gateway Timeout, хотя бэкенд их завершал успешно.
- Корень: Cloud-балансировщик имел дефолтный таймаут на чтение ответа 60 секунд. Агент про это не знал.
- Решение: Перейти на webhook-модель для долгих задач — клиент инициирует, получает
job_id, опрашивает или ждёт push-уведомления.
Инцидент 45. CORS preflight на API-Gateway убивал latency на 200 мс
- Проект: Образовательная платформа.
- Симптом: Каждый запрос к API делался дважды — OPTIONS, потом GET/POST, latency удваивался.
- Корень: Агент не настроил кэш preflight через
Access-Control-Max-Age, и браузер запрашивал OPTIONS перед каждым запросом. - Решение:
Access-Control-Max-Age: 86400в Nginx — preflight кэшируется на сутки.
Инцидент 46. Ротация ключа провайдера ломала прод посередине дня
- Проект: Self-hosted чат-фронтенд.
- Симптом: После плановой ротации ключа сервис перестал отвечать на запросы.
- Корень: Агент обновил ключ в
.envи сделалsystemctl restart. Юнит запустился до того, как.envбыл полностью записан — прочитал старый ключ. - Решение: Атомарная запись
.envчерезmv tmpfile envfile, а не построчное обновление. Перезапуск службы — только после проверки checksum.
Инцидент 47. Лимит размера запроса в Anthropic API при длинных промптах
- Проект: Self-hosted чат-фронтенд.
- Симптом: При больших RAG-контекстах ответ от модели возвращал ошибку прежде, чем достигал генерации.
- Корень: Агент собирал контекст без учёта лимита 200K токенов на запрос. Иногда сжатый системный промпт + 50 retrieved chunks + история чата перебирал лимит.
- Решение: Подсчёт токенов клиентским tokenizer (
@anthropic-ai/tokenizer) до отправки. Если близко к лимиту — урезать историю чата.
Инцидент 48. Replicate webhook полз 6 часов вместо мгновенной доставки
- Проект: Сервис медиа-генерации.
- Симптом: Картинки от Replicate приходили с задержкой 5-6 часов после генерации.
- Корень: Replicate ставит webhook в очередь и шлёт после успешной доставки предыдущих. Если на один из ранних запросов наш бэкенд возвращал 5xx, очередь полностью застывала.
- Решение: Долбить Replicate за статусом из бэкенда (poll-режим) параллельно с webhook. Webhook остаётся как быстрый push, polling — как fallback.
Что это значит человеческим языком. Современный продукт почти никогда не работает в одиночку — он зовёт на помощь внешние сервисы. Сервис генерации картинок, сервис расшифровки голоса, сервис рассылки, сервис оплаты, языковая модель. Каждое такое обращение — это сделка с третьей стороной, у которой свои правила, лимиты и капризы.
ИИ-агент, когда пишет код для общения с внешним сервисом, использует знания из обучающей выборки — то есть документацию на тот момент, когда модель обучали. Реальные правила сервиса могут отличаться: лимит «20 запросов в секунду» на бумаге, а на деле 4 одновременных запроса уже отлетают с ошибкой. У сервиса может быть скрытый лимит для вашего конкретного региона, о котором в общей документации не написано. У сервиса может быть особенность: повторять отправку результата 5 раз, если ваш сервер один раз не ответил — и если вы не подготовились, один результат запишется в вашу базу пять раз. 12 кейсов в этой главе — про эти ловушки. Главная мысль: внешние API — это всегда минное поле, и ИИ-агент не помнит, где какая мина.
Глава 5. DevOps, безопасность и мульти-агенты (кейсы 49-56)
Инцидент 49. systemd-воркеры в RAM держат старый код при blue/green деплое
- Проект: Backend ассистента.
- Симптом: Обновления логики интеграции не вступали в силу несмотря на успешный blue/green-свитч и зелёный CI.
- Корень: Фоновые процессы воркеров запускались через systemd и кешировали скомпилированный код в RAM. Blue/green-скрипт переключал веб-слоты, но не перезапускал фоновые системные процессы.
- Решение: Хуки перезапуска системных служб в деплой-скрипт.
Инцидент 50. OOM-watchdog убивал здоровый сервис при падениях соседних
- Проект: Self-hosted чат-фронтенд.
- Симптом: Чат обрывал стриминг и перезапускался каждые пять минут, хотя логи показывали стабильность и низкое RAM.
- Корень: Watchdog проверял здоровье через
journalctl -k --since "1 hour ago" | grep -i oom— глобальный лог ядра. Когда на сервере падал по памяти любой другой процесс (тяжёлый сборщик dev-сервиса в Docker), watchdog видел OOM, ошибочно считал, что упал чат, и принудительно перезапускал здоровый контейнер. - Решение: Ограничить область поиска OOM строго cgroup целевой службы:
journalctl -u app-chat.service --since "5 minutes ago" | grep -i "oom-kill".
Инцидент 51. Мульти-агент и тихий саботаж через git stash -u
- Проект: Backend ассистента.
- Симптом: Из рабочего каталога спорадически пропадали незакоммиченные файлы и настройки конфигурации.
- Корень: Два параллельно запущенных ИИ-агента работали в одной директории. Один из них перед выполнением задачи решил «прибраться» и запустил
git stash -u, который тихо стёр все незакоммиченные файлы второго агента, включая локальные.env. - Решение: Жёсткое разделение рабочих ветвей и изоляция агентов через git worktree.
Инцидент 52. Коллапс синхронных explore-агентов
- Проект: Backend ассистента.
- Симптом: Мульти-агентная сессия исследования (5 агентов одновременно) ушла в бесконечный цикл уплотнения контекста, потратив $80 на токены без полезного результата.
- Корень: Агенты запускались синхронно, делили общий контекст чата, перебивали друг друга, путали стейты и впадали в «вежливые отказы» из-за перегрузки лимитов контекста.
- Решение: Строго асинхронный запуск субагентов с изоляцией рабочих директорий и оркестрацией через списки задач.
Инцидент 53. SSRF и обход авторизации внутренних API-прокси
- Проект: Сервис медиа-генерации.
- Симптом: Риск несанкционированных платных генераций за чужой счёт.
- Корень: Внутренние прокси
/api/billing-proxy/*слепо доверяли входящему заголовкуX-User-Idбез проверки подлинности источника. Внешний атакующий мог отправить прямой запрос на бэкенд и указать в заголовке любой ID. - Решение: Блокировка путей во внешнем Nginx (возврат 403) и заголовок
X-Internal-Tokenна базе разделяемого секрета.
Инцидент 54. Deploy mid-pipeline убивает активные генерации
- Проект: Сервис медиа-генерации.
- Симптом: Длинные генерации видео обрывались на 90% прогресса при автоматическом деплое мелкого фикса фронтенда.
- Корень: CI/CD пайплайн убивал активные контейнеры бэкенда без
SIGTERMи ожидания завершения in-flight задач. - Решение: Graceful shutdown в Docker-контейнере с ожиданием до 60 секунд перед остановкой.
Инцидент 55. WAF-bouncer и неполный матч путей исключений
- Проект: Сервис авто-разбана.
- Симптом: Страница авто-разблокировки сама выдавала 403 от bouncer, делая авто-разбан невозможным.
- Корень: Агент настроил
EXCLUDE_LOCATIONв bouncer со строгим exact-матчингом. Исключение/unban/работало, но API-запросы/unban/check-ipbouncer блокировал. Шаблон страницы бана кешировался Lua-движком Nginx в RAM и не применялся без перезагрузки веб-сервера. - Решение: Явное указание всех путей исключений в bouncer и автоматизация перезагрузки Nginx при обновлении шаблонов.
Инцидент 56. Неротируемые логи как бомба замедленного действия
- Проект: Сервис медиа-генерации.
- Симптом: Спорадическое переполнение диска
/var/до 100%, приводившее к падению PostgreSQL и блокировке записи сессий. - Корень: Агент при запуске фронтенда настроил простое перенаправление вывода
>> /var/log/app-frontend.log 2>&1. Лог не был зарегистрирован вlogrotateи непрерывно рос, записывая гигабайты CORS-ошибок и системных трассировок. - Решение: Регистрация лог-файла в
logrotateс лимитом 50 MB и хранением 7 архивных копий.
Что это значит человеческим языком. Сервер — это не один компьютер, а целая семья программ, живущих на нём одновременно. Веб-сервер (Nginx) принимает запросы. Приложение их обрабатывает. База данных хранит факты. Очередь задач помнит, что нужно сделать в фоне. Файрвол блокирует подозрительные запросы. Система мониторинга смотрит за здоровьем. Кэш ускоряет повторные обращения. Логи пишут историю всего, что произошло.
Каждая из этих программ требует ухода. Лог-файлы нужно регулярно подрезать, иначе они забьют весь диск и тогда упадёт база — потому что ей будет некуда писать данные (это инцидент №56). Когда вы разворачиваете новую версию кода, нужно правильно остановить старую версию — а не «прибить» её в момент важного действия (это инцидент №54, когда генерации видео обрывались на 90%). Если двух ИИ-агентов запустить в одной директории одновременно, они могут случайно стереть друг другу файлы (это инцидент №51 — реальная история, я потерял настройки на полдня).
Самая невидимая вещь в этой главе — безопасность внутренних API. У вас есть «внутренние» точки входа в код, которые предназначены для общения серверов между собой. ИИ-агент может оставить их открытыми снаружи. Тогда любой человек может подделать заголовок «я пользователь №123» и заказать платные услуги за чужой счёт (это инцидент №53). 8 кейсов в этой главе — про то, что под капотом сервера и в момент пиковой нагрузки. Если этого не знать, сервис будет иногда внезапно падать в самый дорогой момент дня.
Четыре упражнения на разбор кода
Прежде чем смотреть на промпты в блоке 5, попробуйте сами поймать четыре типовых паттерна. По одному на слой. Если все четыре найдёте без подсказки — у вас есть инженерный глаз; вы готовы писать собственные промпты под свой проект. Если не найдёте — это нормально, посмотрите ответ и запомните паттерн.
УПРАЖНЕНИЕ 1 · 5 МИН · СЕТЬ
DevOps и сетевая топология
Агент написал Nginx-конфигурацию для проксирования приложения, работающего под префиксом /chat. Найдите критическую ошибку, которая вызывает бесконечный редирект на мобильных устройствах:
location /chat {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
Ответ
В блоке location /chat отсутствует закрывающий слэш, а также не настроена обработка PROXY-protocol от внешнего балансировщика. Nginx при запросе к https://example.com/chat делает автоматический 301 редирект на https://example.com/chat/. Если внешний балансировщик проксирует трафик с PROXY-protocol, Nginx без сниппета real_ip_header proxy_protocol теряет реальный IP. Бэкенд видит IP балансировщика, антифрод считает это атакой мультиаккаунтинга и банит подсеть.
location = /chat { return 301 /chat/; }
location /chat/ {
include /etc/nginx/snippets/proxy-protocol.conf;
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
УПРАЖНЕНИЕ 2 · 5 МИН · БАЗА ДАННЫХ
Silent overwrite в PATCH-схеме
Агент спроектировал эндпоинт частичного обновления профиля (PATCH). Найдите логическую уязвимость, которая приводит к тихому затиранию настроек приватности:
const UpdateProfileSchema = z.object({
username: z.string().min(3).optional(),
email: z.string().email().optional(),
isPrivate: z.boolean().default(false)
});
const data = UpdateProfileSchema.parse(req.body);
await db.user.update({ where: { id }, data });
Ответ
Использование .default(false) в PATCH-схеме — это silent overwrite. При PATCH клиент присылает только те поля, которые меняет. Валидатор, видя отсутствие isPrivate, автоматически подставляет дефолт false и отправляет на перезапись. Настройки приватности сбрасываются, даже если пользователь их не трогал. В PATCH-схемах дефолты запрещены — только .optional().
УПРАЖНЕНИЕ 3 · 5 МИН · БЕЗОПАСНОСТЬ API
SSRF через внутренний прокси
FastAPI-middleware для извлечения ID пользователя во внутреннем API биллинга:
@app.middleware("http")
async def extract_user(request: Request, call_next):
user_id = request.headers.get("X-User-Id")
request.state.user_id = user_id
response = await call_next(request)
return response
В чём уязвимость? Как атакующий может использовать её для бесплатных генераций?
Ответ
Агент слепо доверился входящему заголовку X-User-Id, полагая, что эндпоинт «внутренний» и внешние запросы до него не доберутся. Если Nginx проксирует запросы на бэкенд без жёсткой фильтрации заголовков, любой атакующий выполнит:
curl -H "X-User-Id: 1" https://example.com/api/billing-proxy/charge
И спишет с чужого баланса. Исправление — секретный токен на базе разделяемого секрета:
INTERNAL_TOKEN = os.getenv("INTERNAL_API_KEY")
@app.middleware("http")
async def extract_user(request: Request, call_next):
token = request.headers.get("X-Internal-Token")
if token != INTERNAL_TOKEN:
return JSONResponse(status_code=403, content={"error": "Access Denied"})
request.state.user_id = request.headers.get("X-User-Id")
return await call_next(request)
УПРАЖНЕНИЕ 4 · 5 МИН · ГИДРАТАЦИЯ
Безопасная гидратация на динамическом времени
Почему этот React-компонент намертво ломает интерактивность кнопок на мобильных при SSR?
export function ReviewTime({ createdAt }) {
return (
<span className="time">
Добавлено: {timeAgo(createdAt)}
</span>
);
}
Ответ
Классическая ошибка гидратации. На сервере timeAgo вычисляется в один момент и возвращает «1 минуту назад». HTML отправляется в браузер. Через 300мс на клиенте гидратация вычисляет заново и возвращает «2 минуты назад». React видит расхождение между сервером и клиентом и полностью прекращает навешивать обработчики событий во всём поддереве. Интерфейс отрисован, но кнопки умирают для кликов. Решение — рендерить стабильную абсолютную дату на сервере, относительное время — только после монтирования:
export function ReviewTime({ createdAt }) {
const [mounted, setMounted] = useState(false);
useEffect(() => { setMounted(true); }, []);
return (
<span className="time">
Добавлено: {mounted ? timeAgo(createdAt) : formatDateAbsolute(createdAt)}
</span>
);
}
БЛОК 5. Четыре универсальных промпта аудита
Четыре промпта для четырёх технических слоёв (пятый слой, обслуживание сервера, требует прямого доступа к машине и не закрывается одним промптом — там нужен playbook реакции на инциденты, который не уместится сюда). Каждый промпт — это инструкция ИИ-агенту: какую роль играть, что искать, в каком формате отвечать.
Как использовать. Скопируйте промпт в файл ~/.claude/skills/audit-XX/SKILL.md (или в эквивалент вашего инструмента — Cursor, Cline, Continue) и подключите как автоматический шаг при сохранении кода. Дальше при каждом изменении агент пройдёт по диффу за секунды, нашёл подозрительный паттерн — выдаст отчёт, блокирует сохранение.
В промптах длинные тире намеренно заменены на двоеточия и запятые — это моё личное стилевое предпочтение для рабочих markdown-файлов, чтобы агент в редакторе не пытался их «исправлять» обратно.
Аудит транзакций к базе данных (Python / FastAPI / SQLAlchemy)
Промпт ищет место, где внутри транзакции к базе выполняется долгий внешний HTTP-вызов. Это самая частая причина выбивания пула соединений в боевой работе. На пятом одновременном пользователе сервис ложится, и обычно никто не понимает почему.
Роль: Сеньор-архитектор баз данных PostgreSQL.
Задача: проверить предоставленный Python/FastAPI код на критическую уязвимость
исчерпания пула коннектов СУБД.
Критерий проверки:
Найди все места, где внутри асинхронного контекста транзакции
(`async with session.begin():`, `async with db.begin():`)
выполняются блокирующие или асинхронные внешние HTTP-вызовы
(`httpx.post`, `requests.get`, `aiohttp` к LLM-провайдерам, генераторам видео,
S3 или платёжным шлюзам).
Почему это критично:
Удержание открытого соединения с БД во время ожидания ответа от внешнего API
(2 до 45 секунд) мгновенно забивает пул QueuePool SQLAlchemy и вешает весь
сервер при 5-10 параллельных пользователях.
Формат отчёта:
1. Файл и строка с уязвимостью.
2. Пошаговый план исправления: вынос внешнего HTTP-вызова за рамки транзакции.
3. Исправленный безопасный код.
Безопасная гидратация страницы (React / Next.js / Astro)
Промпт ищет в клиентских компонентах динамические данные, которые ломают интерактивность интерфейса на мобильных. Самый частый источник — относительное время «1 минуту назад / 2 минуты назад» рядом с записями, и оно тихо убивает все клики в поддереве страницы.
Роль: UX/UI Frontend Engineer.
Задача: проверить JSX/TSX-файлы на уязвимость гидратации React (Hydration Mismatch).
Критерий проверки:
Найди все клиентские компоненты (директива 'use client' или Astro-острова),
которые выводят динамические данные непосредственно в теле рендера без ожидания
монтирования на клиенте.
Особое внимание на:
1. Относительное время: timeAgo(), formatDistance().
2. Системное время: new Date(), Date.now().
3. Window-свойства, localStorage, window.innerWidth.
Формат отчёта:
Покажи места, где серверный HTML разойдётся с клиентским DOM. Напиши
безопасный вариант с mounted-state хуком: рендерить абсолютную дату на сервере,
относительное время после useEffect.
SSRF и фильтрация заголовков (Nginx и API-прокси)
Промпт ищет точки входа в код, которые предназначены для внутреннего общения серверов, но случайно остались открытыми снаружи. Если такая точка есть, любой атакующий может подделать заголовок «я пользователь №123» и заказывать платные услуги за чужой счёт.
Роль: Инженер по информационной безопасности (AppSec).
Задача: проверить API-роуты проксирования и конфигурацию Nginx на уязвимости
подмены личности (header spoofing, SSRF).
Критерий проверки:
1. Доверяют ли внутренние роуты вида /api/billing-proxy/, /api/chat/openai-compat/
заголовкам X-User-Id, X-Real-IP без проверки подлинности источника.
2. Блокирует ли внешний Nginx эти пути для запросов из интернета.
3. Защищены ли внутренние вызовы секретным ключом (X-Internal-Token),
который не пробрасывается на внешние апстримы.
Формат отчёта:
1. Список уязвимых эндпоинтов с цитатой кода или конфига.
2. Рекомендации по закрытию: location-блок в Nginx + middleware-проверка
секретного токена.
Ротация ключей и конфигов
Шаблон промпта для безопасной ротации секретов и API-ключей в проекте силами ИИ-агента, без риска сломать импорты или зависимости. Если ваш ключ протёк в логи или ушёл вместе с уволенным сотрудником, надо менять — и это надо уметь делать без падения сервиса в момент смены.
Роль: Системный архитектор и DevOps Engineer.
Задача: разработать процедуру ротации API-ключей и токенов внешних провайдеров
в проекте.
Критерий проверки:
1. Просканировать проект и составить карту файлов: .env, EnvironmentFile,
код инициализации клиентов API-провайдеров.
2. Подготовить Python-скрипт атомарной ротации ключей:
2.1. Делает резервную копию изменяемых файлов.
2.2. Безопасно обновляет секреты на новые значения через mv tmpfile envfile.
2.3. Не затрагивает структуру и синтаксис файлов.
2.4. Проверяет валидность конфигурации после записи.
3. Описать процедуру бесшовной перезагрузки зависимых systemd-юнитов
для применения новых ключей в RAM, без даунтайма.
Формат отчёта:
Полный список затронутых мест, безопасный скрипт автоматического обновления,
инструкция по перезапуску воркеров.
Иллюзия бычьего рынка ИИ не в том, что ИИ-агент плохо пишет код. Он пишет хорошо. Иллюзия в том, что менеджер с подпиской верит, что заменил себе шесть ролей разом. На деле помог одной, инженерной, и формально облегчил работу остальным пяти — но никого не закрыл. Через полгода-год продукт не растёт, и причина не в коде. Причина в том, что пять ролей вообще не работали в этой команде.
Что забирает инженерный аудит на себя. Файл известных проблем в каждом проекте обновляется после каждого инцидента. Четыре промпта P01-P04 встают в автоматическую проверку перед каждым сохранением кода. Раз в неделю — полный прогон по всему репозиторию через дешёвую LLM. Цена — копейки в месяц, сэкономлено — недели разбора инцидентов. Это закрывает шестую роль из шести; пять остальных закрываются людьми, а не промптами.
// Обсуждение
Можно писать анонимно. Укажите email, чтобы получать уведомления об ответах.