Модель не виновата: разбираем 3 громких ИИ-инцидента, которые случились из-за отсутствия архитектуры
Я проектирую системы на LLM, и после каждого громкого ИИ-инцидента мне прилетает одна и та же ссылка с одним и тем же вопросом: «а у нас такое может случиться?» Чтобы отвечать не на глазок, я завёл привычку разбирать каждый такой инцидент до конкретной технической причины. За последние месяцы таких разборов набралось три, и они удивили меня не сходством, а различием. Дыры, в совершенно разных местах: у одного проекта не проверялись источники, у другого, данные на границах системы, у третьего, права автономного агента. А вот причина одна: системы вокруг LLM строили люди, которые умеют писать промпты, но не умеют проектировать архитектуру.
Судите сами. Deloitte Australia возвращает правительству деньги за отчёт, в котором ИИ выдумал источники (Источник). В Миннесоте полиция четырьмя машинами блокирует на парковке журналиста, потому что сеть ИИ-камер несколько дней вела его как угонщика, из-за опечатки, сделанной за две тысячи миль от него (Источник). А основатель инди-SaaS просыпается и обнаруживает, что написанный моделью код ночью отменил все подписки его клиентов: месячная регулярная выручка обрушилась до $38, и то лишь потому, что уцелели две тестовые подписки (Источник).
Из этих разборов у меня сложились три идеи, через которые я теперь смотрю на любую LLM-систему. Первая: контроль должен жить в детерминированном коде, а не в промпте, промпт снижает вероятность ошибки, но не даёт гарантий. Вторая: у каждой техники есть область применимости, и вероятностные компоненты нельзя ставить на места, где нужна детерминированность. Третья: строгость контролей калибруется под цену ошибки, высокорисковые сценарии требуют других порогов и других процедур, чем рутинные. Три инцидента ниже, это три нарушения этих идей в дикой природе. Разберём каждый: что произошло, где сломалось и как закрывается. Решения покажу с кодом на Claude API, во-первых, потому, что чинить абстрактными рассуждениями скучно, а во-вторых, потому, что сам строю на Claude: из всех моделей, с которыми я работал в проде, у него самый продуманный инструментарий именно для архитектурных гарантий, citations, structured outputs, tool use спроектированы так, что правильную систему построить проще, чем неправильную.
-
Кейс 1. Несуществующие источники в отчёте Deloitte
-
Кейс 2. Распознавание номеров машин без валидации
-
Кейс 3. Массовое удаление подписок в проде
-
Выводы
Кейс 1. Deloitte: отчёт, который все проверили и никто не прочитал
Хроника: сноски, которых не было
Deloitte Australia готовила для правительства официальный отчёт и использовала при этом ИИ. Документ прошёл внутренние ревью, не одно, и был опубликован. А потом внешний исследователь, читавший его по своей академической надобности, начал проверять сноски. Часть цитат была приписана не тем авторам. Часть источников не существовала вовсе. Дальше всё развивалось по законам жанра: публикации в СМИ, вопросы от законодателей, исправленная версия документа и возврат части оплаты по контракту. Для фирмы, которая продаёт в том числе аудит и проверку чужих отчётов, сюжет вышел особенно неловкий.
Где сломалось: беглость перестала означать достоверность
Самое интересное в этой истории, не галлюцинации. То, что языковые модели выдумывают источники, известно каждому, кто провёл с ними больше недели. Интересно, что документ прошёл несколько слоёв проверки, и ни один из проверяющих не споткнулся. Ревьюеры читали текст и оценивали его связность, логику, убедительность. Но LLM генерирует именно связный, логичный и убедительный текст, в том числе тогда, когда врёт. Беглость изложения десятилетиями работала прокси-сигналом качества, и процесс ревью молча на неё полагался. С приходом генеративных моделей этот сигнал умер, а процессы этого не заметили.
Архитектурно здесь нарушен принцип groundedness: фактическое утверждение модели должно быть привязано к проверяемому источнику, и проверять привязку должен механизм, а не внимательность уставшего человека. «Мы попросили модель не выдумывать» — это не механизм. Модель, которую попросили не выдумывать, выдумывает реже. Реже, не значит никогда.

