Читать 7 мин

Пагинация API для агентов ИИ: ограниченное обнаружение

Пагинация API для агентов ИИ требует бюджета страниц, непрозрачных курсоров, безопасных повторов и отчётов, в которых точно указано, что агент не проверил.

Пагинация API для агентов ИИ: ограниченное обнаружение

Агент, который вызывает конечную точку списка без бюджета страниц, не занимается обнаружением. Он запускает удалённую процедуру без понятного конца и надеется, что счёт, лимит запросов и набор результатов останутся приемлемыми. Противоположная ошибка не менее опасна: загрузить первую страницу, увидеть правдоподобный ответ и молча принять его за всю систему.

Пагинация определяет, что агент может утверждать честно. Возвращённая запись доказывает существование этой записи. Она не доказывает, что других записей нет. Отсутствующая запись тоже почти ничего не доказывает, если агент не может показать, какую область он проверил, на какой порядок опирался и почему сервер указал на завершение.

Я видел, как агенты превращали безобидный запрос вроде «найди просроченные токены доступа» в тысячи вызовов, потому что никто не сказал им, где заканчивается обнаружение. Я также видел, как инвентаризация одной страницы приводила к попытке очистить аккаунты со второй страницы. Решение не в более хитрой формулировке запроса. Агенту нужен ограниченный контракт обхода, а в итоговом отчёте должна быть видна его граница.

Для вызовов обнаружения нужен явный бюджет

Каждый вызов постраничного обнаружения должен иметь ограничения, которые агент не сможет незаметно расширить. До первого запроса задайте максимальное число страниц, максимальное число возвращённых записей, крайний срок и допустимый расход лимита запросов. Значения зависят от задачи, но сами ограничения нужны всегда.

Считайте обнаружение отдельным этапом, не смешивайте его с действиями. Во время обнаружения агент собирает идентификаторы и факты. Он не должен удалять, ротировать или изменять объекты только потому, что на одной странице обнаружилось что-то подозрительное. Когда доказательств достаточно, агент может представить план с заданной областью или начать отдельный этап, для которого получено разрешение.

Полезный контракт состоит из четырёх частей:

  • конечная точка и все фильтры, включая порядок сортировки, если API его поддерживает;
  • размер страницы и предел числа страниц или записей;
  • условие завершения, определённое API, например отсутствие следующего курсора;
  • условие досрочной остановки, например обнаружение объекта с заданным именем или исчерпание бюджета.

Не путайте ограничение числа записей с ограничением числа страниц. Если сервис разрешает получать по 100 записей на странице, а предел агента равен 500 записям, может хватить пяти вызовов. Если из-за фильтров или разрешений сервис уменьшит фактический размер страницы, для той же задачи понадобится больше вызовов. Хорошая реализация проверяет оба ограничения после каждого ответа.

Например, агент, которому поручено найти репозиторий с именем billing-service, может остановиться сразу после точного совпадения, если фильтр и порядок конечной точки делают такой вывод безопасным. Агент, которому нужно найти все репозитории без защиты веток, не может остановиться на первом совпадении. Для этой задачи нужна полная выборка или явное сообщение о частичном результате.

Слово «все» должно чего-то стоить. Агент может использовать его только после того, как обход достиг конечного условия сервера и не превысил бюджет страниц, записей, времени или ошибок. Если сработал хотя бы один лимит, в отчёте нужно написать «частичное сканирование» и назвать границу.

Размер страницы контролирует стоимость, но не полноту

Параметр limit, per_page или page_size сообщает сервису, сколько записей нужно попытаться вернуть в одном ответе. Он не сообщает, какую долю коллекции агент должен проверить. Максимальное значение сокращает число обращений, но каждый ответ может оказаться настолько тяжёлым, что вызовет тайм-аут, превысит бюджет контекста или скроет важные детали среди большого количества нерелевантных данных.

