Читать 6 мин

Асинхронные задания API: отслеживаемые рабочие процессы агентов

Асинхронным заданиям API нужны постоянное состояние, идемпотентность, опрос, подтверждение отмены, проверка результата и записи аудита для AI-агентов.

Асинхронные задания API: отслеживаемые рабочие процессы агентов

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

Проблема не в самом HTTP-запросе. Сложность в том, чтобы сохранить намерение после перезапуска агента, тайм-аута сети, сбоя провайдера и команды человека «остановить», отданной уже после начала удалённой работы. Основой должна стать постоянная локальная запись задания. Каждый внешний вызов должен оставлять ответ, к которому можно вернуться позже: что мы запросили, какому удалённому заданию это соответствует, какое состояние увидели и что сделали затем?

Запрос на создание должен установить владельца

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

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

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

Минимальная запись может выглядеть так:

{
  "operation_id": "op_01J7Q5X4D4PA3D",
  "request_fingerprint": "sha256:4f8b...",
  "idempotency_token": "idem_5b5c76c7",
  "remote_job_id": null,
  "state": "create_pending",
  "created_at": "2025-03-08T14:22:11Z",
  "create_deadline": "2025-03-08T14:24:11Z",
  "result_deadline": "2025-03-08T15:22:11Z"
}

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

Ответ на создание может выглядеть так:

HTTP/1.1 202 Accepted
Location: /v1/jobs/job_7ad2
Content-Type: application/json

{
  "job_id": "job_7ad2",
  "state": "queued",
  "status_url": "/v1/jobs/job_7ad2"
}

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

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

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

Токен нужно передавать в запросе на создание согласно контракту провайдера. Одни API принимают заголовок Idempotency-Key, другие требуют поле запроса. Используйте документированный вариант и создавайте токен с достаточной энтропией, чтобы независимые операции не столкнулись. Сохраняйте один и тот же токен, пока исходная операция не будет разрешена.

POST /v1/reports HTTP/1.1
Content-Type: application/json
Idempotency-Key: idem_5b5c76c7
X-Trace-ID: tr_0830d3

{
  "account": "acct_218",
  "range": {"start": "2025-02-01", "end": "2025-02-28"},
  "format": "csv"
}

Безопасная последовательность повторной отправки короткая:

  1. Создайте токен и сохраните запись операции.
  2. Отправьте запрос на создание с этим токеном.
  3. Если ответ потерян или клиент получил тайм-аут, повторите тот же запрос с тем же токеном.
  4. Если сервер вернул исходное задание, сохраните его ID и продолжайте работу.
  5. Если нужны другие входные данные, по возможности закройте или отмените старую операцию, затем создайте новую запись и новый токен.

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

Стандарт HTTP проводит нужную границу. RFC 9110 определяет идемпотентные методы как методы, при которых предполагаемый эффект повторения одинаковых запросов совпадает с эффектом одного запроса. POST по умолчанию не идемпотентен. Провайдер может добавить идемпотентность конечной точке POST, но клиент должен считать это отдельным прикладным контрактом, а не правилом HTTP.

Популярный плохой совет звучит так: «Повторяйте POST только один раз». Число повторов здесь не главное. Даже одна повторная отправка может провести платёж, создать окружение или запустить дорогую пакетную обработку. Повторяйте запрос на создание, пока это допускают крайний срок и рекомендации провайдера, но только с токеном, который позволяет серверу распознать исходную операцию.

После тайм-аута состояние задания неизвестно

Тайм-аут при создании не означает, что сервис отклонил запрос. Он означает, что агент не получил окончательный ответ до собственного крайнего срока. Сервер мог принять запрос, всё ещё обрабатывать его или вообще не получить.

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

Храните явное состояние create_pending. При неоднозначном сбое запишите класс ошибки, временную метку и число попыток, но не удаляйте операцию. Затем используйте путь сверки, предусмотренный API. Провайдеры реализуют его по-разному:

  • Повторная отправка с тем же токеном идемпотентности может вернуть исходный ответ о принятии.
  • Конечная точка списка или поиска может фильтровать записи по ссылке на запрос клиента.
  • Проверка состояния может принимать operation ID, заданный клиентом.
  • Провайдер может документировать запрос последних операций по идентификатору запроса.

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

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

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

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

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

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

Учитывайте Retry-After, если API его передаёт. Если API ничего не сообщает, используйте ограниченную экспоненциальную задержку со случайным разбросом. Точный предел зависит от того, как быстро бизнесу нужен ответ, и от ограничений провайдера, но интервалы должны предотвращать синхронные всплески.

попытка 1: ждать случайный интервал около 2 секунд
попытка 2: ждать случайный интервал около 4 секунд
попытка 3: ждать случайный интервал около 8 секунд
следующие попытки: увеличивать интервал до заданного предела

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

Ответ о состоянии должен обновлять только наблюдаемые факты. Например:

{
  "job_id": "job_7ad2",
  "state": "running",
  "updated_at": "2025-03-08T14:26:40Z",
  "progress": {"completed": 146, "total": 500}
}

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

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

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

Переходы состояний должны отсекать желаемое за действительное

Контролируйте каждый важный вызов
Настройте подтверждение для каждого ключа и требуйте один клик или Touch ID при каждом важном вызове API.

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

Практическая локальная модель:

create_pending -> accepted -> observing -> result_collecting -> succeeded
create_pending -> create_unknown -> reconciliation
accepted or observing -> cancel_requested -> cancelling -> cancelled
accepted or observing -> failed
observing -> result_timed_out

Стрелки здесь, это правила, а не иллюстрация для документации. Рабочий процесс должен отклонять переход без доказательства. Нельзя отметить succeeded, увидев прогресс 100. Нельзя отметить cancelled, отправив DELETE /jobs/job_7ad2. Нельзя перевести failed обратно в observing, если удалённый API явно не поддерживает повтор или продолжение и новое действие не записано отдельно.

