Перейти к содержимому
arckep.ru — все нейросети в одном месте без VPN Перейти
>AISTUDY_
Поддержать
AUTHORСвежий выпуск №024 → Куда внедрять агентов: фронт или тыл
Авторская колонка · семь классов инцидентов с AI-агентамиСЕРИЯ 016
Авторская колонка · выпуск №016

Семь классов
инцидентов

Что на самом деле ломается в production с AI-агентами — и как перестать наступать на одно и то же.

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

Зачем я это сделал

За полгода ежедневной работы с AI-агентами на пяти проектах у меня накопилось больше восьмидесяти задокументированных производственных инцидентов. Не «ой, забыл точку с запятой» — а настоящие: молчаливая потеря данных, двойные списания денег с пользователей, сервисы, которые считались живыми, а на самом деле были мертвы неделями.

В какой-то момент я перестал видеть отдельные баги и начал видеть систему. Одни и те же классы инцидентов повторялись в разных проектах, с разными моделями, в разное время. Агент на базе Claude попадал в ту же структурную ловушку, что и агент на базе Gemini. Это не вопрос «кто виноват» — это вопрос «какие условия гарантированно приводят к инциденту».

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

Класс 01

Разрыв между средой разработки и продакшеном

Суть простая: агент тестирует в одном окружении, а код поедет в другом. В эту яму падают и люди — но человек хотя бы спросит «а на проде точно такой же линукс?». Агент не спросит, пока вы ему не скажете.

1.1 «Working tree = production»

На одном из проектов система запускает Python-код прямо из рабочей директории. Никакого GitHub, никакого CI — файлы с диска сразу становятся production-сервисом. Один агент оставил в файле синтаксическую ошибку, ушёл, следующий перезапуск сервиса (автоматический, Restart=always) уронил бота.

Здесь агент виноват: не проверил компиляцию перед тем, как считать работу законченной. Решение: правило «py_compile после каждого edit». Пока компиляция не прошла — работа не сделана.

1.2 tmpfs на 2 ГБ — три параллельных npm install переполнили RAM

Агенты использовали git worktree для изоляции параллельных задач. Worktree размещались в /tmp, который на этом сервере — tmpfs в оперативной памяти (2 ГБ). Три параллельных npm install плюс сборка production-бандла переполнили RAM. Bash-инструмент агента начал молча падать с exit 1 на любую команду.

Здесь агент мог проверить df -h /tmp перед тем как разворачивать три тяжёлых процесса. Не проверил. Решение: worktree — только в /var/tmp (диск), правило записано в документацию каждого проекта.

Честное замечаниеТри других инцидента из этой же категории — облачное хранилище, доступное из дата-центра, но не из домашних сетей пользователей; продакшен-сервер без доступа к западным API из-за блокировок; разное поведение браузеров на разных моделях телефонов — я из класса исключил. Да, все три случились, но их бы не заметил и опытный разработчик при тех же вводных. Агент тут не уникально плох — проблема в неполноте информации о production-окружении, а не в том, кто пишет код.
Класс 02

Молчаливые отказы — самый опасный класс

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

2.1 Sentry не работал 70 дней

После миграции на Next.js 15+ конфигурационные файлы Sentry (sentry.client.config.ts, sentry.server.config.ts) перестали подхватываться фреймворком. Next.js 15+ использует instrumentation.ts, а не legacy config files. Агент, переносивший проект, не знал об этом изменении. Sentry SDK не загружался — 70 дней производственный фронтенд работал без единого события об ошибках.

Решение: проверка после деплоя — grep хеша Sentry DSN в клиентских чанках. Если нет — SDK не зашёлся в сборку, деплой откатываем.

2.2 Google Gemini чаты были бесплатными

Биллинг-прокси определял, нужно ли списывать деньги, по подстрокам chat/completions или messages в URL. Google Gemini шлёт запросы по путям :generateContent и :streamGenerateContent — ни одна не матчилась. Дополнительно: streamGenerateContent содержит заглавную G, проверка была case-sensitive.