Начинайте с самой маленькой страницы, достаточной для принятия решения. Если агенту нужен один конкретный аккаунт, обычно лучше запросить 20 коротких записей, чем 1 000 полных объектов. Если нужно построить инвентаризацию, увеличивайте поддерживаемый размер страницы только после проверки, что конечная точка возвращает ограниченное и полезное представление.

Поля важны не меньше размера страницы. Многие API предлагают fields, include, expand или похожий механизм. На этапе обнаружения запрашивайте идентификаторы, имена, состояние, временные метки и одно свойство, от которого зависит решение. Полные сведения загружайте только для кандидатов, требующих проверки. Так уменьшается трафик и количество случайного текста, который модель может неправильно истолковать.

Если провайдер поддерживает пагинацию через курсор, запрос может выглядеть так:

GET /v1/projects?state=active\u0026limit=50\u0026sort=id HTTP/1.1
Authorization: Bearer injected-by-gateway
Accept: application/json

Ожидайте форму ответа, в которой записи отделены от состояния продолжения:

{
  "data": [
    {"id": "prj_104", "name": "billing-service", "state": "active"}
  ],
  "next_cursor": "eyJvcmRlciI6ImlkIiwicG9zIjoiMTA0In0"
}

Агент должен записать, что запросил 50 записей и получил одну. Нельзя делать вывод о завершении по короткому массиву. В этом примере полезный сигнал завершения только один: отсутствие next_cursor или задокументированное значение null.

Избегайте произвольного правила вроде «всегда использовать 100». Максимальный размер страницы различается у разных API, а некоторые сервисы учитывают вложенные объекты или байты ответа по отдельным лимитам. Запрашивайте задокументированный максимум только тогда, когда он нужен задаче. Для широких сканирований средний размер страницы часто даёт более удобные контрольные точки, повторные попытки и отчёты, которые легко проверить человеку.

Сохраняйте курсоры как непрозрачное состояние сервера

Курсор - это не offset с необычным названием. Его смысл определяет сервер, поэтому агент должен побайтно копировать курсор в следующий запрос. В нём могут быть зашифрованы позиция сортировки, идентификатор снимка, граница разрешений или подпись. То, что курсор похож на Base64, не даёт права декодировать, редактировать или создавать его.

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

request = { state: "active", limit: 50, sort: "id" }
seen_ids = set()
pages = 0

while pages < 10 and len(seen_ids) < 500:
    response = GET /v1/projects with request
    record response status, request, and response cursor

    for item in response.data:
        if item.id in seen_ids:
            report "duplicate record encountered" with item.id
            stop or apply the provider's documented recovery method
        seen_ids.add(item.id)

    pages += 1
    if response.next_cursor is absent:
        report "complete"
        break

    request.cursor = response.next_cursor
else:
    report "partial: traversal budget reached"

Проверка дубликатов - не украшение. Изменяемые коллекции могут сдвигаться во время обхода, а ошибочные реализации пагинации встречаются. Дубликат не всегда означает сбой сервиса, но агент должен перестать делать вид, что получил чистую полную выборку. Если провайдер предлагает токен снимка, параметр as_of или задокументированный режим согласованности, используйте его для задач, за которыми последует важное действие.

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

Перезапуск тоже может создать ложное ощущение полноты. Если записи изменились между первым проходом и перезапуском, объединённый список может содержать пропуски или дубликаты. Укажите факт перезапуска и условие возобновления, например created_at >= last_observed_timestamp. Если стабильного способа продолжить нет, сообщите, что коллекция изменилась во время обхода, и не используйте результат как список для удаления.

Пагинация через offset смещается при изменении коллекций

При пагинации через offset используется число вроде offset=200\u0026limit=50 или page=5\u0026per_page=50. Такой способ легко запрограммировать и объяснить, поэтому он по-прежнему распространён. Но он становится ненадёжным, когда во время обхода добавляются новые записи или исчезают старые.

