MAATRIX / Блог / ИИ-агент зациклился и сжёг лимиты за ночь

ИИ-агент зациклился и сжёг лимиты за ночь

MAATRIX

Вы запустили автономного ИИ-агента на ночь — пусть доделает задачу, пока вы спите. Утром открываете дашборд провайдера API и видите, что расход за ночь в разы больше обычного дневного. В логах — не десяток разных шагов, а один и тот же неудачный ход, повторённый снова и снова. Это не редкий баг конкретного фреймворка, а системная дыра в том, как устроен цикл «думать → действовать → проверить результат» у большинства агентов. Разберём, почему так происходит и как поставить ограничители заранее, а не после того, как счёт за API вырос.

Как это выглядит утром

Картина обычно одна и та же независимо от того, на чём написан агент — LangChain, кастомный цикл на голом Python или самодельная обёртка поверх Claude/OpenAI API.

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

Дальше вы открываете лог агента и видите что-то вроде:

[02:14:03] step 1042: run_tests() → FAILED: ModuleNotFoundError: no module named 'utils'
[02:14:03] step 1042: thought: "Похоже, модуль utils не установлен, попробую переустановить зависимости"
[02:14:05] step 1043: action: pip install -r requirements.txt
[02:14:07] step 1043: result: already satisfied
[02:14:08] step 1044: run_tests() → FAILED: ModuleNotFoundError: no module named 'utils'
[02:14:08] step 1044: thought: "Похоже, модуль utils не установлен, попробую переустановить зависимости"
[02:14:10] step 1045: action: pip install -r requirements.txt
...

Разница между шагом 1042 и шагом 1500 практически нулевая. Агент не «застрял» в смысле зависшего процесса — он активно работает, дёргает API модели, вызывает инструменты, получает ответы. Просто он раз за разом приходит к одному и тому же неверному выводу и повторяет одно и то же действие (иногда с косметическими вариациями вроде смены порядка аргументов, не меняющими суть). За несколько часов таких итераций набегает совсем не то количество вызовов API, на которое вы рассчитывали.

Отдельно неприятно то, что внешне процесс выглядит «живым»: CPU занят, сеть используется, в логе что-то происходит. Без мониторинга именно на паттерн повторов (а не просто на «жив ли процесс») обнаружить проблему раньше утра почти нереально — та же логика, что и в разборе тихого сбоя cron-задачи: процесс не падает и не сигналит об ошибке явно, он просто молча делает не то, что нужно.

Почему агенты вообще зацикливаются

Автономный агент — это не просто «чат-бот, который сам решил делать что-то ещё». Ключевое отличие, которое мы уже разбирали в статье «ИИ-агенты vs чат-боты: в чём разница», в том, что агент работает по циклу: он получает задачу, планирует шаг, выполняет действие через инструмент (запуск команды, вызов API, чтение файла), получает результат этого действия обратно в контекст и на основе этого результата решает, что делать дальше. Цикл повторяется, пока задача не будет решена или не сработает какое-то условие остановки.

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

Типичная причина зацикливания — не «модель тупит вообще», а конкретная механика:

  • Агент получает результат инструмента, который не может правильно интерпретировать. Ошибка сформулирована не так, как модель ожидает, или содержит техническую деталь, которую она трактует неверно. В примере выше ModuleNotFoundError: no module named 'utils' на самом деле может означать, что не тот utils, а локальный модуль с относительным путём — а агент читает это как «зависимость не установлена» и раз за разом чинит не ту вещь.
  • Модель предлагает тот же самый (или почти тот же) неверный подход снова. Это особенно характерно, когда в контексте нет явной памяти о предыдущих неудачных попытках, либо контекст настолько разросся, что модель теряет из виду, что уже пробовала — подробнее об этом в статье про управление контекстным окном для агентов. Каждая новая итерация выглядит для модели как свежая попытка, а не как повтор.
  • В самой логике агентного цикла нет жёсткого лимита на число итераций. Это главная архитектурная дыра: если код позволяет циклу выполняться, пока не выполнено условие успеха, и не имеет предохранителя «а если не выполнено — прерви после N попыток», предела теоретически нет. На практике он появляется только тогда, когда кончаются деньги на счету у провайдера или агент упирается в лимит скорости запросов.
  • Повторяющийся паттерн действий никак не детектируется. Агент не сравнивает текущий шаг с предыдущими — у него просто нет для этого механизма, хотя человеку, глядя на лог, достаточно секунды, чтобы увидеть повтор.

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

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Развернуть ИИ на сервере