Счёт: сотни запросов в день, каждый — от 0,06 до 0,83 рублей. Недели бесплатного трафика.

Решение: явные проверки на провайдер-специфичные пути. Не полагаться на «похоже на chat/completions».

2.3 print() вместо logger и kwargs, которые не работают

Массовое использование print() в бэкенд-сервисах — 1388 вхождений по данным аудита, накопленных исторически разными авторами (не только агентами). print() не пишет в structured logs, не виден в Sentry, не несёт контекст.

Но есть специфично-агентский вклад в эту же зону. Стандартный питоновский logging не принимает kwargs: logger.info("msg", user_id=123) падает с TypeError в проде. Тесты не ловят — при уровне WARNING строка не исполняется. Агент, привыкший к structlog-стилю с kwargs, написал именно так — и тихо уронил продакшен.

Решение: только f-строки со стандартным логгером. structlog.get_logger() — можно kwargs, но это другой объект.

2.4 Навык не работал в обычном чате — два контура исполнения, один реализован

Функция публикации пользовательских сайтов через чат. В обычном чате навык молча возвращал пустоту — модель не «видела» сайтов пользователя.

Корень: два независимых контура исполнения builtin-инструментов — клиентский (браузерный) для обычного чата и серверный (Node.js) для групповых агентов. Навык имел только server runtime. Клиентский реестр не находил исполнителя — «No executor found» — и молча возвращал пустой результат. Ни ошибки, ни лога.

Решение: любой новый инструмент с серверными данными требует парной регистрации — client executor и server runtime. Правило в чеклисте для новых интеграций.

Класс 03

Числа и конфигурация — почему агенты ошибаются в цифрах

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

3.1 GLM-5 Turbo в 24 раза дешевле реальной цены

В реестре моделей для GLM-5 Turbo стояла цена $0,05/$0,20 за миллион токенов. Реальная — $1,20/$4,00. На каждом миллионе output-токенов терялось 418 рублей. Цена была скопирована из соседней модели и не сверена с официальным прайсом.

3.2 Gemini считался в 40 раз дороже реальной цены

Обратная ситуация: для Gemini считали стоимость по длине текста, а модель возвращала отдельное поле thoughtsTokenCount, которое накладывалось на candidatesTokenCount. Двойной счёт — завышение в 40 раз. Пользователь платил рубль за две копейки. Это обратная сторона инцидента 2.2: там биллинг не распознавал путь Gemini и пропускал запросы бесплатно — здесь распознал, но насчитал в десятки раз больше нужного.

3.3 Qwen reasoning: 300+ токенов на «да, это баг»

Qwen 3.6 — reasoning-модель. Без enable_thinking: false генерирует цепочку рассуждений на 400-1500 токенов даже для тривиального ответа. Каждый такой ответ — деньги и лишние секунды.

Общее решение по всем трёмЦены — только с живой страницы провайдера. Формат ответа (usage) — под каждый API свой парсер, не один универсальный. Параметры модели (enable_thinking, responseMimeType) — обязательная часть чеклиста при регистрации новой модели.
Класс 04

Невидимые контракты фреймворков

У каждого фреймворка есть правила, которые нигде не написаны крупно, но нарушение которых ломает всё. Опытный разработчик знает их. Агент — нет, пока ему не покажут.

4.1 Zod 4: null ≠ undefined

Клиент шлёт null для неустановленного поля. Zod-схема ждёт undefined. .optional() допускает отсутствие ключа, но не null. Результат: 400 Bad Request.

Хуже: .default('someValue') в PATCH-схеме. Клиент отправляет только изменившиеся поля, Zod подставляет дефолт в опущенное → существующие данные в БД затираются.

Решение: .nullable().optional() для всех полей, .default() в PATCH запрещён. Contract-тесты с реальными payload клиента.

4.2 Четверги «прилипали» к средам

new Date() + setDate() в браузере — локальная таймзона (МСК, UTC+3). .toISOString() конвертит в UTC. Локальная полночь 16 апреля → UTC 15 апреля 21:00. Ключ в Map — «среда» вместо «четверга».