Представим, что первая страница возвращает записи с 1 по 50 в порядке от новых к старым. До запроса второй страницы появляются десять новых записей. Теперь offset=50 начинается после вновь добавленных записей и пересекается с объектами, которые агент уже видел. Если записи исчезнут с первой страницы, тот же offset может пропустить объекты, сдвинувшиеся вверх. Устранить это одной дедупликацией идентификаторов нельзя: она обнаруживает повторы, но не пропуски.

Если API позволяет использовать стабильную сортировку, выберите детерминированный порядок с дополнительным ключом для разрешения совпадений. Одного created_at часто недостаточно, потому что у нескольких записей может быть одинаковая временная метка. Сортировка вроде created_at,id, если провайдер её документирует, позволяет записать верхнюю границу и аккуратнее продолжить обход. Если API даёт только offset без снимка и стабильного порядка, делайте осторожные выводы по нескольким страницам.

Для задачи, требующей полного ответа, используйте следующие подходы в порядке убывания надёжности:

  1. Запросите у сервиса снимок, задачу экспорта или курсор, для которого задокументирован стабильный вид данных.
  2. Ограничьте запрос неизменяемым временным диапазоном и используйте задокументированный стабильный порядок.
  3. Выполните второе сканирование, сравните идентификаторы и сообщите о расхождениях.
  4. Попросите человека утвердить более узкую и чётко определённую область вместо широкого изменения.

Не превращайте плохой интерфейс в разрушительный процесс. Offset можно использовать для выборки, поиска конкретного объекта или создания частичного инвентаря. Нельзя использовать нестабильное сканирование через offset как доказательство того, что найдены все подходящие учётные данные, проекты или пользователи.

Завершение должно следовать протоколу, а не догадке

Поместить доступ к API в хранилище
Храните bearer-, basic- и пользовательские учётные данные заголовков в хранилище, а не в инструкциях обхода.

Разные API размещают состояние пагинации в разных местах. В теле JSON могут быть next_cursor, has_more или URL следующей страницы. Другие API используют заголовок HTTP Link. RFC 8288 определяет Web Linking и параметр rel, который описывает отношения вроде next. Заголовок содержит отношение, но не гарантирует наличие знакомого поля курсора в теле ответа.

Агенту нужно правило завершения для конкретной конечной точки. Запишите его рядом с определением запроса. Например: «Завершить, когда next_cursor равен null». Или: «Завершить, когда нет отношения Link с rel="next"». Не пишите: «Завершить, когда вернулось меньше 100 записей». Это сокращение ломается на отфильтрованных страницах, при сокращении доступа, сервисных ограничениях и намеренно неравномерных страницах.

Типичный заголовок Link выглядит так:

Link: </v1/events?limit=100\u0026cursor=a6f3>; rel="next",
      </v1/events?limit=100\u0026cursor=first>; rel="first"

Агент должен выбирать только понятное ему отношение. Нельзя склеивать заголовок, считать ссылку first безопасной контрольной точкой для перезапуска или делать вывод, что отсутствие ссылки last означает отсутствие последней страницы. За контракт пагинации отвечает документация самого API, а RFC 8288 описывает только передачу отношений ссылок в HTTP-заголовках.

Некоторые API возвращают has_more: true вместе с пустой страницей. Это кажется нелепым, пока вы не столкнётесь с фильтром разрешений, одновременным удалением или отстающим индексом. Если API документирует такое поведение, продолжайте по токену, пока позволяет бюджет, и запишите пустую страницу. Если поведение не задокументировано, остановитесь и отметьте противоречивый ответ пагинации. Бесконечное продолжение из-за постоянного has_more - ошибка программы, а не настойчивость.

Отделяйте конечный ответ от успешного статуса HTTP. 200 OK означает, что конкретный запрос выполнен успешно. Он не означает, что коллекция закончилась. 404 может означать истёкший курсор, неверную конечную точку или исчезнувший ресурс. Сохраняйте статус, тело ответа и последний курсор в записи запуска, чтобы человек мог понять, что именно произошло.

Для частичного результата нужна формулировка границы

