Перейти к содержимому
AUTHORВЫПУСК №008 → АВТОМАТИЗАЦИЯ АГЕНТАМИ: 90% НЕ ПРОМПТ / имейте совесть, когда будете делиться или копировать
>AISTUDY_
FOLDER №06 / SHEET 006 REV·01 / SCALE NTS BY · AUTHOR / 2026 RU
006 FOLDER №06 / SHEET 006
· · ·  ИЛЛЮЗИЯ БЫЧЬЕГО РЫНКА  · · ·
REV·01 / SCALE NTS / RU · 2026-05-22
PRE-FLIGHT · SELECT AUDIENCE · 2 MODES

ВЫБЕРИ, КАК ЧИТАТЬ

Выпуск идёт в двух версиях. Общая часть — для всех, кто строит продукт: разбор шести ролей в команде и как у них рассыпается работа, если за неё взялся только ИИ-агент. Инженерная часть — детальный технический разбор 56 инцидентов из моих проектов, для тех, кто чинит инфраструктуру руками.

РОЛИ · СЕТЬ · СУБД · БРАУЗЕР · API · СЕРВЕР · ПРОМПТЫ АУДИТА

РЕЖИМ:

006ИЛЛЮЗИЯ БЫЧЬЕГО РЫНКА НА ИИ-РАЗРАБОТКЕ

«У меня в руках ИИ, сейчас соберу свой SAP или CRM на тысячи пользователей.» Шесть ролей в продуктовой команде, которые ИИ-агент не закрывает, и 56 инфраструктурных инцидентов из моих проектов — с разметкой, какие из них валят боевой сервис в один момент.

СЕРИЯBULL-RUN 006
ВЫПУСК006 / 2026-05-22
ОБЪЁМ≈ 8 400 СЛОВ
УРОВЕНЬСЛОЖНЫЙ

БЛОК 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 — это просто файл с известными проблемами. Технический дневник проекта. У меня в каждом репозитории такой лежит, ИИ-агент читает его при старте сессии и не наступает на старые грабли.

Шесть шагов

  1. Соберите свой список известных проблем. Пройдите по последним 50-100 изменениям в коде проекта, выпишите все случаи, когда после разворачивания в боевую работу что-то пошло не так. Симптом — причина — решение. Если такого файла ещё нет, это первый артефакт у вас на руках. У меня он растёт второй год, и каждая новая запись стоила одного-двух дней разбирательств в прошлом.
  2. Разбейте их на пять технических слоёв. Путь от пользователя до приложения через прокси-серверы (сеть), хранение и изменение данных (база данных), поведение страницы в браузере у клиента (фронтенд и гидратация), вызовы внешних сервисов (API оплаты, генерации, рассылки), обслуживание сервера и реакция на инциденты (серверная часть). Эта раскладка — не теоретическая, она вышла из моих 56 инцидентов: всё кладётся в один из пяти слоёв без остатка.
  3. Напишите по одному промпту на каждый слой. Один промпт берёт изменения в коде (это называется diff — разница между старой и новой версией), читает один-два файла за раз, ищет паттерны конкретного слоя, выдаёт отчёт. Готовые шаблоны промптов P01-P04 — в блоке 5 этого выпуска. Их можно копировать и адаптировать под себя.
  4. Поставьте эти промпты в автоматическую проверку перед сохранением кода. Дешёвая модель (Anthropic Haiku, Google Gemini Flash, китайский Kimi K2.6 — это разные нейросети, которые умеют выполнять промпты быстро и дёшево) проходит по изменениям за секунды и копейки. При обнаружении подозрительного паттерна — блокирует сохранение и говорит, что именно ей не нравится. Программист (или ИИ-агент в кресле программиста) либо исправляет, либо обосновывает.
  5. Раз в неделю — полный аудит всего кода. Тот же набор промптов, но не на изменениях, а на всём коде проекта. Через GLM, Gemini или Kimi — там, где у вас лучше цена за объём. На выходе получаете список «потенциальных мест, на которые стоит посмотреть», который инженер-человек просматривает за час. Это вторая линия защиты — она ловит то, что протиснулось через первую.
  6. Обновляйте файл известных проблем после каждого нового инцидента. Это и есть память команды. ИИ-агент, который читает этот файл в начале сессии, перестаёт совершать те же ошибки. В моих проектах он лежит как раздел «Known Issues» внутри файла CLAUDE.md (это специальный файл, который ИИ-агент Claude Code читает автоматически перед началом работы). У других ИИ-агентов аналогичные механизмы есть тоже — почитайте документацию вашего.

Что НЕ работает

Не работает «один универсальный промпт, который ловит всё». Это самая частая ошибка тех, кто только начинает. ИИ-агент сильно слабеет, когда задача становится размытой. В пятислойном промпте он сконцентрируется на первом слое и пропустит остальные четыре. По одному промпту на слой — это не бюрократия, это техническая необходимость. Так у каждого промпта есть чёткий фокус.