Решение: toLocalDateString(date) через getFullYear/getMonth/getDate. ESLint-правило блокирует .toISOString().split('T')[0].

4.3 timeAgo() в render → вся гидратация отвалилась

Client-компоненты в Next.js рендерятся дважды: сервер (SSR) и браузер (hydration). Date.now() или Math.random() в render дают разный результат → React видит mismatch → все обработчики событий в поддереве не привязываются.

Инцидент: timeAgo() в шести компонентах на странице. Сториз не открывались по клику. На главной те же сториз работали — там не было компонентов с timeAgo().

Решение: компонент <TimeAgo> с mount-guard — абсолютная дата при SSR, относительная через useEffect на клиенте.

4.4 [1,2,3,4].slice(0, max) режет до 4 при max=8

Кнопки выбора количества изображений. Агент написал [1,2,3,4].slice(0, max). При max=8 — четыре кнопки. Месяц в проде.

Решение: Array.from({length: max}, (_, i) => i + 1).

4.5 Цена без знака валюты

«Создать за 7» вместо «Создать за 7 ₽». Старый шаблон имел `${cost} ₽`, новый — `${cost}`. Агент не проверил визуальный вывод.

Решение: правило «всегда выводить знак валюты» в стандартах проекта.

Класс 05

Инфраструктура: деплой и миграции

Тут код обычно правильный — ломается обвязка вокруг него: порядок шагов деплоя, симлинк, забытая команда. Самый обидный класс, потому что ревью кода его не ловит — в самом коде дефекта нет, он в том, как код доезжает до прода.

5.1 npm run build напрямую ломает blue/green

Blue/green деплой: два слота .dist-blue / .dist-green, симлинк dist на активный. Агент запустил npm run build напрямую — создалась реальная директория dist/. Следующий deploy.sh упал.

Решение: никто кроме deploy.sh не трогает dist/. Правило зашито в документацию.

5.2 Миграция с будущей датой применилась досрочно

Миграция на удаление таблиц с именем 20260511d001_drop_.... deploy.sh делает alembic upgrade head при каждом деплое. Миграция применилась 22 апреля вместо 11 мая.

Решение: датированные миграции не коммитятся в main до дня применения. Держатся в feature-ветке или оборачиваются в условный op.execute.

5.3 Build до миграций — дефект пайплайна

Next.js с cacheComponents: true prerender'ит страницы при next build. Если новый код читает колонку, добавляемую миграцией, а миграция в деплой-скрипте идёт после сборки — билд падает с «column does not exist».

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

5.4 Забытый rsync сломал SPA

Сборка требует двух rsync: .next/ и public/_spa/. Агент скопировал .next/, пропустил public/_spa/. SPA не мог загрузить чанки — 404 на всё.

Решение: build-скрипт с проверкой BUILD_ID, не полагаться на память.

Класс 06

Изоляция и безопасность

Агент постоянно запускает дочерние процессы — тесты, миграции, сборки. Каждый наследует окружение родителя, и если там лежит доступ к боевой базе, агент об этом не подозревает. Цена ошибки здесь максимальная: не кривая вёрстка, а стёртые данные или открытый к деньгам доступ.

6.1 DATABASE_URL утекал в тесты соседнего проекта

Сервис автоматического исправления ошибок: получает алерт, создаёт изолированный git-клон, запускает агента. При запуске pytest в клоне процесс унаследовал DATABASE_URL родителя. Тестовая фикстура сделала DROP TABLE ... CASCADE в боевой базе данных.

Решение: принудительная очистка DATABASE_URL и DATABASE_URL_TEST из окружения subprocess. Не полагаться на .env клонируемого проекта.

6.2 Каскадный отказ: бан-система забанила живого пользователя

Здесь нет одного виноватого — это классический каскад из четырёх независимых причин, каждая из которых по отдельности не была ошибкой:

Пользователь с iPhone пытался сгенерировать видео. Первый запрос — ошибка. Но UI показал пустой toast: HTTP/2 не передаёт reason phrase, фронтовый обработчик ошибок не подставил человеческий текст. Пользователь нажал «Сгенерировать» ещё пять раз — 6 запросов за 134 миллисекунды. Nginx, настроенный с burst=5 на генерационных эндпоинтах, засчитал превышение лимита. Защитный фильтр сработал — бан на 4 часа. Пользователь с балансом 1481 ₽ потерял доступ.

Каждый элемент цепочки был разумен: низкий burst — защита от ботов, HTTP/2 без reason phrase — спецификация, пустой toast — баг в парсинге ошибок (вот здесь агентская вина реальна). Но вместе они дали катастрофу.

Решение — четыре слоя защиты, по одному на каждое звеноФронтовый mutex против repeat-кликов; идемпотентность на бэкенде через Idempotency-Key; человеческий текст ошибки вместо пустого toast; whitelist авторизованных пользователей в бан-системе.

6.3 Забаненный пользователь продолжал списывать деньги

JWT с TTL 30 дней. Пользователя забанили, но все живые токены продолжали работать — handler'ы проверяли только подпись и срок, не сверяясь с бан-листом.

Это архитектурная ошибка: бан применялся на уровне auth middleware, но money-path эндпоинты его не проверяли. Решение: is_user_banned(user_id) в каждом money-path handler'е, а не только в middleware.

Класс 07

LLM-специфика: когда модель не «сама разберётся»

У каждого AI-провайдера свой контракт. Модель не знает его, пока вы явно не скажете. И агент, программирующий взаимодействие с моделью, тоже.

7.1 Gemini thoughtSignature терялся в tool-use loop

Gemini 3.x возвращает thoughtSignature в каждом functionCall — зашифрованное представление reasoning. Это поле обязательно для возврата в следующем запросе. У Anthropic такого требования нет. Общий промежуточный формат, спроектированный по Anthropic-модели, терял поле при конверсии. Gemini API: «Function call is missing a thought_signature».

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

7.2 Gemini отдаёт JS-объекты вместо JSON

По умолчанию Gemini может вернуть {key: "val"} вместо {"key": "val"} — без кавычек на ключах. JSON.parse падает. Промпт «верни JSON» не гарантирует.

Решение: responseMimeType: 'application/json' — нативный валидатор на стороне модели.

7.3 LLM не умеют считать символы

Проверка длины рекламного текста через LLM-критика. Тот подтвердил «длина в порядке». API ответил «Превышена допустимая длина» — ошибка на 5 символов из 81.

Решение: character-count — только кодом (string.length). LLM — для семантики, не для чисел.

7.4 Claude refusal text стал заголовком кнопки

Пакетный перевод UI-строк через Claude Haiku. Короткие ключи спровоцировали отказ: модель вернула «I'm sorry, I can't assist with that request» вместо перевода. Текст записался в JSON-файл как валидный перевод. На сайте кнопка показала «I'm sorry, I can't assist with that request» как label.

Решение: пост-обработка LLM-переводов с детекцией refusal-паттернов. CI-сканер падает при обнаружении таких текстов в файлах переводов.

Свод

Что из этого вышло

Эти семь классов — не теоретическая классификация. Каждый вырос из реальных инцидентов на живых проектах. Не все они — ошибки агентов. Часть — дефекты пайплайна, часть — каскадные отказы, где нет одного виноватого. Но у них есть общее свойство: каждый инцидент можно было предотвратить, если бы перед агентом (или перед человеком, запускающим агента) была явно сформулирована граница, которую нельзя переходить.

Вот что я вынес:

КонтекстАгент не знает среду исполнения, пока вы ему не расскажетеtmpfs или диск? Права на запись в чужой .git? Эту информацию нужно держать в проектной документации, которую агент читает до начала работы. И главное — не ждать, что агент «сам догадается».
НаблюдаемостьМолчаливый отказ — самый опасный вид инцидентаВерификация «код работает» — это не «тесты прошли», а «в production-логах появились ожидаемые события». Нет наблюдаемости — считай, что код не работает.
ЧислаНи одно число в конфиге не вечноЦены провайдеров меняются, модели появляются и исчезают, лимиты сдвигаются. Проверка «число из живой документации провайдера» — обязательный шаг.
КонтрактыПравила, которые агент нарушает по незнанию, должны быть частью документацииНе «агент должен помнить», а «проект не даёт забыть» — ESLint-правила, CI-гейты, gotchas-документы.
ПайплайнСтруктурный дефект чинится на уровне скриптов, не памяти агентаЕсли порядок шагов при деплое гарантирует, что правка «новая колонка + новый код» падает — проблема в пайплайне, а не в том, кто написал код.
ИзоляцияНи один subprocess агента не должен иметь доступа к productionОчистка переменных, read-only доступ к базам, проверка прав — не «best practices», а условия выживания.
LLMКаждый AI-провайдер — отдельный набор контрактовЧеклист для новой модели: формат ответа, обязательные параметры, лимиты, особенности streaming. Не «провайдеры похожи», а «каждый уникален, пока не доказано обратное».

За полгода я превратил этот список в работающую систему: каждый проект имеет AGENTS.md с инвариантами, KNOWN_ISSUES.md с историей инцидентов, и gotchas.md с неочевидными правилами, которые агент читает до правки. Новая проблема → запись в документацию → та же проблема не повторяется.

Агенты не стали идеальными. Они по-прежнему не знают того, чего нет в контексте. Но у них теперь есть карта уже обнаруженных проблем — и они перестали открывать одни и те же люки во второй раз.

Прикладное

Что скопировать агенту

Ниже — конкретные инструкции, которые можно дать своему агенту. Каждый пункт привязан к инциденту из статьи: не абстрактный совет, а прямое противоядие. Файл с правилами называется AGENTS.md — это универсальное имя, его читают все AI-агенты (Claude, Gemini, Copilot), а не кто-то один. Вайбкодеру достаточно скопировать текст и отправить агенту — дальше он сам.

Про длинные тиреВ блоках ниже длинные тире заменены на запятые и двоеточия — это рабочие файлы для копирования агенту, а не текст статьи. Копируйте как есть.

Базовый AGENTS.md

Скажи агенту: «Создай в корне проекта файл AGENTS.md с этими правилами. Адаптируй под наш стек — если у нас Python, а не JavaScript, поправь команды сборки. Если у нас нет тестов — добавь их создание в план».

# Правила для агента

## Перед тем как сказать «готово»

1. Код компилируется. Какая бы ни была команда сборки в этом проекте, она должна проходить.
2. Продакшен трогает ровно один способ. Если в проекте уже есть скрипт деплоя, только он. Если нет, ты должен его создать, а не жать кнопки вручную.
3. Каждый новый код-путь оставляет след. Если что-то пошло не так, это должно быть видно в логах, а не молча исчезнуть.

## Что тебе запрещено

- Трогать работающий код, не связанный с задачей, даже «для красоты»
- Удалять импорты, переменные, функции «за ненадобностью»: они могут быть нужны соседу
- Писать в логи голые сообщения без контекста (какой пользователь, какой запрос)
- Оставлять в коде «TODO» или «доделаю потом» без явной задачи в трекере

## Числа и конфигурация

- Любая цена, лимит, версия модели: из живой документации провайдера (WebSearch), не из памяти
- Если не уверен в числе: «нужна проверка», не придумывай

## Зоны максимального риска

- Код, который меняет деньги или данные пользователя: проверяй дважды
- Миграции базы данных: отдельная осторожность, не коммитить с будущей датой, тестировать на копии
- Удаление файлов, таблиц, данных: только с явного разрешения

Чеклист «перед деплоем»

Скажи агенту: «Перед каждым деплоем проходи этот список и показывай мне результат».

## Pre-deploy checklist

1. Сборка проходит без ошибок
   -> без этого: инцидент 5.1 (прямой npm run build сломал blue/green)
2. Тесты проходят. Если тестов нет, создать хотя бы smoke-тест
3. Все новые цены и лимиты сверены с живой документацией провайдера
   -> без этого: инциденты 3.1 (цена в 24x ниже) и 3.2 (цена в 40x выше)
