TL;DR
Исследование показывает: LLM-агент почти не проверяет, к какой задаче относится результат инструмента. Если в контекст агента попал реальный, неизменённый ответ инструмента от другой задачи, от устаревшего состояния или повторно доставленный из очереди, модель обычно использует его как факт. Ответ из кэша, очереди или восстановления после сбоя читается как «свежий» и «мой».
Боль выглядит так. Агент проверил статус заказа №1837, потом получил независимую задачу «отмени заказ №2941». В контекст попал старый результат по №1837, и агент отменяет не тот заказ. Никто ничего не взламывал и не подделывал, ответ инструмента был правдивым. Причина проста: у модели нет «паспорта» результата, она видит только текст. Текст выглядит правдоподобно, значит, модель продолжает работу.
Что помогает. Явные пометки свежести (возраст результата, номер версии состояния) помогают частично. Полностью ошибки убирает проверка валидности в самой среде выполнения: она до действия сверяет, что результат относится к текущей задаче и состоянию. Это уже код, а не промпт. Выбор модели влияет на уязвимость сильно, выбор фреймворка почти не влияет.
Схема цепочки отказа
ШАГ 1: Доставка → реальный результат от другой задачи попал в контекст агента
(через кэш, очередь или восстановление после сбоя)
ШАГ 2: Генерация действия → модель принимает его как доказательство и формирует
неверный вызов инструмента ← ЗДЕСЬ ломается почти всё
ШАГ 3: Приёмка → слой исполнения пропускает действие (нет проверки)
ШАГ 4: Итог → в системе остаётся неверное состояние (не тот заказ отменён, письмо не туда)
Защита: проверка «результат ещё валиден для ЭТОЙ задачи?» ДО шага 3
Четыре вида нарушений, которые проверяли:
T01 Чужой объект — результат от задачи А применён в задаче Б
T02 Устаревший — результат был верен, но контекст изменился (права, статус, политика)
T03 Дубль — уже выполненное действие выполняется повторно
T04 Не по порядку — результаты применены в порядке, нарушающем цепочку состояний
Пример применения
Задача: Вы продавец на Ozon и ведёте поддержку через автоматизацию в n8n. Агент по очереди обрабатывает обращения: «проверь статус», «отмени», «верни деньги». В цепочке есть ретраи и очередь. После сбоя сценарий перезапускается и подсовывает агенту старый ответ API по заказу из прошлого обращения. Нужно, чтобы агент не совершал необратимое действие по чужому результату.
Промпт (системная инструкция агенту):
Ты агент поддержки интернет-магазина. Текущая задача: отменить заказ №2941 по
запросу клиента Ивана Петрова.
Правила работы с результатами инструментов:
1. Каждый результат инструмента имеет паспорт: task_id, order_id, state_version,
result_age. Результат без паспорта считай непроверенным.
2. Перед любым необратимым действием (отмена, возврат, удаление) выпиши в ответе:
- номер заказа из ТЕКУЩЕЙ задачи: №2941
- номер заказа из результата, на который опираешься
- возраст результата и версию состояния
3. Если номера не совпадают, паспорт отсутствует или результат старше 5 минут, НЕ
выполняй действие. Вызови инструмент get_order_status заново и опирайся
только на свежий ответ.
4. Если результат пришёл «из ниоткуда» (ты его не запрашивал в этой задаче),
считай это сигналом тревоги и запроси данные заново.
Результат: Перед отменой агент выведет короткую сверку: номер заказа из задачи, номер из результата, возраст и версию. Если данные расходятся, он остановится и заново запросит статус. Гарантии это не даёт. Статья проверяла только показ полей свежести в самих результатах, а не такую текстовую инструкцию. Но формулировка заставляет модель явно сопоставить два идентификатора, а не молча продолжить.
Почему это работает (и почему не работает «в лоб»)
Слабость LLM. Модель читает контекст как единый текст. Если в нём есть правдоподобный результат инструмента, она по умолчанию считает его частью текущей работы. Откуда он взялся, модель не видит, потому что у текста нет «паспорта». Исследование показывает: почти все ошибки возникают именно на шаге, где модель решает, годится ли этот результат для её задачи.
Что модель умеет хорошо. Она сверяет явные поля. Когда в результате есть возраст и версия состояния, она начинает замечать устаревшее. В статье при сильно устаревшем результате доля ошибок упала с 100% до 60%. Это улучшение, но не защита.
Как применить. Идея в том, чтобы превратить неявную проверку «моё ли это?» в явное сравнение двух значений. Рычаги: - Паспорт результата (ID задачи, ID объекта, версия, возраст): чем больше полей, тем больше за что зацепиться модели. - Порог свежести (в примере 5 минут): подстройте под скорость изменения ваших данных. - Список «необратимых» действий: чем короче список, тем реже агент тормозит на безопасных операциях. - Повторный запрос вместо доверия: для критичных действий пусть агент всегда запрашивает данные заново.
Главный урок статьи: на модель в этом месте полагаться нельзя. Надёжная защита стоит вне модели: среда выполнения должна проверять, что результат принадлежит этой задаче.
Шаблон промпта
Это моя сборка на основе проверенного в статье (явные поля свежести помогают, но не гарантируют). Саму формулировку инструкции авторы не тестировали.
Ты агент, который выполняет {тип_задач} через инструменты.
Текущая задача: {описание_задачи}. Объект задачи: {id_объекта}.
Правила работы с результатами инструментов:
1. У каждого результата ожидается паспорт: {поля_паспорта}.
Результат без паспорта считай непроверенным.
2. Перед любым необратимым действием ({список_необратимых_действий})
выпиши в ответе:
- объект текущей задачи
- объект из результата, на который опираешься
- возраст и версию результата
3. Остановись и запроси данные заново, если:
- объекты не совпадают
- паспорта нет
- результат старше {порог_свежести}
- ты не запрашивал этот результат в текущей задаче
4. Уже выполненное необратимое действие повторно не выполняй.
Сначала проверь текущее состояние объекта.
Что подставлять:
- {тип_задач}: поддержка, финансы, почта, CRM, облачные файлы.
- {поля_паспорта}: ID задачи, ID объекта, версия состояния, время получения.
- {список_необратимых_действий}: отмена, возврат, удаление, отправка письма, выдача прав.
- {порог_свежести}: время, за которое ваши данные могут измениться.
Паспорт должен добавлять ваша автоматизация, например поле в узле n8n или в ответе инструмента. Сама модель его не создаст. Если такой возможности нет, оставьте пункт 2 и пункт 3 (кроме условий про паспорт).
Ограничения
⚠️ Промпт не заменяет защиту в среде выполнения: Полностью ошибки в тестах убрала только проверка валидности в самой среде. Инструкции модели и пометки свежести снижают риск, но не закрывают его.
⚠️ Явные пометки не гарантируют: Для свежих и среднего возраста результатов ошибки оставались на максимуме независимо от того, были ли поля свежести видны. Помогало только при сильно устаревших данных, и то частично.
⚠️ Устойчивость сильно зависит от модели: Одни модели ошибались почти всегда, другие почти никогда. Если вы выбираете модель для агента с необратимыми действиями, проверьте её на своём сценарии с подсунутыми результатами.
⚠️ «Хорошая» защита от одного вида подмены не защищает от других: Одна из моделей отвергала результаты от другого объекта, но принимала устаревшие, повторные и пришедшие не по порядку. Сверка ID не спасает от остальных видов подмены.
⚠️ Тестовые условия: Всё проверено в песочнице, по 10 запусков на конфигурацию. Часть результатов в предоставленном тексте оборвана (изменения среды, сравнение с prompt injection, детали защиты). Эти выводы я здесь не пересказываю.
⚠️ Читателю без кода часть вывода недоступна: Сама защита (привязка результата к контексту) строится в коде агентной системы. В чате и в инструкции агенту доступна только «страховочная» часть.
Как исследовали
Исследователи придумали атаку, не требующую ничего взламывать: берём настоящий неизменённый результат инструмента из одной задачи и в нужный момент подсовываем его другой. Для этого собрали ReplayBench: 19 сценариев (почта, календарь, платежи, облачные файлы, управление доступом, инциденты), четыре вида нарушений и три способа доставки (хранилище событий, очередь, кэш сессии). Агенту никто не говорил делать ошибку, он просто продолжал работу. Результат проверял независимый скрипт, который смотрел на итоговое состояние песочницы.
Проверили восемь моделей на трёх агентных фреймворках, обычно по 10 запусков на конфигурацию. При подмене результата от другой задачи в среднем 65% запусков заканчивались неверным итоговым состоянием, а 38% конфигураций (64 из 168) ломались во всех запусках. Если брать весь набор нарушений, доля ошибок доходила до 99%.
Дальше идёт самое интересное. 74% разброса объясняется выбором модели: самая уязвимая ошибалась почти всегда (~97%), самая устойчивая почти никогда (~2%). Фреймворк объясняет меньше 1%, то есть смена LangGraph на LlamaIndex ничего не меняет. Разбор по стадиям показал, что результат доходит до агента почти всегда, а ошибка возникает именно в момент, когда модель решает, использовать его или нет.
Удивили три вещи. Во-первых, схожесть объектов не влияла: путаница не зависела от того, похожи ли идентификаторы. Во-вторых, модель, которая отвергала подмену «чужого объекта», на других нарушениях ломалась на 100%: она умеет сверять ID, но не проверяет актуальность и порядок. В-третьих, пометки свежести помогают лишь частично.
Практический вывод: не надейтесь, что модель сама заметит подмену. Закладывайте проверку вне модели, а модели оставьте роль вторичной страховки.
Адаптации и экстраполяции
🔧 Техника: вынести сверку в отдельный шаг → дополнительный барьер. Это экстраполяция, в статье не проверялась. Вместо одной инструкции дайте агенту два прохода.
Шаг 1 (проверка): Перед действием заполни таблицу:
| что требует задача | что утверждает результат | совпадает? |
Не вызывай инструменты изменения, пока все строки не «да».
Шаг 2 (действие): Выполняй только если проверка пройдена.
Зачем: отдельный видимый шаг сверки труднее пропустить, чем пункт в общем списке правил. Это гипотеза для вашей задачи, проверьте её на нескольких подмененных результатах.
🔧 Техника: ручной «красный тест» своей автоматизации → найти дыры до продакшена.
Я подсуну в твою задачу результат инструмента, который относится к другому
объекту. Если ты увидишь расхождение, остановись и объясни, на каком поле
заметил. Если не увидишь, продолжай как обычно.
Идея взята из дизайна исследования: подмена реального результата и проверка итогового состояния. В автоматизации сами подсуньте старый ответ API и посмотрите, что сделает агент.
Ресурсы
- Работа: TRAP: Tool-result Replay Attack for Probing Misbinding in LLM Agents
- Авторы: Yutong Zhi, Jie Zhang, Xiuqi Lu, Jiayuan Xu, Jing Shao
- Организации: Tsinghua University, Shanghai AI Laboratory
- Код и бенчмарк: https://github.com/Phitooo/Replaybench
- Упомянутые инциденты: отчёт LangGraph.js о загрязнении состояния между потоками; CVE-2026-25536 в MCP TypeScript SDK (ответы уходят не тому клиенту)