Не работает «прогон промпта вручную раз в месяц». Через месяц у вас уже 30 изменений в коде, и найденный паттерн придётся искать в истории. К тому моменту он уже месяц жил в боевой работе и, возможно, успел стоить вам пользователей. В автоматической проверке перед сохранением паттерн ловится в моменте, до того, как код попадёт куда-либо.

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


БЛОК 4. Атлас 56 инцидентов

Пять глав по техническим слоям. Каждая глава — схема + 12 (или 8 в последней главе) инцидентов с симптомом, корнем и решением. После всех глав — четыре упражнения на разбор кода, по одному на слой.

Главы написаны в техническом тоне для инженерной аудитории. После каждой главы — короткое резюме «что это значит человеческим языком» для тех, кто читает этот выпуск в режиме «продукт и команда».

Глава 1. Сетевой хаос, Nginx и web-топология (кейсы 1-12)

СХЕМА 01 / БЫК ИИ-ОПТИМИЗМА И 4 СЛОЯ МЕЖДУ КЛИЕНТОМ И ПРИЛОЖЕНИЕМ ИИ-АГЕНТ: 10X КОДИНГ, 0X КОНТЕКСТ ИНФРЫ CDN / WAF PROXY proto NGINX basePath SPA / SSR SSO loop API CORS 12 инцидентов: на каждом из 4 слоёв ИИ-агент допускает по 3 ошибки. Все 12 кейсов сводятся к тому, что путь от клиента до приложения — не один шаг, а четыре. Если ИИ забыл про один из них, пользователь видит белый экран или ложный бан.
СХЕМА 01 / БЫК ИИ-ОПТИМИЗМА И 4 СЛОЯ МЕЖДУ КЛИЕНТОМ И ПРИЛОЖЕНИЕМ

Инцидент 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)

СХЕМА 02 / РОЛЛБЭК СЕССИИ СТИРАЕТ АУДИТ-ЛОГИ FASTAPI REQUEST DB SESSION (get_db) 1. ЗАПИСЬ АУДИТ-ЛОГА БЛОКИРОВКИ 2. HTTPException(403) 3. АВТО-ОТКАТ СЕССИИ — ЛОГ СТЁРТ ИЗОЛИРОВАННАЯ СЕССИЯ (SAFE AUDIT) AsyncSessionLocal() + commit() Аудит-лог сохранён независимо от ошибок в основном запросе.
СХЕМА 02 / СЕССИОННЫЙ РОЛЛБЭК И ВЫДЕЛЕННЫЕ ТРАНЗАКЦИИ

Инцидент 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)

СХЕМА 03 / HYDRATION MISMATCH — ДИНАМИЧЕСКОЕ ВРЕМЯ SERVER-SIDE SSR RENDERING HTML: «Добавлено 1 мин. назад» CLIENT-SIDE HYDRATION (300ms) DOM: «Добавлено 2 мин. назад» MISMATCH React видит расхождение и тихо отключает обработчики событий в поддереве. Интерфейс отрисован, но кнопки мертвы для кликов.
СХЕМА 03 / HYDRATION MISMATCH И ДИНАМИЧЕСКИЕ ДАННЫЕ

Инцидент 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)

СХЕМА 04 / БЫК БЬЁТСЯ В КВОТУ ВЕНДОРА (CONCURRENCY=1) АСИНХРОННЫЙ ВОРКЕР Task 1: Generate Video Task 2: Generate Video Parallel execution: 2+ tasks NO RATE LIMITS! VENDOR API LIMIT: CONCURRENCY = 1 RESULT: HTTP 429 Quota Exceeded и Job Drops ИИ-воркер шлёт задачи параллельно, не зная про региональную квоту вендора. Дневной лимит не выработан, но concurrency=1 — и второй запрос отлетает в 429.
СХЕМА 04 / БЫК БЬЁТСЯ В КВОТУ — CONCURRENCY=1

Инцидент 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)

СХЕМА 05 / SSRF И УТЕЧКА ВНУТРЕННИХ API-ПРОКСИ ATTACKER (EXTERNAL) Sends raw curl with: Header «X-User-Id: 1» NGINX PROXY Sloppily passes header to billing-proxy port INTERNAL BACKEND Trusts header blindly. CHARGES CHOSEN USER!
СХЕМА 05 / SSRF И НЕФИЛЬТРОВАННЫЕ ВНУТРЕННИЕ ЗАГОЛОВКИ

Инцидент 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-ip bouncer блокировал. Шаблон страницы бана кешировался 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-файлов, чтобы агент в редакторе не пытался их «исправлять» обратно.

P01

Аудит транзакций к базе данных (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. Исправленный безопасный код.
P02

Безопасная гидратация страницы (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.
P03

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-проверка
   секретного токена.
P04

Ротация ключей и конфигов

Шаблон промпта для безопасной ротации секретов и 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. Цена — копейки в месяц, сэкономлено — недели разбора инцидентов. Это закрывает шестую роль из шести; пять остальных закрываются людьми, а не промптами.

СЕРИЯ 006 · ВЫПУСК 2026-05-22

// Обсуждение

Можно писать анонимно. Укажите email, чтобы получать уведомления об ответах.