Агент должен описывать обнаружение так, как внимательный оператор написал бы заметку об инциденте: указать, что было запрошено, что наблюдалось и что не проверялось. В большинстве плохих отчётов не хватает именно последнего. В них перечислены находки, но не указан курсор, лимит или ошибка, из-за которых результаты неполны.

Используйте формат отчёта, с которым человек сможет работать, не восстанавливая сессию:

Scope: GET /v1/projects?state=active\u0026sort=id
Requested page size: 50
Pages fetched: 10
Records received: 487
Completion: partial
Stop reason: page budget reached
Last continuation cursor: eyJvcmRlciI6ImlkIiwicG9zIjoiNTg3In0
Observed finding: 12 projects matched the review rule
Uninspected scope: records after the last continuation cursor
Action taken: none

Последняя строка важна. В отчёте об обнаружении нужно сказать, изменилось ли что-нибудь. Человек, проверяющий отчёт, не должен угадывать, только ли агент перечислил объекты или ещё и действовал с ними.

Не показывайте чувствительный курсор в переписке, если провайдер рассматривает его как bearer-возможность или если его содержимое может раскрыть структуру аккаунта. Храните точный токен в защищённых метаданных запуска, а в отчёте для человека показывайте отпечаток или скрытый префикс. Агенту нужно достаточно состояния для продолжения и аудита обхода, но людям не нужны токены продолжения, разбросанные по тикетам и терминалам.

Здесь формулировки снижают риск. «Я не нашёл подходящих записей среди первых 500 проверенных» - точное утверждение. «Подходящих записей нет» верно только после полного обхода достаточно стабильного представления данных. Разница кажется придиркой, пока от неё не зависит решение об очистке или соблюдении требований.

Для лимитов запросов и повторов нужны отдельные правила остановки

Увидеть каждый запрос страницы
Журнал Activity записывает отдельные HTTP-вызовы, чтобы можно было проверить последовательность страниц и повторные попытки.

Пагинация усиливает ошибки работы с лимитами, потому что один запрос превращается в цикл. Получив 429 Too Many Requests, агент не должен забрасывать конечную точку повторными вызовами с тем же курсором. Учитывайте Retry-After, если сервер его передаёт, вычитайте ожидание из крайнего срока запуска и останавливайтесь после исчерпания бюджета повторов.

При временных сбоях, например тайм-ауте или ответе 5xx, повторите запрос той же страницы до перехода дальше. Если API поддерживает идентификатор идемпотентности или запроса для вызовов списка, используйте его по документации. Не переходите к следующему курсору после неопределённого ответа лишь потому, что запрос, возможно, успел выполниться. Так возникает незаметный пропуск.

Ограниченная политика повторов может выглядеть так:

  • повторить текущую страницу не более двух раз после временной транспортной ошибки или ошибки сервера;
  • при лимите запросов соблюдать Retry-After, если это позволяет оставшийся срок;
  • не повторять ошибки аутентификации и авторизации без изменения состояния авторизации;
  • остановиться при неверных данных пагинации, повторном курсоре или недокументированном ответе продолжения.

Повторный курсор требует особого внимания. Если третья страница возвращает тот же next_cursor, который агент отправил, продолжение может создать бесконечный цикл. Сравнивайте каждый новый курсор с отправленным и с набором предыдущих курсоров. Останавливайтесь при повторении, если только провайдер не документирует случай, в котором повтор ожидаем. Это бывает редко и требует отдельного правила для конкретного провайдера.

Агент должен сохранять достаточно метаданных ответа, чтобы разобраться с повтором, но не записывать секреты. Сохраняйте код статуса, путь запроса, выбранные несекретные заголовки, номер страницы, отпечаток курсора, временные метки и дайджест тела ответа, если это разрешает политика. Не помещайте в журналы заголовки авторизации, полные bearer-токены или URL с учётными данными только потому, что запрос завершился ошибкой.

Пример сбоя показывает, почему первая страница опасна