Немедленные действия: что делать в первые полчаса

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

  1. Остановите агента. Не «дайте ему доработать, может, сам справится» — если он не справился за сотни итераций, за тысячу первую тоже не справится. Убейте процесс или отмените запланированную задачу.
  2. Оцените реальный ущерб. Откройте дашборд использования у провайдера API и посмотрите число токенов и стоимость за время, пока агент работал без присмотра — потраченные токены это деньги. Если у агента отдельный API-ключ (заведите именно отдельным — упрощает мониторинг и отзыв доступа), проверка займёт пару минут.
  3. Не перезапускайте задачу вслепую. Прочитайте первые итерации цикла (не последние — они уже почти не отличаются друг от друга) и поймите, какая ошибка запустила повторение, прежде чем нажимать «запустить снова».
  4. Проверьте побочные эффекты и зафиксируйте причину. Если агент в рамках попыток создавал файлы, делал коммиты или дёргал внешние сервисы — повторение могло размножить и эти действия, не только сжечь токены. Отключить агенту доступ в интернет на ночь — не решение; решение — понять, что в логике цикла позволило повторению идти бесконтрольно, и закрыть дыру предохранителем, о которых речь ниже.

Жёсткий лимит шагов и бюджета — обязательный минимум

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

Если вы пишете цикл сами, это выглядит примерно так:

MAX_STEPS = 40

def run_agent_loop(task):
    step = 0
    while not task.is_done():
        if step >= MAX_STEPS:
            raise AgentLoopLimitExceeded(
                f"Достигнут лимит {MAX_STEPS} шагов, задача не завершена"
            )
        action = model.decide_next_action(task, history=task.history)
        result = execute_tool(action)
        task.history.append((action, result))
        step += 1

Конкретное число шагов зависит от сложности типичной задачи: для простых сценариев (проверить статус, вызвать один инструмент, оценить ответ) достаточно 10-15 шагов, для многоэтапных задач с исследованием кода — в разы больше. Ориентируйтесь на то, сколько шагов реально требуется успешному прогону, и ставьте лимит с запасом в 2-3 раза — не «на всякий случай побольше». Если вы используете готовый фреймворк, большинство даёт такой параметр из коробки, но у него бывает значение по умолчанию, которое либо отсутствует, либо неоправданно высокое — проверьте документацию явно, а не полагайтесь на дефолт.

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

MAX_TASK_COST_USD = 2.00

def run_agent_loop(task):
    spent = 0.0
    while not task.is_done():
        if spent >= MAX_TASK_COST_USD:
            notify_and_stop(task, reason="budget_exceeded", spent=spent)
            break
        action = model.decide_next_action(task, history=task.history)
        result = execute_tool(action)
        spent += estimate_cost(action, result)  # по токенам и цене модели
        task.history.append((action, result))

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

Тип лимитаЧто ловитЧего не ловит
Лимит шаговБесконечный цикл с примерно одинаковой стоимостью шагаДорогие единичные шаги (большой контекст на один вызов)
Лимит бюджета ($)Реальный финансовый ущерб независимо от числа шаговЗацикливание с дешёвыми шагами, которое не успевает выбрать бюджет за разумное время
Лимит по времени (wall-clock)Задачи, которые физически идут слишком долгоБыстрый, но дорогой цикл — уложится в лимит времени, спалив бюджет
Детекция повторов (см. ниже)Зацикливание на конкретном паттерне действийРазнообразные, но всё равно безуспешные попытки без явного повтора

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

Детекция повторяющегося паттерна как отдельный сигнал остановки

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

