Практика эксплуатации агентов

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

Уровень: средний Чтение: до 7 минут Результат: независимые лимиты и диагностируемая остановка

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

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

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

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

  • деньги ограничивают накопленную оценочную стоимость;
  • время ограничивает полную длительность запуска;
  • действия ограничивают количество вызовов модели и инструментов.

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

1. Задайте политику остановки

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

{
  "budget": {
    "max_cost_usd": 0.50,
    "max_elapsed_seconds": 90,
    "max_actions": 20
  },
  "tool_timeout_seconds": 15
}

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

2. Используйте единый объект состояния

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

type StopReason =
  | "completed"
  | "cost_limit"
  | "time_limit"
  | "action_limit"
  | "cancelled"
  | "unhandled_error";

type BudgetState = {
  startedAtMs: number;
  elapsedMs: number;
  actionsUsed: number;
  estimatedCostUsd: number;
  stopReason?: StopReason;
};

type BudgetLimits = {
  maxCostUsd: number;
  maxElapsedMs: number;
  maxActions: number;
};

stopReason — машинно-читаемое поле. Пользовательское сообщение можно строить отдельно, но нельзя сохранять только текст вроде «что-то пошло не так».

3. Проверяйте бюджет до и после действия

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

function checkBudget(
  state: BudgetState,
  limits: BudgetLimits,
  nowMs: number
): StopReason | undefined {
  state.elapsedMs = nowMs - state.startedAtMs;

  if (state.estimatedCostUsd >= limits.maxCostUsd) {
    return "cost_limit";
  }
  if (state.elapsedMs >= limits.maxElapsedMs) {
    return "time_limit";
  }
  if (state.actionsUsed >= limits.maxActions) {
    return "action_limit";
  }
  return undefined;
}

async function runAgent(limits: BudgetLimits): Promise<BudgetState> {
  const state: BudgetState = {
    startedAtMs: Date.now(),
    elapsedMs: 0,
    actionsUsed: 0,
    estimatedCostUsd: 0
  };

  while (true) {
    const before = checkBudget(state, limits, Date.now());
    if (before) {
      state.stopReason = before;
      return state;
    }

    const next = await chooseNextAction();

    if (next.kind === "finish") {
      state.stopReason = "completed";
      return state;
    }

    state.actionsUsed += 1;

    const result = await executeWithTimeout(next);
    state.estimatedCostUsd += result.estimatedCostUsd;

    const after = checkBudget(state, limits, Date.now());
    if (after) {
      state.stopReason = after;
      return state;
    }

    await applyResult(result);
  }
}

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

4. Не угадывайте стоимость

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

function estimateCost(
  inputUnits: number,
  outputUnits: number,
  inputUsdPerMillion: number,
  outputUsdPerMillion: number
): number {
  return (
    inputUnits * inputUsdPerMillion +
    outputUnits * outputUsdPerMillion
  ) / 1_000_000;
}

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

5. Возвращайте диагностируемый итог

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

{
  "status": "stopped",
  "reason": "action_limit",
  "budget": {
    "actions_used": 20,
    "estimated_cost_usd": 0.18,
    "elapsed_ms": 42310
  },
  "limits": {
    "max_actions": 20,
    "max_cost_usd": 0.50,
    "max_elapsed_ms": 90000
  }
}

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

Проверка результата

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

  1. Установите maxActions: 3, а денежный и временной пределы сделайте заведомо большими.
  2. Настройте имитатор так, чтобы он всегда возвращал «продолжить».
  3. Убедитесь, что выполнено ровно три действия и получена причина action_limit.
  4. Повторите проверку с искусственной задержкой и малым maxElapsedMs; ожидайте time_limit.
  5. Повторите с фиксированной оценкой стоимости каждого действия; ожидайте cost_limit.
  6. Проверьте обычный путь завершения; его причина должна быть completed.

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

Типовые ошибки

Один лимит вместо трёх
Ограничение в 20 шагов не защищает от одного вызова длительностью несколько минут или от дорогого запроса.
Сброс бюджета при повторной попытке
Retry — часть исходного запуска. Передавайте ему тот же объект состояния.
Учёт только успешных действий
Неуспешные вызовы также расходуют время, квоты и иногда деньги. Считайте попытку до её выполнения.
Проверка только после вызова
Так агент начинает новое действие, хотя лимит уже исчерпан. Проверяйте состояние на обеих границах.
Нечёткая причина остановки
Строка «агент завершён» скрывает разницу между успехом и защитным отключением. Используйте стабильный код причины.
Подмена общего дедлайна тайм-аутом инструмента
Серия коротких вызовов способна превысить общий срок, даже если каждый уложился в собственный тайм-аут.

Ограничения подхода

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

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

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

Короткий контрольный список

  • Три лимита заданы независимо и действуют на весь запуск.
  • Бюджет проверяется до и после каждого действия.
  • Неудачные и прерванные попытки учитываются.
  • Общий дедлайн не заменён тайм-аутом отдельного инструмента.
  • Результат содержит стабильную причину остановки и значения счётчиков.
  • Стоимость помечена как фактическая или оценочная.
  • Повторные попытки не обнуляют состояние.

Другие схемы безопасного выполнения собраны в практических руководствах, а определения терминов — в глоссарии.