Как закрыть: citations плюс механическая проверка
Теперь, как это закрывается на Claude. Первый слой, citations: API умеет отвечать строго по переданным документам и к каждому утверждению возвращать структурированную ссылку на фрагмент, из которого оно взято.
import anthropic client = anthropic.Anthropic() response = client.messages.create( model=»claude-sonnet-4-6″, max_tokens=2048, messages=[{ «role»: «user», «content»: [ { «type»: «document», «source»: { «type»: «text», «media_type»: «text/plain», «data»: report_source_text, # исходные данные исследования }, «title»: «Данные аудита, Q2», «citations»: {«enabled»: True}, # ключевая строка } { «type»: «text», «text»: «Составь раздел отчёта о выявленных рисках. » «Используй только предоставленный документ.» }, ], }], )
Ответ приходит блоками, и у каждого блока естьcited_textс координатами фрагмента источника. Само по себе это ещё не гарантия, гарантией становится второй, детерминированный слой. Перед сборкой финального документа код проверяет каждый блок:
def validate_grounding(response) -> list[str]: «»»Блоки без подтверждённых цитат не попадают в документ.»»» rejected = [] for block in response.content: if block.type != «text»: continue citations = getattr(block, «citations», None) if not citations: rejected.append(block.text) # утверждение без источника continue for c in citations: # цитата обязана дословно присутствовать в источнике if c.cited_text not in source_documents[c.document_index]: rejected.append(block.text) return rejected
Утверждение без прошедшей проверку цитаты физически не попадает в финальный текст, оно уходит в список «требует ручной проверки». Ревьюер перестаёт искать иголку в стоге сена: система сама подсвечивает те несколько процентов текста, которые ничем не подкреплены, и человеческое внимание тратится только на них. У Deloitte такой список из пары десятков строк сэкономил бы контракт и заголовки в прессе.
Кейс 2. Flock: два потерянных символа и четыре полицейские машины

Хроника: «Вы вооружены? Выйдите из автомобиля!»
Эта история случилась в июле 2026-го, и её главный герой описал её сам, Джоэл Федер, автомобильный журналист The Drive, пятнадцать лет тестирующий машины. В то воскресенье он с женой поехал по магазинам на Range Rover за $155 тысяч из пресс-парка Jaguar Land Rover. На выезде с парковки их заблокировали четыре полицейские машины. «Вы вооружены? Выйдите из автомобиля!».
Выяснилось, что полиция Плимута несколько дней вела Федера по камерам: система Flock, общенациональная сеть ИИ-камер, распознающих номера, пометила его машину как краденую. Офицер даже показал журналисту фотографии его собственных перемещений в приложении. Час разбирательств, звонок в Jaguar Land Rover прямо с парковки, и картина сложилась.
В Калифорнии заявили о пропаже номерного знака 34 03 DTM. Но на номерах этого формата средние цифры напечатаны мелким шрифтом, и при вводе в базу их просто опустили, записали «34 DTM». Распознавание Flock эти мелкие цифры на проезжающих машинах тоже не считывало. Результат: любая машина пресс-парка JLR с номером шаблона «34 ## DTM», а их по стране десятки, начала триггерить алерт о краже. Полиция сказала Федеру, что только в Миннесоте в ту неделю отслеживали ещё четыре такие машины; он просто оказался первым задержанным. Вишенка: исходный номер вообще не был украден, его потеряли на фотосессии.
И ещё одна деталь, важная для нашего разбора. У Flock есть официальная политика: алерт — это повод для проверки, офицер обязан вручную сверить номер до каких-либо действий. Политика существовала. На практике алерт стал основанием для многодневной слежки со «стингом» на парковке. Один из офицеров честно сказал журналисту: считайте, повезло, что это Плимут, в Миннеаполисе вас бы положили на землю под прицелом.

Где сломалось: три границы без единой проверки
Здесь нарушен принцип валидации на границах системы, причём трижды. Вероятностный вывод (распознавание с камеры) сравнивался с невалидированной записью (усечённый номер). Частичное совпадение обрабатывалось как точное. И результат сравнения напрямую запускал высокорисковое действие, а человеческая проверка, которая должна была стоять между ними, жила в документе, а не в системе.
|
Граница |
Как было у Flock |
Как должно быть |
|
Ввод в базу |
усечённый номер «34 DTM» сохранён без проверки |
schema/pattern-валидация при записи: неполная запись не сохраняется |
|
Матчинг |
частичное совпадение обработано как точное |
только exact match; неполные распознавания не участвуют |
|
Действие |
алерт стал основанием для слежки и задержания |
human-in-the-loop как обязательная ветка кода, а не пункт политики |
Как закрыть: строгая схема и human-in-the-loop, который нельзя пропустить
Паттерн универсальный: он касается любой системы, где модель извлекает структурированные данные, номера, идентификаторы, суммы, коды диагнозов. На Claude это закрывается через structured outputs: модель не пишет ответ свободным текстом, а обязана заполнить строгую схему.
schema = { «name»: «register_plate», «description»: «Регистрация распознанного номерного знака», «input_schema»: { «type»: «object», «properties»: { «state»: {«type»: «string», «enum»: US_STATES}, «plate»: { «type»: «string», # полный формат, включая мелкие средние символы «pattern»: «^[0-9]{2} [0-9]{2} [A-Z]{3}$» }, «confidence»: {«type»: «number», «minimum»: 0, «maximum»: 1}, «all_characters_legible»: {«type»: «boolean»}, }, «required»: [«state», «plate», «confidence», «all_characters_legible»], }, } response = client.messages.create( model=»claude-sonnet-4-6″, max_tokens=1024, tools=[plate_schema], tool_choice={«type»: «tool», «name»: «register_plate»}, messages=[{«role»: «user», «content»: [ {«type»: «image», «source»: {…}}, # кадр с камеры {«type»: «text», «text»: «Распознай номерной знак.»}, ]}], )
Схема убивает сам класс ошибки из кейса. Запись «34 DTM» не проходит pattern , неполный номер не может быть сохранён в базу, ни оператором, ни моделью. Не разглядела средние цифры, обязана выставить all_characters_legible: false , и такая запись вообще не участвует в матчинге.
Дальше слой сравнения и действия, уже без всякого ИИ:
def process_recognition(result: dict) -> Action: # 1. Неполные распознавания не матчатся вообще if not result[«all_characters_legible»]: return Action.LOG_ONLY # 2. Только точное совпадение, никаких подстрок match = stolen_plates_db.exact_lookup( state=result[«state»], plate=result[«plate»] ) if match is None: return Action.NONE # 3. Высокорисковое действие — только через человека if result[«confidence»] < 0.98: return Action.QUEUE_FOR_REVIEW return Action.ALERT_WITH_MANDATORY_HUMAN_VERIFICATION

Сравните с оригиналом. «Офицер должен проверить вручную» у Flock, это фраза в PDF с политиками. ALERT_WITH_MANDATORY_HUMAN_VERIFICATION , это ветка кода, которую нельзя пропустить. Между этими двумя состояниями и проходит граница между пожеланием и гарантией. У Федера на этой границе стояли четыре полицейские машины.
Кейс 3. $38 MRR: агент, которому дали ключи от кассы

Хроника: 7 секунд, пока все спали
Третья история громыхнула в X в те же июльские дни. Основатель инди-SaaS проснулся утром и увидел, что месячная выручка бизнеса упала на тысячи долларов, до $38. Паника, проверка Stripe: клиенты не отписывались. Код, написанный моделью GPT 5.6 Sol в агентном режиме, ночью отменил каждую активную подписку. На всё ушло 7 секунд. Технический виновник, cron-джоба, которая обработала пустую очередь удаления как валидную задачу «удалить всё». Уцелели две тестовые подписки.
Совпадение, которое сделало историю вирусной: буквально за день до этого известный в AI-сообществе разработчик Мэтт Шумер написал, что та же модель в агентном режиме случайно удалила почти все файлы на его Mac. Автор поста про Stripe сделал из этого вывод в духе «модель X доверять продакшену нельзя, модель Y, можно».
Вывод эмоционально понятный, и инженерно неверный в обе стороны. Но об этом чуть ниже, сначала про то, что здесь сломалось.
Где сломалось: права агента жили в промпте
Автономный код имел права на массовую необратимую операцию с деньгами и выполнил её без подтверждения, без лимитов, без dry-run, в три часа ночи. Если у агента и были ограничения, они жили в промпте, в слое пожеланий. Принцип минимальных привилегий требует противоположного: то, что агент не должен делать, должно быть тем, что он не может сделать. На уровне выданных инструментов и ключей, а не инструкций.

Как закрыть: три слоя между агентом и деньгами
На Claude агентная система с такими гарантиями строится в три слоя. Первый, узкие инструменты. Агент не получает «доступ к Stripe», он получает конкретные операции, и опасных среди них просто нет:
tools = [ { «name»: «get_subscription», «description»: «Прочитать данные одной подписки», «input_schema»: {…}, }, { «name»: «cancel_subscription», «description»: «Отменить ОДНУ подписку по явному ID», «input_schema»: { «type»: «object», «properties»: { «subscription_id»: {«type»: «string»}, «reason»: {«type»: «string»}, }, «required»: [«subscription_id», «reason»], }, }, # инструмента cancel_all / bulk_cancel не существует ]
Второй слой, исполнение на вашей стороне. При tool use Claude не выполняет операции сам: он возвращает запрос на вызов, а вызывает ваш код. Именно здесь живут предохранители:
MAX_CANCELLATIONS_PER_HOUR = 3 def execute_tool(tool_name: str, tool_input: dict): if tool_name == «cancel_subscription»: # предохранитель от массовых операций if cancellations_last_hour() >= MAX_CANCELLATIONS_PER_HOUR: alert_owner(«Агент превысил лимит отмен — остановлен») raise CircuitBreakerTripped # необратимое действие — только с подтверждением человека approval = request_human_approval( action=f»Отменить подписку {tool_input[‘subscription_id’]}», reason=tool_input[«reason»], timeout_hours=24, ) if not approval: return {«status»: «rejected_by_owner»} stripe.Subscription.cancel(tool_input[«subscription_id»]) return {«status»: «cancel
С таким кодом худший сценарий той ночи, три отменённые подписки, остановленный агент и уведомление владельцу. Не $38 MRR.
Третий слой, скоуп ключей. Stripe поддерживает restricted keys: агентному сервису выдаётся ключ read-only на подписки, операции записи ходят через отдельный сервис с подтверждением. Ключ, который физически не умеет отменять подписки, не отменит их никогда, ни из-за бага в cron-джобе, ни из-за галлюцинации, ни спросонья.
Отступление: «А вот модель Y так бы не сделала»
Теперь вернёмся к «модели Y доверять можно». Более аккуратная модель действительно реже инициирует катастрофу, частота инцидентов зависит от модели. Но их максимальный масштаб определяется контуром вокруг неё. В ту ночь масштаб был «весь MRR за 7 секунд», потому что контура не существовало. Поменяйте модель на самую осторожную на рынке, и вы уменьшите вероятность повторения, оставив цену повторения прежней.
Что общего
Сложим три истории рядом:
|
|
Deloitte |
Flock |
Stripe-агент |
|
Что сломалось |
выдуманные источники прошли все ревью |
опечатка в базе → ложные совпадения по всей стране |
код агента отменил все подписки за 7 секунд |
|
Нарушенный принцип |
groundedness: факты без привязки к источникам |
валидация на границах: ввод, матчинг, действие |
least privilege: права шире задачи |
|
Где жил «контроль» |
во внимательности ревьюеров |
в PDF с политиками |
в промпте и добрых намерениях |
|
Цена инцидента |
возврат денег, репутация, пресса |
час задержания невиновного, расследования |
минус весь MRR за одну ночь |
Закономерность видна невооружённым глазом: во всех трёх случаях вероятностному компоненту доверили работу детерминированного. И во всех трёх исправление не требует ни более умной модели, ни исследовательского прорыва, только скучной инженерии на границах. Каждое из решений в таблице пишется за дни. Каждый из инцидентов попал в мировую прессу.
Вернусь к трём идеям из начала статьи, теперь у каждой есть цена, измеренная чужими деньгами и нервами. Контроль в детерминированном коде, а не в промпте: у Deloitte и в кейсе со Stripe «контроль» был текстом, и текст не сработал. Каждый компонент на своём месте: у Flock вероятностное распознавание поставили туда, где системе нужна была детерминированная сверка. Строгость под цену ошибки: алерт, ведущий к задержанию человека, и алерт, двигающий тикет в очереди, не могут проходить через одинаковую проверку.
Отсюда простой тест, который я теперь применяю к любому требованию вида «система никогда не должна X»: где живёт это «никогда»? Если в промпте, в инструкции для персонала или в PDF с политиками, у вас пожелание. Если в JSONсхеме, в скоупе ключа, в ветке кода, которую невозможно обойти, у вас гарантия. Все три инцидента случились ровно в зазоре между первым и вторым.
Показательно, что и сами разработчики моделей это понимают. Посмотрите, во что превращается Anthropic: не в поставщика одной модели, а в целую экосистему для промышленного внедрения. Claude Code, Model Context Protocol как способ подключать инструменты, structured outputs и citations как встроенные механизмы гарантий, готовые паттерны агентных систем. Всё это устроено вокруг одной идеи: чтобы LLM можно было ответственно поставить в продакшен, вокруг неё нужна инженерная обвязка, и вендор эту обвязку выстраивает сознательно, а не оставляет каждую команду изобретать её заново.
Отсюда главный вывод для всех, кто внедряет LLM в промышленный проект. Ключевая инвестиция сейчас не в подписку на модель поумнее, а в архитектурную грамотность команды. Модель это только ядро. Работающий и безопасный продукт получается тогда, когда вокруг ядра выстроена правильная архитектура: валидация на границах, детерминированные гарантии вместо пожеланий в промпте, продуманные права и пороги под цену ошибки. Разбор чужих инцидентов, вроде трёх в этой статье, самый дешёвый способ этому научиться. Свой инцидент обходится куда дороже, и в комплекте к нему идут пресса и возврат денег заказчику.
Источник: habr.com
Оцените материал:
Похожие записи
Присоединяйтесь и подпишитесь на рассылку самых свежих новостей по Email
Получайте свежие новости и идеи на почту. Без спама — только самое интересное.
Нажимая «Подписаться», вы соглашаетесь с политикой конфиденциальности.