4. Все новые переменные окружения задокументированы
5. Все новые эндпоинты и фоновые задачи пишут в логи при ошибках
   -> без этого: инциденты 2.3 (print вместо logger) и 2.1 (Sentry не работал 70 дней)
6. Миграции базы данных протестированы на копии
   -> без этого: инциденты 5.2 (Alembic досрочно) и 5.3 (build до миграций)
7. Деплой, только через проектный скрипт или CI, не руками
   -> без этого: инциденты 5.1 и 5.4 (забытый rsync сломал SPA)

Чеклист «новая AI-модель»

Скажи агенту: «Когда добавляешь новую LLM-модель, проходи этот список».

## New model registration checklist

1. Название модели, точное, как в API провайдера (не из памяти модели)
   -> модель уверенно называет несуществующие версии, сверяй имя по живому списку API
2. Цена, с официальной страницы pricing, для всех типов токенов (input, output, thinking, cache)
   -> без этого: инциденты 3.1 (24x) и 3.2 (40x)
3. Формат usage-ответа, отдельный парсер под провайдера. OpenAI, Google и Anthropic отдают usage в разных полях
   -> без этого: инциденты 3.2 (thoughtsTokenCount не учтён) и 2.2 (Gemini-чат был бесплатным)
4. Провайдер-специфичные параметры:
   - Gemini: responseMimeType application/json для structured output -> инцидент 7.2
   - Qwen: enable_thinking false для обычных ответов -> инцидент 3.3
   - Gemini 3+: thoughtSignature обязан выживать через всю цепочку вызовов -> инцидент 7.1
   - Проверить, что наружу к провайдеру уходит его API-ключ, а не внутренний токен авторизации
5. Streaming: на каких URL модель отдаёт поток, как помечает конец стрима
   -> без этого: инцидент 2.2 (streamGenerateContent не матчился)
6. 2-3 теста с реальным вызовом API (не mock)

Чеклист «новый проект»

Скажи агенту: «Создай проект. Затем, до первого коммита, заложи эти семь вещей».

## New project foundation (до первого коммита)

1. Файл AGENTS.md с правилами, по шаблону выше
   -> закрывает классы 1, 2, 3 и 5
2. Файл KNOWN_ISSUES.md, пока пустой, сюда будем записывать каждый инцидент
   -> чтобы один и тот же инцидент не повторялся
3. Файл docs/gotchas.md, пока пустой, сюда неочевидные правила фреймворка
   -> закрывает класс 4 (Zod, timezone, hydration, всё сюда)
4. Нормальное логирование: структурированное, с контекстом, не print() / console.log()
   -> без этого: инцидент 2.3
5. Мониторинг ошибок, подключён и проверен до первого деплоя
   -> без этого: инцидент 2.1 (70 дней без Sentry)
6. Один способ деплоя (скрипт или CI), который никто не обходит
   -> без этого: инциденты 5.1 и 5.4
7. CI, который падает при ошибках сборки или тестов, до первого коммита

Что покрывает каждый чеклист

Класс инцидентаЗакрывается
1. Разрыв dev/prodAGENTS.md + чеклист «новый проект»
2. Молчаливые отказы«перед деплоем» п.5 + «новый проект» п.4-5
3. Числа и конфигурацияAGENTS.md + «новая модель» п.2-3 + «перед деплоем» п.3
4. Контракты фреймворковdocs/gotchas.md — каждый новый контракт → запись
5. Инфраструктура«перед деплоем» п.1,6,7 + «новый проект» п.6-7
6. ИзоляцияAGENTS.md (зоны риска) + опыт в KNOWN_ISSUES.md
7. LLM-спецификачеклист «новая модель» целиком

Это не сделает проект неуязвимым. Но это закроет шесть из семи классов инцидентов — не потому, что агент стал умнее, а потому что ему явно сказали, где наступать нельзя.

Серия 016 · 2026-06-27 · семь классов инцидентов, выведенных из 80+ реальных производственных постмортемов
Авторская колонка · выпуск №016 · «Семь классов инцидентов с AI-агентами»

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

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