Идея простая: сохраняйте сигнатуру каждого шага (какое действие, с какими параметрами, какой инструмент) и сравнивайте с недавней историей. Если одна и та же (или почти одна и та же) сигнатура встретилась подряд N раз — это сильный сигнал, что агент застрял, и продолжать бессмысленно, даже если формальные лимиты по шагам и бюджету ещё не исчерпаны.

from collections import deque
import hashlib

RECENT_WINDOW = 5
REPEAT_THRESHOLD = 3  # сколько повторов подряд считать зацикливанием

def action_signature(action):
    # нормализуем: инструмент + отсортированные ключи аргументов
    # (без учёта мелких деталей вроде временных меток в аргументах)
    key = f"{action.tool_name}:{sorted(action.args.items())}"
    return hashlib.sha256(key.encode()).hexdigest()[:16]

def run_agent_loop(task):
    recent_signatures = deque(maxlen=RECENT_WINDOW)
    repeat_count = 0
    last_sig = None

    while not task.is_done():
        action = model.decide_next_action(task, history=task.history)
        sig = action_signature(action)

        if sig == last_sig:
            repeat_count += 1
        else:
            repeat_count = 0
        last_sig = sig

        if repeat_count >= REPEAT_THRESHOLD:
            notify_and_stop(task, reason="repeated_action_detected", signature=sig)
            break

        result = execute_tool(action)
        task.history.append((action, result))
        recent_signatures.append(sig)

Это упрощённый пример — на практике полезно ловить не только точное совпадение подряд идущих действий, но и повтор через один-два шага (агент иногда «пробует что-то другое» и возвращается к тому же подходу), а также семейство похожих действий с разными, но по сути эквивалентными аргументами. Даже базовая версия ловит подавляющее большинство реальных случаев зацикливания вроде примера из начала статьи.

Отдельно заведите уведомление (Telegram, почта, вебхук) именно на срабатывание этого детектора — оно должно приходить туда, где его увидят быстро, а не только в лог, который никто не читает до утра. Та же логика, что и с любым тихим сбоем: сигнал, спрятанный только в логах, которые смотрят постфактум, работает не лучше, чем незамеченная ошибка 429 rate limit — её тоже часто находят только на следующий день.

Как это выглядит на практике в разных агентных сетапах

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

  • В большинстве реализаций на базе LangChain/LangGraph есть параметр, ограничивающий число итераций исполнителя — проверьте его явно в конфигурации, а не полагайтесь на дефолт.
  • Если агент запущен как фоновый процесс через systemd, cron или в контейнере, добавьте на уровне оркестрации ещё один независимый предохранитель — например, RuntimeMaxSec в systemd, как страховку на случай, если предохранитель внутри кода агента не сработал. Дублирование лимитов на разных уровнях — не избыточность, а нормальная практика для автономных процессов.
  • Если задача предполагает несколько под-агентов сразу (как в мультиагентных системах), лимиты нужны и на каждого, и на сумму по всей системе — иначе один застрявший останется незамеченным на фоне остальных.
  • Отдельный API-ключ на каждого автономного агента — не бюрократия, а необходимость: без этого вы не поймёте по биллингу, какой именно процесс сжёг лимит.
  • На арендованном сервере разумно вынести мониторинг расхода API в общий стек рядом с метриками CPU/памяти/диска — тогда алерт придёт по тому же каналу, что и остальные критичные уведомления.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Развернуть ИИ на сервере

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Можно ли просто снизить temperature модели, чтобы она не повторяла одно и то же?

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

Что делать, если задача действительно сложная и ей нужно больше итераций, чем разумный лимит?

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

Нужно ли останавливать агента, если он «почти решил» задачу к моменту достижения лимита?

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

Достаточно ли лимита по времени выполнения, чтобы не ставить отдельно лимит по бюджету?

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

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

Запустите первые несколько прогонов вручную под присмотром и зафиксируйте фактическую стоимость успешного решения, затем ставьте лимит с запасом (обычно 2-3-кратным) на основе этих цифр, а не на глаз.

Обсудить статью, задать вопрос или начать новую тему

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

Перейти в сообщество →