Представим, что агенту поручили отключить все неактивные интеграции, которые старше указанной даты. Конечная точка интеграций по умолчанию возвращает 25 объектов, сортирует их по времени последнего изменения и передаёт next_cursor только при наличии новых страниц. Агент загружает первую страницу, находит три неактивные интеграции, отключает их и сообщает, что очистил неактивные интеграции.

Этот отчёт ошибочен по двум причинам. Агент не проверил все интеграции и начал действовать во время обнаружения. Три найденных кандидата ничего не говорят о второй и последующих страницах. Более того, отключение меняет updated_at, поэтому коллекция может изменить порядок, если конечная точка использует сортировку по умолчанию. Агент сам сделал свой обход менее стабильным.

Безопаснее начать с явного фильтра и стабильной сортировки, если API их поддерживает:

GET /v1/integrations?status=inactive\u0026updated_before=2024-01-01\u0026limit=50\u0026sort=id HTTP/1.1

Агент собирает идентификаторы на всех страницах, не изменяя объекты. Он останавливается только после того, как сервер не передал следующий курсор или сработал бюджет обнаружения. Затем он сообщает либо полный набор кандидатов, либо частичный набор. Отдельный запрос действия может использовать собранные идентификаторы, желательно после того, как человек увидит количество и область.

Если конечная точка списка не поддерживает стабильный порядок, агент должен прямо об этом сказать. Он может собрать кандидатов, но не должен утверждать, что получил полный набор без гонок. Популярный совет «действуй сразу при обнаружении» кажется эффективным, потому что экономит второй проход. Для изменяемых списков он ошибочен: действие может изменить положение, соответствие условиям или разрешения объекта.

То же относится к находкам безопасности, аккаунтам пользователей, записям развёртываний и артефактам сборки. Сначала прочитайте и определите область, затем изменяйте. Исключения бывают при срочном сдерживании угрозы, например при отзыве конкретно названного скомпрометированного ключа, но это не задача постраничной очистки. Это адресное действие с известным идентификатором.

Храните учётные данные и наблюдаемость вне агента

Одобрить новый обход
Авторизация для каждой сессии распознаёт новый процесс агента до его первого постраничного HTTP-запроса.

Агенту не нужен секрет API только для пагинации. Компонент, выполняющий HTTP-запросы, может подставлять учётные данные, требовать подтверждение человека и записывать фактическую последовательность запросов. Так агент занимается построением и интерпретацией запроса, а не токенами, которые позволили бы ему выполнять неограниченные вызовы в других местах.

Для команд, использующих Sallyport, HTTP-вызовы могут проходить через его шлюз действий, а учётные данные оставаться в зашифрованном хранилище. Журнал Activity записывает отдельные вызовы. Такая запись помогает сопоставить заявленное агентом число страниц с реально выполненными запросами, но не заменяет бюджеты страниц в инструкциях агента.

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

Журнал сессии Sallyport и история действий для каждого вызова помогают отозвать доступ и провести проверку, если запуск агента пошёл не по плану. Но агент всё равно должен получить указание остановиться при неизвестной форме курсора, исчерпании бюджета или ответе API, противоречащем задокументированному контракту конечной точки. Записать плохой обход после события лучше, чем не иметь свидетельств, но это не отменяет лишние вызовы или неправильное действие.

Проверяйте обход на сложных тестовых данных пагинации

Проверка только успешного сценария скрывает важные дефекты. До того как доверить рабочий процесс агенту, протестируйте ответы с пустой первой страницей и курсором, короткой страницей с продолжением, дублированным объектом, повторным курсором, истёкшим курсором и ответом об ограничении скорости между двумя обычными страницами.

Ожидаемое поведение должно быть простым и конкретным. Агент продолжает после пустой страницы только тогда, когда задокументированное состояние продолжения это разрешает. Он удаляет дубликаты или останавливается при повторных идентификаторах в соответствии с требованиями задачи к согласованности. Он никогда не придумывает курсор после истечения срока и сообщает о частичном охвате, когда запуск завершается по защитному лимиту.

При проверке цикла вызовов инструмента используйте такую таблицу:

