Я проектирую системы на LLM, и после каждого громкого ИИ-инцидента мне прилетает одна и та же ссылка с одним и тем же вопросом: «а у нас такое может случиться?» Чтобы отвечать не на глазок, я завёл привычку разбирать каждый такой инцидент до конкретной технической причины. За последние месяцы таких разборов набралось три, и они удивили меня не сходством, а различием. Дыры, в совершенно разных местах: у одного проекта не проверялись источники, у другого, данные на границах системы, у третьего, права автономного агента. А вот причина одна: системы вокруг LLM строили люди, которые умеют писать промпты, но не умеют проектировать архитектуру.
Судите сами. Deloitte Australia возвращает правительству деньги за отчёт, в котором ИИ выдумал источники (Источник). В Миннесоте полиция четырьмя машинами блокирует на парковке журналиста, потому что сеть ИИ-камер несколько дней вела его как угонщика, из-за опечатки, сделанной за две тысячи миль от него (Источник). А основатель инди-SaaS просыпается и обнаруживает, что написанный моделью код ночью отменил все подписки его клиентов: месячная регулярная выручка обрушилась до $38, и то лишь потому, что уцелели две тестовые подписки (Источник).
Из этих разборов у меня сложились три идеи, через которые я теперь смотрю на любую LLM-систему. Первая: контроль должен жить в детерминированном коде, а не в промпте, промпт снижает вероятность ошибки, но не даёт гарантий. Вторая: у каждой техники есть область применимости, и вероятностные компоненты нельзя ставить на места, где нужна детерминированность. Третья: строгость контролей калибруется под цену ошибки, высокорисковые сценарии требуют других порогов и других процедур, чем рутинные. Три инцидента ниже, это три нарушения этих идей в дикой природе. Разберём каждый: что произошло, где сломалось и как закрывается. Решения покажу с кодом на Claude API, во-первых, потому, что чинить абстрактными рассуждениями скучно, а во-вторых, потому, что сам строю на Claude: из всех моделей, с которыми я работал в проде, у него самый продуманный инструментарий именно для архитектурных гарантий, citations, structured outputs, tool use спроектированы так, что правильную систему построить проще, чем неправильную.
Кейс 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 эти мелкие цифры на 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 в проезжающих машинах тоже не считывало. Результат: любая машина пресс-парка 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 в промышленный проект. Ключевая инвестиция сейчас не в подписку на модель поумнее, а в архитектурную грамотность команды. Модель это только ядро. Работающий и безопасный продукт получается тогда, когда вокруг ядра выстроена правильная архитектура: валидация на границах, детерминированные гарантии вместо пожеланий в промпте, продуманные права и пороги под цену ошибки. Разбор чужих инцидентов, вроде трёх в этой статье, самый дешёвый способ этому научиться. Свой инцидент обходится куда дороже, и в комплекте к нему идут пресса и возврат денег заказчику.
Автор: Aiarchpro