Храните удалённое и локальное состояния раздельно. cancel_requested описывает локальный факт: агент отправил запрос на отмену и ждёт подтверждения. cancelled описывает удалённый факт: сервис сообщил о конечном состоянии отмены. Это небольшое различие сильно упрощает разбор инцидента.

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

{
  "at": "2025-03-08T14:29:02Z",
  "actor": "worker-3",
  "from": "observing",
  "to": "cancel_requested",
  "cause": "human_request:req_91af",
  "remote_job_id": "job_7ad2"
}

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

Для отмены нужны подтверждение и граница ущерба

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

Когда человек или политика решает остановить задание, сначала запишите намерение отмены. Укажите, кто отправил запрос, почему и какого эффекта ожидали. Затем вызовите документированную конечную точку отмены с сохранённым remote job ID. Сохраните ответ, даже если в нём сказано лишь, что сервер принял запрос.

После отмены продолжайте опрос. Обычно допустимы конечные состояния cancelled, succeeded и failed. Завершённый результат после запроса отмены не обязательно означает ошибку. Это может быть честным исходом задания, которое пересекло точку фиксации несколькими секундами раньше. Рабочий процесс должен точно сообщить эту последовательность, а не переписать историю под желаемый результат.

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

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

Полезен и крайний срок отмены. После разумного документированного ожидания перейдите в cancellation_unconfirmed, а не заявляйте об успехе. Передайте на эскалацию remote job ID, trace ID, историю запросов и идентификаторы запросов провайдера. Такой пакет позволяет человеку или службе поддержки провайдера увидеть реальную последовательность, не восстанавливая её по сообщениям в чате.

Получение результата, отдельное действие

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

Конечное состояние успеха означает, что удалённая работа завершилась. Оно не гарантирует, что результат получен, проверен, сохранён или передан следующей системе. Считайте получение отдельным записанным действием.

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

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

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

Для пакетной работы сохраняйте результаты по отдельным элементам, если провайдер это поддерживает. Конечное задание может содержать 498 успешных элементов и два неудачных. Если назвать его просто «успешным», следующему агенту придётся заново искать частичный сбой в содержимом результата. Локальное конечное состояние может остаться успешным, а сводка результата должна содержать числа и список ссылок на неудачные элементы.

Закрывайте операцию только после выполнения условий получения. succeeded должно означать, что нужный результат доступен и проверен по вашим правилам. Если провайдер завершил работу, но получение завершилось ошибкой, используйте отдельное локальное состояние, например result_unavailable или result_validation_failed. Удалённое задание может быть закончено, а ваш рабочий процесс, ещё нет.

Trace ID связывает действия, а записи аудита фиксируют факты

Храните SSH-ключи вне агентов
sp-ssh выполняет удалённые команды, не помещая SSH-ключи в контекст агента.

Используйте trace ID для каждой операции и передавайте его при создании, чтении состояния, отмене и получении результата, если API принимает пользовательские заголовки. Узнав remote job ID, свяжите его с trace ID. Trace ID связывает события внутри ваших систем, а job ID позволяет провайдеру найти собственный рабочий объект.

Не смешивайте назначение этих идентификаторов. Trace ID не должен становиться токеном идемпотентности, потому что одна операция может включать несколько запросов с разными правилами повторов. Job ID не должен становиться записью о разрешении, потому что провайдер создал его после вашего локального решения действовать.

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

Sallyport может выполнять HTTP-вызовы API, не раскрывая сохранённые учётные данные агенту. Его сессии и отдельные вызовы создают защищённый от незаметного изменения след, который можно проверить с помощью sp audit verify. Это защищает хранение учётных данных и свидетельства действий. Но рабочему процессу всё равно нужна собственная запись операции, потому что только он знает, соответствует ли job_7ad2 нужной бизнес-задаче.

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

Эталонный цикл обрабатывает обычные сбои

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

загрузить операцию по локальному operation ID

если операции нет:
    создать и сохранить запись с отпечатком и токеном идемпотентности

если remote job ID отсутствует:
    отправить запрос на создание с сохранённым токеном
    если ответ подтверждает job ID:
        сохранить job ID и перейти к observing
    если ответ неоднозначен:
        перейти к create_unknown и выполнить сверку по сохранённому токену
    если ответ однозначно отклоняет запрос:
        перейти к failed

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

если крайний срок истёк до получения конечного состояния:
    перейти к result_timed_out и сохранить запись для сверки

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

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

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

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

Что такое асинхронное задание API?

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

Как не допустить повторной отправки асинхронного задания?

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

Как часто агент должен проверять состояние задания?

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

Что агент должен сохранить после создания фонового задания?

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

Может ли агент безопасно отменить асинхронное задание?

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

Что считается конечным состоянием асинхронного задания?

Конечные состояния, такие как succeeded, failed, cancelled или expired, означают, что задание больше не изменится. Конечное состояние не всегда означает наличие пригодного результата. Изучите контракт API, чтобы понять, возвращают ли задания в состояниях failed и cancelled диагностику, частичный результат или ничего.

Что агенту делать после тайм-аута при создании задания?

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

Нужны ли одновременно trace ID и job ID?

Используйте один trace ID при создании, проверке состояния, отмене и получении результата, а затем сохраните рядом с ним идентификатор задания API. Trace ID связывает ваши журналы, а job ID указывает на рабочий объект у провайдера. При инцидентах, затрагивающих разные процессы и сервисы, нужны оба идентификатора.

Вебхуки лучше опроса для долгих заданий?

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

Может ли шлюз действий управлять всем рабочим процессом задания?

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

Sallyport

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

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