Я проектирую системы на LLM, и после каждого громкого ИИ-инцидента мне прилетает одна и та же ссылка с одним и тем же вопросом: «а у нас такое может случиться?» Чтобы отвечать не на глазок, я завёл привычку разбирать каждый такой инцидент до конкретной технической причины. За последние месяцы таких разборов набралось три, и они удивили меня не сходством, а различием. Дыры, в совершенно разных местах: у одного проекта не проверялись источники, у другого, данные на границах системы, у третьего, права автономного агента. А вот причина одна: системы вокруг LLM строили люди, которые умеют писать промпты, но не умеют проектировать архитектуру.

Судите сами. Deloitte Australia возвращает правительству деньги за отчёт, в котором ИИ выдумал источники (Источник). В Миннесоте полиция четырьмя машинами блокирует на парковке журналиста, потому что сеть ИИ-камер несколько дней вела его как угонщика, из-за опечатки, сделанной за две тысячи миль от него (Источник). А основатель инди-SaaS просыпается и обнаруживает, что написанный моделью код ночью отменил все подписки его клиентов: месячная регулярная выручка обрушилась до $38, и то лишь потому, что уцелели две тестовые подписки (Источник).

Из этих разборов у меня сложились три идеи, через которые я теперь смотрю на любую LLM-систему. Первая: контроль должен жить в детерминированном коде, а не в промпте, промпт снижает вероятность ошибки, но не даёт гарантий. Вторая: у каждой техники есть область применимости, и вероятностные компоненты нельзя ставить на места, где нужна детерминированность. Третья: строгость контролей калибруется под цену ошибки, высокорисковые сценарии требуют других порогов и других процедур, чем рутинные. Три инцидента ниже, это три нарушения этих идей в дикой природе. Разберём каждый: что произошло, где сломалось и как закрывается. Решения покажу с кодом на Claude API, во-первых, потому, что чинить абстрактными рассуждениями скучно, а во-вторых, потому, что сам строю на Claude: из всех моделей, с которыми я работал в проде, у него самый продуманный инструментарий именно для архитектурных гарантий, citations, structured outputs, tool use спроектированы так, что правильную систему построить проще, чем неправильную.

Кейс 1. Deloitte: отчёт, который все проверили и никто не прочитал

Хроника: сноски, которых не было

Deloitte Australia готовила для правительства официальный отчёт и использовала при этом ИИ. Документ прошёл внутренние ревью, не одно, и был опубликован. А потом внешний исследователь, читавший его по своей академической надобности, начал проверять сноски. Часть цитат была приписана не тем авторам. Часть источников не существовала вовсе. Дальше всё развивалось по законам жанра: публикации в СМИ, вопросы от законодателей, исправленная версия документа и возврат части оплаты по контракту. Для фирмы, которая продаёт в том числе аудит и проверку чужих отчётов, сюжет вышел особенно неловкий.

Где сломалось: беглость перестала означать достоверность

Самое интересное в этой истории, не галлюцинации. То, что языковые модели выдумывают источники, известно каждому, кто провёл с ними больше недели. Интересно, что документ прошёл несколько слоёв проверки, и ни один из проверяющих не споткнулся. Ревьюеры читали текст и оценивали его связность, логику, убедительность. Но LLM генерирует именно связный, логичный и убедительный текст, в том числе тогда, когда врёт. Беглость изложения десятилетиями работала прокси-сигналом качества, и процесс ревью молча на неё полагался. С приходом генеративных моделей этот сигнал умер, а процессы этого не заметили.

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

Схема 1. Гарантии даёт код-валидатор, а не LLM; человек разбирает только непрошедшее
Схема 1. Гарантии даёт код-валидатор, а не LLM; человек разбирает только непрошедшее

Как закрыть: 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 есть официальная политика: алерт — это повод для проверки, офицер обязан вручную сверить номер до каких-либо действий. Политика существовала. На практике алерт стал основанием для многодневной слежки со «стингом» на парковке. Один из офицеров честно сказал журналисту: считайте, повезло, что это Плимут, в Миннеаполисе вас бы положили на землю под прицелом.

Схема 2, Failure Cascade (кейс Flock): каскад отказа от опечатки до задержания.
Схема 2, Failure Cascade (кейс 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
Схема 3. Тот же процесс с барьерами: alert создаёт код, действие санкционирует человек.
Схема 3. Тот же процесс с барьерами: alert создаёт код, действие санкционирует человек.

Сравните с оригиналом. «Офицер должен проверить вручную» у 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, в три часа ночи. Если у агента и были ограничения, они жили в промпте, в слое пожеланий. Принцип минимальных привилегий требует противоположного: то, что агент не должен делать, должно быть тем, что он не может сделать. На уровне выданных инструментов и ключей, а не инструкций.

Схема 4. Три слоя авторизации агента; любой может остановить вызов, ограничивая масштаб.
Схема 4. Три слоя авторизации агента; любой может остановить вызов, ограничивая масштаб.

Как закрыть: три слоя между агентом и деньгами

На 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 в промышленный проект. Ключевая инвестиция сейчас не в подписку на модель поумнее, а в архитектурную грамотность команды. Модель это только ядро. Работающий и безопасный продукт получается тогда, когда вокруг ядра выстроена правильная архитектура: валидация на границах, детерминированные гарантии вместо пожеланий в промпте, продуманные права и пороги под цену ошибки. Разбор чужих инцидентов, вроде трёх в этой статье, самый дешёвый способ этому научиться. Свой инцидент обходится куда дороже, и в комплекте к нему идут пресса и возврат денег заказчику.