ИИ-агент застревает в бесконечном цикле, когда модель продолжает вызывать инструмент, который завершается ошибкой (обычно с теми же аргументами), поскольку цикл агента не содержит инструкций об остановке. Каждая повторная попытка отправляет всю беседу заново, поэтому каждый шаг обходится дороже предыдущего, а оставленный работать на ночь цикл может стоить тысяч долларов.
Это руководство поможет остановить цикл с помощью четырех средств защиты, которые мы протестировали на шести актуальных моделях. Три из них работают внутри вашего агента через OpenRouter Agent SDK (@openrouter/agent 0.11.0): обнаружение зацикливания, правило для определения повторяющихся вызовов, а также ограничения на количество шагов и стоимость. Четвертое средство — лимит средств на вашем API-ключе. Мы применяем его на своей стороне, поэтому оно работает, даже если в коде вашего агента есть ошибка.
TL;DR
- При включенном обнаружении зацикливания все шесть протестированных нами моделей были заблокированы на третьем идентичном вызове инструмента. Без него 4 из 18 запусков повторяли попытки до достижения лимита в 40 шагов. С ним 16 из 18 запусков завершались после двух реальных вызовов инструмента.
При включенном обнаружении зацикливания все шесть протестированных нами моделей были заблокированы на третьем идентичном вызове инструмента. Без него 4 из 18 запусков повторяли попытки до достижения лимита в 40 шагов. С ним 16 из 18 запусков завершались после двух реальных вызовов инструмента.
- doomLoop: true в callModel блокирует третий идентичный вызов и завершает работу на шестом.
doomLoop: true в callModel блокирует третий идентичный вызов и завершает работу на шестом.
- Некоторые модели обходят обнаружение, слегка изменяя аргумент. loopKey, который нормализует аргумент, останавливает их.
Некоторые модели обходят обнаружение, слегка изменяя аргумент. loopKey, который нормализует аргумент, останавливает их.
- stopWhen: [stepCountIs(n), maxCost(x)] ограничивает каждый запуск независимо от причины зацикливания.
stopWhen: [stepCountIs(n), maxCost(x)] ограничивает каждый запуск независимо от причины зацикливания.
- Лимит средств на ключе ограничивает общие расходы. Запрос, превышающий лимит, завершается с ошибкой 402 и параметром limit_source: “openrouter_key_limit”.
Лимит средств на ключе ограничивает общие расходы. Запрос, превышающий лимит, завершается с ошибкой 402 и параметром limit_source: “openrouter_key_limit”.
Почему ИИ-агенты застревают в бесконечных циклах?
Цикл агента завершается, когда модель дает ответ, не запрашивая инструмент. Инструмент, который продолжает выдавать ошибки, не дает модели повода сделать это. Ошибка возвращается в беседу, инструкции все еще требуют завершить задачу, и новая попытка выглядит правильным следующим шагом. Ничто в цикле не считает попытки, если вы не добавите соответствующий код.
Цикл также становится дороже по мере выполнения, поскольку каждый шаг повторно отправляет всю беседу, включая все предыдущие ошибки. В одном из запусков, которые мы отслеживали шаг за шагом, объем входных данных вырос со 131 токена до 3262, а стоимость каждого шага выросла почти в четыре раза. При использовании более дорогой модели и множестве одновременно работающих сессий такой рост приводит к тому, что за одну ночь набегают тысячи долларов.
Чтобы увидеть это вживую, мы создали агента выставления счетов, единственный инструмент которого работает так же, как неработающая платежная служба. Чтобы запустить его, сохраните код как agent.ts, выполните bun add @openrouter/agent zod, установите переменную OPENROUTER_API_KEY и запустите bun agent.ts:
Все 41 вызов были идентичны, и работа завершилась только из-за срабатывания stepCountIs(40). Дополнительный вызов — это тот, который модель уже запросила к моменту срабатывания лимита, и SDK все равно выполняет его (об этом говорится в Шаге 3). Без stopWhen Agent SDK продолжает циклический опрос до тех пор, пока модель не перестанет запрашивать инструменты, поэтому данный запуск не остановился бы сам по себе. Финальный ответ гласит, что служба выдавала тайм-аут, поэтому модель знала об этом на каждом шаге и все равно повторяла попытки.
Мы запустили одного и того же агента три раза на каждой из шести моделей с включенным и выключенным обнаружением:
Поведение в циклах также меняется от запуска к запуску. Grok 4.7 сделал 41, 12 и 41 вызов для одних и тех же входных данных, поэтому один успешный тест не доказывает, что ваш агент не зациклится. Во всех 18 запусках система обнаружения сократила количество вызовов инструментов с 314 до 64, а расходы — с $0,29 до $0,05.
Шаг 1: Включите обнаружение зацикливания
Добавьте одну опцию в вызов callModel:
Обнаружение зацикливания создает цифровой отпечаток каждого вызова инструмента на основе его имени и аргументов. Сначала оно приводит аргументы к каноническому виду, поэтому перестановка ключей JSON не позволит замаскировать повтор. Повтор засчитывается только тогда, когда модель увидела предыдущий результат и запрашивает его снова, поэтому пять идентичных вызовов за один ход считаются за один. По мере роста серии задействуется стандартная шкала: наблюдение на 2-м шаге, блокировка на 3-м и остановка на 6-м.
Когда вызов блокируется, SDK пропускает выполнение (execute) и возвращает модели ошибку инструмента, сообщающую о повторении. В отличие от тайм-аута, эта ошибка говорит о том, что еще одна попытка не поможет. В большинстве запусков модель читала ее и сразу же писала окончательный ответ, завершая работу со статусом SessionEnd: complete. В описанном выше запуске она продолжала отправлять запросы, поэтому остановка прервала выполнение с причиной: doom_loop. getDoomLoopVerdict() возвращает сообщение только тогда, когда именно обнаружение зацикливания завершило работу, что делает его удобным сигналом для отправки предупреждений.
Чтобы остановить процесс раньше, уменьшите пороговые значения. С настройкой doomLoop: { ladder: { observe: 2, block: 3, stop: 4 } } запуск GPT-6 Luna завершился на серии из 4 шагов при стоимости $0,0002. Эта шкала также имеет две ступени, которые по умолчанию отключены. Параметр steer отправляет модели уведомление о том, что она повторяется, а escalate выполняет следующий шаг с использованием более мощной модели на ваш выбор.
Шаг 2: Укажите детектору, что считать одинаковым вызовом
По умолчанию два вызова считаются одинаковыми только тогда, когда их аргументы полностью совпадают. После блокировки некоторые модели слегка изменяли аргумент, что меняет цифровой отпечаток вызова и начинает новую серию:
Еще в девяти запусках Mistral Large 4 с включенным обнаружением модель поступала так пять раз, и три из них достигли лимита в 40 шагов, в основном за счет чередования INV-2291 и inv-2291. GPT-6 Luna делала то же самое, добавляя концевые пробелы и символы новой строки. Модели следуют сообщению о блокировке, которое призывает их попробовать другой подход. При наличии всего одного инструмента единственный доступный им другой подход — это другие входные данные.
Параметр loopKey для инструмента указывает детектору, какая часть входных данных идентифицирует вызов. Для идентификатора счета это цифры:
Теперь каждый вариант сопоставляется с 2291, поэтому серия продолжает расти, а повторы остаются заблокированными. Пять из шести запусков остановились после двух реальных вызовов. В шестом запуске модель попробовала другой счет — INV-0000, что корректно сбросило счетчик серии.
Нормализуйте регистр, пробелы, префиксы и все остальное, что не влияет на то, какую запись запрашивает инструмент. Чтобы полностью игнорировать поле, например тайм-аут, который модель увеличивает при каждой попытке, укажите только те поля, которые имеют значение: loopKey: [‘invoice_id’]. Инструмент, задача которого заключается в повторении (например, опрос статуса задания), принимает значение loopKey: false.
Шаг 3: Ограничьте количество шагов и стоимость для каждого запуска
Обнаружение ловит только повторы. Модель, которая придумывает что-то новое на каждом шаге, никогда не повторяет отпечаток, а модель, которая продолжает запросы после блокировки, не остановится до срабатывания остановки. Условия остановки (stop conditions) прерывают выполнение независимо от того, почему оно затянулось. Импортируйте maxCost наряду с stepCountIs, замените stopWhen и добавьте два хука жизненного цикла:
stopWhen проверяется после каждого раунда, и выполнение останавливается, когда любое условие становится истинным. maxCost суммирует usage.cost, который мы возвращаем с каждым ответом, поэтому он учитывает то, с чем вам выставлен счет. Счетчик сбоев игнорирует аргументы, поэтому он фиксирует измененные аргументы из Шага 2 даже без loopKey. В запущенном примере на Mistral Large 4, который аналогичным образом менял свои аргументы, сессия завершилась после шести вызовов на сумму $0.0062 с параметром SessionEnd: max_turns.
Каждое условие stopWhen завершает сессию с причиной max_turns, поэтому логируйте собственную причину, если вам нужно различать лимиты. Когда лимит срабатывает, а у модели еще есть незавершенные вызовы инструментов, SDK выполняет их и делает последний раунд с отключенным вызовом инструментов, чтобы выполнение завершилось письменным ответом. Вот почему этот сеанс сделал шесть вызовов при лимите в пять, а первый демо-пример сделал 41 при лимите в 40. Установите allowFinalResponse: false, чтобы пропустить этот раунд.
Установите оба лимита на основе ваших обычных запусков. Наши сеансы занимали от четырех до шести раундов и стоили от $0.0018 до $0.0053 на Mistral Large 4, так что 15 шагов и $0.01 оставляют запас, не разрешая запуск на 40 раундов. Из этих двух лимитов ограничение по стоимости важнее, так как поздние раунды стоят дороже ранних.
Без Agent SDK те же средства защиты встраиваются в любой цикл. Подсчитывайте вызовы по имени инструмента и нормализованным аргументам, возвращайте результат инструмента вроде «Заблокировано: сообщите пользователю, что пошло не так» вместо выполнения третьего повтора и останавливайтесь, когда шаги или сумма usage.cost превышают ваш лимит. Наша версия на Python для API chat completions останавливала Mistral Large 4 после четырех шагов менее чем за $0.002 во всех трех запускaх.
Шаг 4: Установите кредитный лимит для API-ключа
Все средства защиты, рассмотренные до сих пор, работают внутри процесса вашего агента. Если в этом коде есть ошибка, он развернут без лимитов или запускает сотни сессий, каждая из которых остается в пределах maxCost, ничто не ограничивает общую сумму. Кредитный лимит на ключе решает эту проблему, поскольку мы проверяем его на нашей стороне до того, как запрос достигнет провайдера.
Установите лимит (в долларах) и limit_reset (daily, weekly, monthly или null) в панели управления или с помощью API создания ключей. Если ключ использует BYOK, также установите include_byok_in_limit в значение true, так как по умолчанию оно равно false, и в противном случае расходы BYOK не учитываются в лимите. Дневные лимиты сбрасываются в полночь по UTC, а недельные — в понедельник. Выделите каждому агенту или клиенту собственный ключ, чтобы один цикл мог потратить только долю этого ключа. Если несколько агентов должны использовать общий ключ, следуйте инструкциям по ограничению общих ключей агентов.
Прежде чем маршрутизировать запрос, мы оцениваем его худший сценарий, который представляет собой размер входа плюс max_tokens. Если это не укладывается в остаток на ключе, запрос завершается ошибкой, и плата с вас не взимается. Мы отправили запрос chat completions с max_tokens: 900000 к дорогой модели на ключе с остатком $9.97 и получили следующий ответ:
Мы заменили слаг рабочего пространства, хэш ключа и ID организации.
Выполняйте ветвление по error.metadata.limit_source, а не по тексту сообщения или коду состояния. Исчерпанный ключ также сообщает об openrouter_key_limit, и в документации по лимитам этот источник указан как для ошибок кредита 402, так и для ошибок бюджета 403. Если в сообщении говорится, что ключ может позволить себе меньше токенов, как в примере выше, повторите запрос с меньшим значением max_tokens. Если ключ исчерпан, повторные попытки не смогут увенчаться успехом до сброса лимита, поэтому воспринимайте это как остановку. Agent SDK генерирует ошибку 402 в виде PaymentRequiredResponseError. Единственная ошибка 402, которую стоит повторить — это openrouter_in_flight_budget, после ее заголовка Retry-After. Ограничения частоты возвращают 429 вместо этого, что описано в нашем руководстве по лимитам LLM API.
Бюджеты рабочего пространства объединяют лимит по всем ключам в рабочем пространстве за день, неделю, месяц или за все время. Заблокированные запросы получают статус 403 с сообщением вроде Workspace monthly budget of $500.00 exceeded. Contact your org admin. (Мы не вызывали это условие специально, поэтому сообщение взято из документации.) Запросы, уже находящиеся в обработке, все равно завершаются, поэтому расходы могут немного превысить лимит, но цикл, работающий всю ночь, остановится близко к ежедневному лимиту. Руководство по управлению расходами показывает, как сочетаются бюджетирование рабочего пространства и защитные механизмы для отдельных участников.
Какой защитный механизм останавливает какой цикл?
Каждый защитный механизм перехватывает определенный тип цикла. Лимит ключа перехватывает все из них, но только после того, как лимит ключа исчерпан, поэтому он лучше всего работает в качестве последнего уровня.
Часто задаваемые вопросы
Почему мой агент продолжает вызывать один и тот же инструмент после ошибки?
Результат инструмента говорит о том, что вызов не удался, а промпт предписывает завершить задачу, поэтому модель считает еще одну попытку правильным шагом. Возвращайте ошибки, которые говорят модели, что делать дальше, например: «Сервис выставления счетов не работает. Сообщите пользователю и остановитесь». Не призывайте модель повторять попытку до тех пор, пока она не увенчается успехом.
Останавливает ли лимит максимального количества итераций неоправданные расходы?
Лимит максимального количества итераций мешает одному сеансу длиться вечно, но он не ограничивает ваши расходы, так как поздние раунды стоят дороже, а множество сеансов в пределах лимита все равно суммируются. Такие настройки, как max_iterations в AgentExecutor от LangChain и max_iter в CrewAI, выполняют ту же задачу, что и stepCountIs в OpenRouter Agent SDK. Соедините одну из них с ограничением стоимости за сеанс и кредитным лимитом на API-ключе.
Следующие шаги
- Обнаружение циклов зависания: лестница, управление, эскалация и переопределения для каждого инструмента.
Обнаружение циклов зависания: лестница, управление, эскалация и переопределения для каждого инструмента.
- Условия остановки: stepCountIs, maxCost, maxTokensUsed и пользовательские условия.
Условия остановки: stepCountIs, maxCost, maxTokensUsed и пользовательские условия.
- Настройка контроля расходов команды на ИИ: бюджеты рабочего пространства и защитные механизмы в панели управления.
Настройка контроля расходов команды на ИИ: бюджеты рабочего пространства и защитные механизмы в панели управления.
- Agent SDK: Создание многошаговых рабочих процессов агентов на OpenRouter: как callModel запускает цикл агента.
Agent SDK: Создание многошаговых рабочих процессов агентов на OpenRouter: как callModel запускает цикл агента.










