Усечение результатов API: как не дать агентам вносить ошибочные изменения
Усечение результатов API может подтолкнуть агентов к опасным последующим изменениям. Проектируйте ответы инструментов так, чтобы они сообщали о неполных данных, предупреждениях, лимитах и области поиска.

Агенту не нужен вредоносный инструмент, чтобы внести опасное изменение. Достаточно инструмента, который молча возвращает лишь часть ответа. Дайте агенту результат поиска, выглядящий полным, и попросите удалить то, чего поиск не нашел. Часто он совершит вполне логичную ошибку, опираясь на ложные предпосылки.
Проблема решается не более длинной системной инструкцией с призывом быть осторожнее. Инструмент должен в стабильной машиночитаемой форме сообщать, завершил ли он запрошенную работу, что именно пропустил, почему это произошло и как вызывающая сторона может продолжить. Если инструмент не может этого сообщить, агент не должен принимать отсутствие объекта в результате за разрешение на масштабное изменение.
Неполные и пустые данные говорят о разном
Пустой результат означает, что инструмент не нашел совпадений в пределах фактически проверенной области. Полный пустой результат означает, что инструмент проверил всю запрошенную область и ничего не нашел. Это разные утверждения, но большинство контрактов API сводят их к одному и тому же [].
Из-за этого возникает конкретный ошибочный вывод:
- Агент запрашивает все сервисные учетные записи без текущего владельца.
- API возвращает пустой массив после сканирования первой страницы, достижения лимита результатов или исключения записей, которые нельзя прочитать с данным токеном.
- Агент заключает, что у каждой учетной записи есть владелец.
- Он меняет связанный контроль, отчет или задачу очистки на основании этого вывода.
Агенту не нужно неправильно понять английский. Инструмент дал ему ответ, форма которого подразумевала больше, чем было известно серверу.
Инструменту стоит различать как минимум четыре состояния. Полный запрос может вернуть элементы. Полный запрос может не вернуть ни одного элемента. Неполный запрос может вернуть часть элементов. Неполный запрос может не вернуть ничего. Если обращать внимание только на третье состояние, можно пропустить самый опасный случай: пустой ответ, который убеждает агента, что проблемы не существует.
Права доступа усугубляют ситуацию. Многие сервисы намеренно скрывают недоступные объекты, возвращая пустую или отфильтрованную коллекцию вместо ошибки доступа. Для интерфейса, ориентированного на человека, такое поведение может быть разумным. Для автономной задачи очистки это неприемлемое доказательство, если API точно не сообщает, какая область видимости была у вызывающей стороны.
Не используйте в качестве контракта фразу вроде Some results may be missing. Она не дает агенту надежной ветки для продолжения и не дает инженеру проверяемого условия. Поле complete с логическим значением выглядит скучно. В этом и смысл.
Успешный HTTP-ответ все равно может быть неполным
Коды состояния HTTP описывают обмен между клиентом и сервером. Сами по себе они не доказывают, что поиск, инвентаризация или экспорт охватили запрошенную область целиком.
RFC 9110 определяет семантику кодов состояния HTTP. 200 OK означает, что запрос успешно выполнен в соответствии с семантикой метода. Этот код не говорит, что поиск охватил каждую страницу, каждый раздел, каждую область прав доступа или каждую запись до истечения дедлайна. Команды часто приписывают 200 больше смысла, чем обещает протокол.
Рассмотрим такой ответ:
HTTP/1.1 200 OK
Content-Type: application/json
{
"items": [],
"next_cursor": null
}
Он выглядит окончательным. Но next_cursor: null говорит лишь о том, что у этого механизма пагинации нет следующей страницы. Это ничего не говорит о серверном лимите результатов, прерванной задаче поиска, неисправном источнике, исключенных политикой записях или API, которое молча ограничивает глубину поиска по времени.
Ответ становится пригодным доказательством, когда контракт определяет, что именно охватывает complete. Для поиска учетных записей это может означать все учетные записи, видимые вызывающему субъекту, в рамках указанного снимка. Для поиска кода это может означать все индексированные файлы в указанной ревизии с явным исключением игнорируемых файлов и сгенерированных результатов, отсутствующих в индексе. Область должна быть описана достаточно конкретно, чтобы вызывающая сторона могла решить, соответствует ли она предлагаемому действию.
Не пытайтесь решить проблему, возвращая 500 для каждого частичного ответа. Частичные результаты могут быть полезны. Их можно показать на панели мониторинга, обобщить с помощью агента или передать человеку для проверки. Ошибка возникает тогда, когда частичный результат выдают за авторитетный ответ на вопрос, требующий полноты.
Используйте ошибку, если операция обещает атомарный или полный ответ, но не может выполнить это обещание. Возвращайте успешный ответ с явным указанием на неполноту, если сами частичные данные имеют практическую ценность. Клиенту нужно детерминированное различие, а не спор о том, выглядел ли 200 слишком оптимистично.
Добавляйте метаданные полноты рядом с каждым результатом
Контракт результата должен представлять полноту как структурированные данные, независимо от того, полный список, короткий он или пустой. Не заставляйте вызывающие стороны выводить ее по количеству элементов, отсутствующему заголовку или предложению в поле message.
Для поиска по коллекции подойдет такая форма:
{
"items": [
{"id": "svc-184", "owner": null}
],
"complete": false,
"truncated": true,
"incomplete_reasons": [
{
"code": "RESULT_LIMIT_REACHED",
"message": "The query stopped after the configured result limit.",
"limit": 1000
}
],
"next_cursor": "eyJvZmZzZXQiOjEwMDB9",
"scope": {
"resource": "service_accounts",
"visibility": "resources readable by this credential",
"snapshot": "2025-03-08T14:20:11Z"
},
"warnings": []
}
Точные названия полей менее важны, чем их смысл и единообразие. complete служит полем, по которому принимается решение. truncated описывает один важный путь к неполноте, но не должен превращаться в универсальный флаг. Фильтрация по правам не равна усечению. Тайм-аут распределенного поиска не равен пагинации. Если перегрузить один флаг, вызывающие стороны потеряют причину, которая нужна для безопасного продолжения.
Храните warnings отдельно от incomplete_reasons. Предупреждение может сообщать о появлении устаревшего поля, нормализации значения или замене запрошенной сортировки на стандартную. Причина неполноты означает, что ответ не позволяет делать выводы о невозвращенной части запрошенной области. Это различие определяет, может ли агент продолжить работу.
Не полагайтесь только на простой флаг has_more. Обычно он отвечает на узкий вопрос о пагинации. Увидев has_more: false, агент может обоснованно решить, что коллекция закончилась, даже если серверный лимит или недоступный сегмент не позволили завершить сканирование. has_more можно сохранить, но он не должен один отвечать за полноту.
Для чтения отдельного ресурса применяйте тот же принцип. Если в ответе пропущены поля, нужно указать, не запросила ли их вызывающая сторона, нет ли у нее доступа, не отказал ли источник данных или действительно ли значение отсутствует. Отсутствие поля в JSON компактно, но неоднозначно.
Для пагинации нужна стабильная граница, а не страница побольше
Пагинация безопасна для агентов только тогда, когда API обеспечивает надежное продолжение и объясняет, какие изменения могут его нарушить. Увеличение лимита страницы лишь откладывает проблему.
Пагинация по смещению особенно часто приводит к ошибочным выводам. Агент читает записи с 0 по 99, удаляет или создает объект, а затем читает записи со 100 по 199. Если исходный порядок изменился, агент может пропустить запись или обработать одну дважды. Для справочного отчета это иногда допустимо. Для плана изменений последствия могут быть серьезными.
Пагинация по курсору обычно надежнее, потому что сервер может закодировать позицию в упорядоченном наборе результатов. Но и здесь нужен четкий контракт. Укажите, фиксирует ли курсор снимок, как долго он действителен и делает ли его недействительным изменение фильтров, порядка сортировки или прав доступа. Если срок действия курсора истек, не перезапускайте сканирование молча и не возвращайте объединенный ответ. Верните явное состояние неполноты или заставьте клиента начать заново.
Полезный ответ коллекции дает вызывающей стороне достаточно информации для осознанного завершения работы:
{
"items": ["item-001", "item-002"],
"complete": false,
"next_cursor": "cD0y",
"page": {
"returned": 2,
"requested_size": 2,
"ordering": "id ascending",
"snapshot": "search-7f9c"
},
"incomplete_reasons": [
{"code": "MORE_PAGES_AVAILABLE"}
]
}
Вызывающая сторона должна продолжать работу, пока не получит complete: true, а не просто пока не встретит короткую страницу. Короткие страницы возникают по разным причинам. Некоторые API возвращают их, когда раздел временно почти пуст, внутренний обработчик остановился раньше времени или сервис ограничивает размер ответа в байтах, а не количеством объектов.
Не просите языковую модель запоминать этот цикл из текста. Поместите логику пагинации в реализацию инструмента. Высокоуровневый инструмент search_all может собрать страницы, сохранить снимок, ограничить собственную работу и сообщить, достиг ли он конечного состояния. Если он достиг собственного лимита, он должен вернуть complete: false и указать, что причиной стал лимит на стороне клиента.
Последний случай часто упускают. Инженеры правильно добавляют метаданные в API, а затем создают оболочку агента с max_pages=10 и отбрасывают информацию о том, что сканирование остановилось на десятой странице. Неполнота возникла уже в оболочке. Именно контракт самого внешнего инструмента должен сообщать об этом.
Для лимитов времени, сбоев сегментов и прав доступа нужны отдельные причины
Поиск может завершить HTTP-запрос, хотя часть работы осталась невыполненной. Распределенные сервисы часто направляют запрос сразу в несколько индексов или тенантов. Если один источник не ответил вовремя, а сервис вернул совпадения из остальных, результат может быть полезным, но он неполный.
Представляйте причину кодом, по которому программа может выбрать ветку. Человеческий текст добавляйте рядом, но не вместо кода. Пусть коды будут немногочисленными, стабильными и задокументированными. Например:
MORE_PAGES_AVAILABLEозначает, что вызывающая сторона может запросить следующую страницу.RESULT_LIMIT_REACHEDозначает, что сервис применил лимит до исчерпания совпадений.TIME_BUDGET_EXCEEDEDозначает, что поиск остановился до завершения всей запланированной работы.SOURCE_UNAVAILABLEозначает, что определенный источник не ответил.VISIBILITY_RESTRICTEDозначает, что авторизация вызывающей стороны исключила часть запрошенной области.
Не скрывайте VISIBILITY_RESTRICTED за обычным успешным ответом. Команды безопасности иногда предпочитают неразличимые ответы, чтобы не раскрывать существование конкретного объекта. Это обоснованная забота. API может сообщить, что ограничения видимости не позволяют составить полную инвентаризацию, не называя скрытые объекты. Но он не должен позволять принять частичную инвентаризацию за исчерпывающую.
То же правило действует для ограничений частоты и квот. Если API успел прочитать только первую часть запроса до исчерпания бюджета, сообщите и возвращенные данные, и состояние бюджета. Повторная попытка может завершиться позже, но это новая попытка. Агент не должен объединять две попытки в утверждение о полноте, если API не дает стабильного снимка или задача не допускает изменений данных между попытками.
Дедлайн должен быть не только результатом, но и входным параметром. Когда агент запрашивает широкую инвентаризацию, позвольте ему задать лимит времени и получить сведения о выполненном объеме работы. Так компромисс становится видимым. Десятисекундный разведочный поиск может подойти перед проверкой человеком. Но это слабое доказательство для удаления каждого ресурса, которого поиск не увидел.
Отсутствие является слабым доказательством для разрушительных изменений
Агент может безопасно использовать частичные данные, чтобы подготовить отчет, найти кандидатов или попросить человека проверить небольшую область. Ему не следует использовать частичные данные, чтобы заключить, что ресурс не используется, не имеет владельца, дублируется или безопасен для удаления.
Разница в направлении вывода. Найденная запись с owner: null дает положительное свидетельство об этой записи, с учетом актуальности поля. Отсутствие учетных записей без владельца является универсальным утверждением обо всей области поиска. Для универсальных утверждений нужна полная проверка определенной области.
Такая ошибка часто маскируется под повышение эффективности. Команда дает агенту инструмент list_inactive_projects, а затем разрешает архивировать каждый возвращенный проект или, что еще хуже, каждый проект, отсутствующий во втором списке. У инструмента есть максимальное число результатов. Через несколько месяцев крупная организация превышает этот предел. В инструкции агента ничего не меняется, но смысл его работы превращается из «работать с инвентаризацией» в «работать с произвольным началом инвентаризации».
Проектируйте инструменты действий так, чтобы они требовали доказательства, а не принимали объяснение. Операция архивирования может требовать идентификаторы, выбранные предыдущей полной инвентаризацией, и токен снимка, связывающий выборку с чтением. Если инвентаризация была неполной, инструмент отклоняет операцию. Так проверка безопасности оказывается в месте, где модель не сможет обойти ее общими словами.
Для действий, которые не используют токены снимка, требуйте явную область и повторно проверяйте каждый объект во время выполнения. Это не доказывает полноту исходного поиска, но не позволяет одному устаревшему списку разрешить несвязанные изменения. Область действия должна быть достаточно узкой, чтобы проверяющий мог понять, какие объекты будут затронуты.
Популярная альтернатива, сказать агенту: «Никогда ничего не удаляй, если не уверен». Звучит разумно, но на практике не работает. Уверенность, это слово в инструкции. complete: false, это условие, которое может принудительно проверить инструмент.
Схемы инструментов должны заставлять агента учитывать неопределенность
Инструмент MCP или любая оболочка для агента должны возвращать типизированный конверт, а не привлекательный блок прозы. Модель может читать текст, но окружающему программному обеспечению нужны поля, которые можно проверить, записать в журнал, использовать для блокировки и протестировать.
Практический тип ответа может выглядеть так:
{
"status": "partial",
"data": {
"repositories": [
{"id": "repo-a", "default_branch": "main"}
]
},
"completeness": {
"complete": false,
"reasons": ["TIME_BUDGET_EXCEEDED"],
"continuation": {
"kind": "retry_with_deadline",
"minimum_seconds": 30
}
},
"warnings": [
{
"code": "STALE_INDEX",
"message": "Search index may lag the source repository."
}
]
}
Не используйте для такого ответа status: "success". Простые клиенты могут отбросить метаданные. partial сообщает вызывающей стороне, что она получила полезные данные с ограничением. Если протокол требует единого успешного статуса, сделайте complete обязательным и потребуйте от клиентов, способных выполнять действия, проверять его перед изменением данных.
Поле продолжения должно описывать реальный путь восстановления. next_cursor подходит для следующей страницы. retry_after, для ограничения частоты. narrow_query, для серверного лимита. Не предлагайте продолжение, которое просто повторяет тот же запрос в надежде, что окружающий мир изменился.
Инструкции для агента должны задавать небольшой строгий набор правил:
- Агент может считать пустую коллекцию доказательством отсутствия только при
complete: true. - Агент может использовать частичный ответ, чтобы предложить ограниченное исследование без изменений.
- Перед запросом одобрения действия, основанного на результате, агент должен показать
incomplete_reasons. - Агент не должен придумывать отсутствующий токен продолжения или утверждать, что повторная попытка завершилась успешно, если у него нет ее результата.
Эти правила короткие, потому что подробности содержатся в данных. Инструкция не способна восстановить сведения, которые инструмент решил не сообщать.
У предупреждений должны быть ответственные и путь к устранению
Предупреждения превращаются в фон, когда каждый ответ содержит расплывчатое предостережение. Делайте их конкретными, понятными по источнику и полезными для принятия решения. Предупреждение, которое никогда не меняет следующий выбор вызывающей стороны, обычно стоит превратить в документацию или убрать.
Например, STALE_INDEX должен указывать индексируемый источник и, если возможно, его наблюдаемую ревизию или время обновления. Тогда агент сможет проверить источник истины перед изменением кода. PARTIAL_FIELD_SET должен сообщать, какие поля сервер пропустил и может ли вызывающая сторона запросить их. DEFAULT_SCOPE_APPLIED должен описывать область, которую выбрал сервер, поскольку значения по умолчанию часто приводят к случайным масштабным действиям.
Не превращайте предупреждения в блокировки случайно. Вызывающей стороне нужно ясное правило серьезности. Метаданные полноты определяют, позволяет ли результат делать вывод обо всей области. Предупреждения относятся к уверенности, актуальности или интерпретации. Инструмент может вернуть complete: true с предупреждением об устаревших данных. Такой результат по-прежнему перечисляет все элементы индекса, но не подходит для изменения, требующего актуального состояния.
Добавляйте к предупреждениям стабильные коды и тестируйте потребителей по ним. Не ограничивайтесь тестами, которые проверяют только понятное сообщение. Текст меняется, когда автор улучшает формулировку, а правило принятия решения меняться не должно.
Заранее определите, кто отвечает за предупреждение после его появления. Если команда эксплуатации видит одно и то же предупреждение при каждом вызове в течение шести месяцев, она перестанет его читать. Устраните исходную причину, превратите предупреждение в жесткую ошибку, если это уместно, или удалите его, если оно не влияет на решение. Постоянные желтые сигналы приучают людей и агентов их игнорировать.
Тесты должны проверять опасный пустой ответ
Большинство наборов тестов проверяют обычную страницу результатов и ошибку сервера. Они пропускают ответ, который приводит к худшему выводу: items: [] вместе с указанием на неполноту.
Напишите контрактные тесты для каждого кода причины. Проверьте, что API возвращает метаданные для заполненных и пустых списков, SDK сохраняют их, а оболочка агента не сводит их к обычному тексту. Сбой на любом уровне может превратить честный ответ сервера в вводящий в заблуждение результат инструмента.
Добавьте в тестовые фикстуры такие случаи:
{
"case": "empty first page with more pages",
"response": {
"items": [],
"complete": false,
"truncated": false,
"incomplete_reasons": ["MORE_PAGES_AVAILABLE"],
"next_cursor": "cursor-2"
},
"expected_agent_decision": "continue_search"
}
Затем протестируйте запрос на изменение после такой фикстуры. Ожидаемым решением должно быть refuse_or_request_review, а не perform_cleanup. Сделайте правило видимым в названии теста. Иначе будущие сопровождающие сочтут ограничение чрезмерной защитой и уберут его, чтобы автоматизация выглядела удобнее.
Проверяйте и пагинацию во время изменений. Вставляйте, удаляйте и перемещайте записи между страницами. Делайте курсор недействительным. Имитируйте сбой одного сегмента после того, как другой уже вернул результаты. Убирайте одно разрешение в середине сканирования. Инструмент должен либо сохранять задокументированный снимок, либо сообщать, что не может гарантировать полноту. Статическая тестовая база не обнаружит ложные выводы, которые появляются в рабочей среде.
Здесь полезны тесты свойств. Создавайте коллекции, превышающие каждый настроенный лимит, меняйте размеры страниц и проверяйте одно правило: клиент может пометить коллекцию как полную только после учета каждого элемента в заявленном снимке. Для этого не нужна языковая модель. Это обычная корректность интерфейса.
При одобрении человеком нужно показывать недостающие доказательства
Контроль со стороны человека работает только тогда, когда на экране показано решение, которое предлагается принять. «Разрешить действие агента» не является одобрением. Это просьба принять непрозрачную цепочку предположений.
Когда инструмент сообщает о неполных данных, покажите предлагаемое действие, целевую область, причину неполноты доказательств и вариант восстановления. Полезный запрос может сообщить, что инвентаризация завершилась по тайм-ауту после возврата 842 ресурсов, и спросить, нужно ли повторить ее с более длинным дедлайном, ограничить действие возвращенными идентификаторами или отказаться от изменения. Тогда проверяющий сможет принять обоснованное решение.
Sallyport не дает учетным данным попасть в процесс агента, когда тот выполняет действия по HTTP и SSH, а журнал активности позволяет увидеть получившиеся вызовы. Такая изоляция и трассировка полезны, когда нужно восстановить ход ошибочного решения. Но они не превращают неоднозначный ответ API в доказательство, поэтому инструмент все равно должен передавать состояние полноты.
Чтобы не создавать усталость от подтверждений, запрашивайте их для действительно важных неоднозначных ситуаций. Инструмент должен сам обрабатывать обычное продолжение, например загрузку следующей задокументированной страницы, не прерывая человека снова и снова. Останавливать его нужно на границе политики: при истекшем снимке, ограниченной видимости, действии, основанном на отсутствии, или изменении, выходящем за пределы собранных доказательств.
Первая инженерная задача невелика: найдите все оболочки API, которые могут возвращать список, агрегат или результат поиска, и добавьте явное состояние полноты во внешний ответ каждой из них. Начните с пустых результатов и поисков с лимитами. Именно там уверенные агенты создают самые убедительные неправильные ответы.
Вопросы и ответы
Что такое неполный результат API?
Неполный результат охватывает лишь часть запрошенной области, например одну страницу записей, каталог одного репозитория, поиск, прерванный по тайм-ауту, или отфильтрированный запрос. Он становится опасным, когда инструмент показывает эту выборку в том же виде, что и полный ответ. Тогда агент принимает отсутствие данных за доказательство.
Означает ли пустой ответ API, что подходящих записей нет?
Нет. Пустой список означает лишь, что сервер вернул ноль элементов в пределах фактически проверенной области. Если эту область сузили пагинация, ограничение времени, права доступа или сбой одного из источников, инструмент должен сообщить об этом отдельно.
Должен ли агент действовать, если ответ инструмента неполный?
Самый безопасный вариант по умолчанию, остановить разрушительные или масштабные последующие действия, если полнота неизвестна. Агент может выполнить обратимое действие в узко заданных пределах, если контракт инструмента прямо это разрешает. Нельзя позволять модели выводить такое правило из обычного текста ответа.
Как API должен сообщать об усеченных результатах?
Используйте явные поля, например complete, truncated, warnings, next_cursor и массив машиночитаемых incomplete_reasons. Добавляйте их в каждую форму успешного ответа, включая пустые результаты. Предупреждение, спрятанное в текстовом описании, слишком легко пропустить и коду, и агенту.
Гарантирует ли пагинация, что агент увидел все записи?
Пагинация дает полный результат только тогда, когда клиент проходит по всем курсорам, пока API не сообщит об отсутствии следующей страницы. Большой размер страницы сокращает число вызовов, но не доказывает полноту. Истекший курсор, изменившиеся параметры запроса и нестабильный порядок сортировки по-прежнему могут сделать сканирование ненадежным.
Как инструменту обрабатывать тайм-ауты с частичными данными?
Запрос с ограничением по времени должен сообщать и о дедлайне, и о незавершенной работе. Вернуть найденные до дедлайна совпадения полезно, но называть их полным ответом неправильно. Тайм-аут нужно считать невыполненным предварительным условием для изменений, основанных на отсутствии данных.
Достаточно ли HTTP 200, чтобы считать поиск API завершенным?
Нет. HTTP 200 означает, что сервер успешно передал именно этот HTTP-ответ, а не то, что в нем содержатся все нужные вызывающей стороне результаты. Добавьте метаданные о полноте в тело ответа или задокументированный заголовок и используйте это значение одинаково для всех конечных точек.
Когда агенту безопасно вносить изменения после поиска?
Да, если вызывающая сторона может показать, что проверила правильную область и получила полный ответ в рамках стабильного снимка. Например, удаление устаревшей метки после чтения одной полной записи отличается от удаления всех якобы неиспользуемых учетных записей после ограниченного поиска. Действие должно соответствовать имеющимся доказательствам.
Устраняют ли повторные попытки неполные результаты API?
Повторные попытки устраняют временные ошибки соединения. Они не исправляют смысловую неполноту, вызванную пагинацией, лимитами запроса, фильтрацией по правам или досрочной остановкой сервера. Инструмент должен сообщить об этих условиях, а вызывающая сторона уже решит, нужно ли повторять запрос и как именно.
Что должно содержать подтверждение человека при неполных данных?
В подтверждении нужно показать предлагаемое действие, затронутую область и причину, по которой агент не смог получить полные данные. Тогда человек сможет выбрать более узкий запрос, предоставить недостающий доступ или одобрить исключение. Общая формулировка скрывает суть решения.