Тестовый сценарийОжидаемый результат
12 записей, следующего курсора нетЗавершить после одного запроса
12 записей, следующий курсор присутствуетПродолжить, несмотря на короткую страницу
Один и тот же курсор возвращён дваждыОстановиться и сообщить о цикле пагинации
429 с Retry-AfterЖдать только в пределах крайнего срока и повторить текущую страницу
Курсор отклонён как просроченныйПерезапустить только через задокументированную контрольную точку или сообщить о неполном результате

Проверяйте не только итоговый текст, но и исходные вызовы. Отполированный отчёт может скрыть пропущенный курсор или лишний запрос после условия остановки. Журнал выполнения должен показывать один исходный запрос, каждый запрос продолжения, все повторы того же курсора и отсутствие вызовов после конечного ответа.

Для исследовательской работы задайте небольшой бюджет по умолчанию, а для широкого перечисления требуйте явного изменения задачи. Одно это ограничение предотвращает две самые затратные ошибки: агент сканирует бесконечно или молча принимает первую страницу за весь ответ.

Вопросы и ответы

Что означает пагинация для агента ИИ, работающего с API?

Пагинация делит коллекцию на части по правилам протокола. Агент должен считать каждую часть неполным свидетельством, а не всей коллекцией, пока не достигнет задокументированного конечного курсора или другого явного условия завершения.

Сколько страниц API должен загружать автономный агент?

Это зависит от размера ответа конечной точки, ограничений скорости и действия, которое собирается выполнить агент. Для обнаружения начните с намеренно небольшого бюджета страниц и увеличивайте его только при необходимости. Нельзя позволять агенту решать, что каждый список нужно просматривать без ограничений.

Должен ли агент разбирать или изменять курсор API?

Курсор - это непрозрачный токен продолжения, который выдаёт сервер. Сохраняйте его и передавайте обратно точно в полученном виде, включая кодировку и регистр. Разбор, изменение или самостоятельное создание курсора может привести к пропуску записей или недействительному запросу.

Означает ли короткая страница API, что результатов больше нет?

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

Лучше ли всегда запрашивать максимально возможный размер страницы?

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

Что должен сообщить агент после частичного постраничного поиска?

В отчёте нужно указать конечную точку, фильтр, запрошенный размер страницы, число загруженных страниц, число возвращённых записей, конечный статус и курсор или границу страницы, на которых запуск остановился. Если запуск завершился раньше времени, назовите результат частичным и не используйте слова вроде «все» или «ни одного».

Что делать агенту, если курсор истёк?

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

Чем отличаются пагинация через курсор и пагинация через offset?

При пагинации через offset запрашивается числовая позиция, например offset=200, а при пагинации через курсор используется выданный сервером токен, связанный с текущим обходом. Offset проще понять, но он смещается при добавлении и удалении записей. Курсоры обычно лучше подходят для изменяющихся коллекций, если провайдер реализовал их корректно.

Можно ли безопасно повторять постраничные GET-запросы?

GET-запрос можно безопасно повторить с точки зрения HTTP, но содержимое списка между страницами всё равно может измениться. Агент должен записывать запрос и наблюдаемые границы, использовать снимок сервера, если он доступен, и не принимать разрушительных решений по результатам сканирования, которое нельзя назвать полным.

Может ли API-шлюз сам обеспечить безопасность пагинации?

Шлюз может выполнять фактический HTTP-вызов и хранить учётные данные отдельно от агента, но он не способен определить, нужно ли бизнес-задаче две страницы или двести. Храните секреты и подтверждения на шлюзе, а бюджеты страниц, условия остановки и честную отчётность задавайте в инструкциях агента.

Sallyport

Sallyport выполняет API-вызовы и SSH-команды за вашего ИИ-агента. Ключи остаются в локальном хранилище на вашем Mac; вы подтверждаете каждый запуск, и каждое действие попадает в запечатанный журнал.

© 2026 Sallyport · Открытый код по лицензии Apache-2.0 · Oleg Sotnikov