Цикл отработал чисто сорок раз на тестах. На сорок первом запуске, уже в продакшне, он снова и снова вызывал один и тот же SQL-инструмент с одним и тем же сломанным запросом - пока не сжёг суточный бюджет API и биллинговое оповещение наконец не разбудило дежурного. Никто не написал плохую модель. Никто не менял промпт. Агент просто так и не решил, что закончил.
Это паттерн, который я раз за разом вижу у команд, переносящих агента из прототипа в круглосуточную нагрузку. Циклы AI-агентов часто дают сбой в продакшне не потому, что модель вдруг стала хуже, а потому что на уровне исполнения нет дисциплины завершения, проверенных контрактов инструментов, ограниченного контекста и надёжного состояния. Цикл агента - это стохастическая система, принимающая одно последовательное решение за другим. Без нескольких конкретных ограничителей редкий сбой становится гарантированным, как только запустить его достаточно долго. Управляемые платформы агентов (Vertex AI Agent Builder, Bedrock Agents, Azure AI Foundry) встраивают часть этих ограничителей; это руководство - для тех, кто выбрал самостоятельный хостинг и хочет управлять циклом самостоятельно.
Ставки достаточно высоки: Gartner ожидает, что более 40% агентных AI-проектов будут свёрнуты к концу 2027 года, ссылаясь на растущие затраты и неясную ценность. Далее описаны шесть конкретных способов, которыми циклы ломаются в продакшне: механизм каждого сбоя и паттерн обвязки, который его устраняет, с деталями для LangGraph и n8n, а также с тем, что нужно для реальной круглосуточной работы.
Кратко
- Бесконечные циклы: агент никогда не решает, что закончил. Совместите жёсткий потолок шагов (LangGraph's
recursion_limit, по умолчанию 25) с детектором отсутствия прогресса, который прекращает выполнение при повторе одинаковых вызовов инструмента с теми же аргументами. - Переполнение контекста: цикл заполняет собственное контекстное окно накопленной историей, пока вызовы не начнут усекаться или завершаться с ошибкой. Сжимайте историю через фиксированные интервалы, чтобы рабочий контекст оставался ограниченным.
- Скрытые сбои инструментов: инструмент возвращает пустую строку, модель воспринимает её как допустимый результат без действий, и агент "успешно" завершает работу, не сделав ничего. Проверяйте каждый результат инструмента до того, как модель его увидит.
- Деградация рассуждений: качество падает по мере роста контекста, даже не достигая жёсткого предела. Сжимайте в середине цикла, но при этом защищайте закреплённые инструкции безопасности.
- Потеря состояния при перезапуске: аварийное завершение означает старт с нуля. Сохраняйте контрольные точки в Postgres (LangGraph
PostgresSaver), не в SQLite, для продакшна. - Шквал повторных запросов: десять агентов, каждый из которых повторяет запросы десять раз, обрушат на неработающий сервис сотню запросов. Добавьте экспоненциальную задержку с джиттером и глобальный автоматический выключатель.
Что не охватывает это руководство
Это руководство по обвязке, сфокусированное на инженерии вокруг цикла, а не на модели внутри него. Несколько смежных тем намеренно выходят за рамки:
- Сбои координации мульти-агентов (устаревшие данные, потерянное состояние между агентами) - это отдельная проблема, заслуживающая отдельного разбора.
- Безопасность агентов (инъекция промптов, отравление инструментов) - отдельная категория сбоев со своей моделью угроз.
- Выбор модели и дообучение. В этом руководстве предполагается, что модель уже выбрана и вы отлаживаете систему вокруг неё.
- Управляемые сервисы агентов, упомянутые выше; паттерны здесь предназначены для пути с самостоятельным хостингом.
Бесконечные циклы: когда агент никак не решает, что закончил
Агент зацикливается навсегда, когда у него нет ни жёсткого потолка шагов, ни способа определить, что прогресс прекратился. Исправление состоит из двух частей: жёсткий потолок как страховочный стопор по затратам и детектор отсутствия прогресса, который хеширует каждый вызов инструмента с аргументами и завершает работу при обнаружении повторного вызова. В LangGraph этот потолок задаётся через recursion_limit, по умолчанию 25 шагов; при его превышении граф генерирует исключение GraphRecursionError.
Документация LangGraph описывает это ограничение как достижение "максимального количества шагов до срабатывания условия остановки", и вот в чём ловушка: recursion_limit - это не защита от зацикливания. Это страховочный стопор, срабатывающий после когда цикл уже потратил двадцать пять шагов и сопутствующий бюджет API. Собственная выученная логика завершения агента должна остановить его задолго до этого - и эта логика может дать сбой независимо. В одном задокументированном случае с LangGraph агент text-to-SQL зациклился и дошёл до предела recursion_limit несмотря на явные условия остановки в промпте. Он снова и снова вызывал один и тот же инструмент запросов с одним и тем же нерабочим SQL, а issue был закрыт как "не планируется". Я расцениваю это как чёткий сигнал: не считайте потолок условием остановки. Это ремень безопасности, а не тормоза.
Повысить потолок несложно - передайте его через конфигурацию при вызове графа:
# The hard ceiling -- a backstop, not loop protection
graph.invoke(
{"messages": [("user", "Generate the quarterly report")]},
{"recursion_limit": 50},
)
То, что реально останавливает застрявший цикл, - это детектор прогресса. Механизм прост: хешировать имя инструмента плюс его аргументы на каждом шаге, хранить небольшое окно последних хешей и прерывать выполнение при обнаружении повтора.
import hashlib
def step_signature(tool_name: str, tool_args: dict) -> str:
payload = f"{tool_name}:{sorted(tool_args.items())}"
return hashlib.sha256(payload.encode()).hexdigest()
# Inside your loop: terminate if the same tool+args repeats within the window
seen = recent_signatures[-WINDOW:]
sig = step_signature(tool_name, tool_args)
if sig in seen:
raise StopReason("no_progress: repeated tool call detected")
recent_signatures.append(sig)
Это отлавливает агента, который технически "работает" (вызывает инструменты, генерирует токены), но зациклился на одном и том же неудачном действии. Описанный режим сбоя соответствует тому, что таксономия MAST (IBM Research и UC Berkeley) называет Unaware of Termination Conditions (FM-1.5) - один из режимов сбоя, который, по результатам их анализа, приводит к полному провалу задачи.
Потолок шагов останавливает неконтролируемые расходы. Детектор отсутствия прогресса останавливает цикл, который технически "прогрессирует", но повторяет одно и то же. В продакшне нужны оба.
Переполнение контекстного окна: когда цикл забивает собственный контекст мусором
Долго работающий цикл накапливает все выходные данные инструментов, все промежуточные рассуждения и все сгенерированные сообщения, а затем на каждом шаге снова загружает всё это в контекстное окно. В итоге окно заполняется, и вызовы либо молча усекаются, либо завершаются с ошибкой. Решение - периодическое сжатие контекста: каждые N шагов сжимать накопленную историю в текущее резюме, чтобы рабочий контекст оставался ограниченным.
Представьте исследовательского агента, работающего уже час. К шагу 60 он несёт полный текст каждой загруженной страницы, каждого результата поиска, каждого следа рассуждений. Вся эта необработанная история не помогает ему на шаге 61, но занимает место в окне, и модель тратит бюджет внимания на токены, которые ей больше не нужны. Когда окно заполняется, провайдер усекает его с одного конца, и агент тихо теряет инструкцию, с которой начинал работу.
Момент срабатывания - это решение по настройке, и здесь есть полезная точка отсчёта. В описании реальной продакшн-системы от Mem0 отмечается, что компрессор агента Hermes "по умолчанию срабатывает при заполнении 50% контекстного окна модели", с дополнительной страховкой при 85% для сессий, которые резко разрастаются между шагами. Пятьдесят процентов - разумная отправная точка: сжимать достаточно рано, чтобы один большой выход инструмента не смог превысить лимит до следующего планового сжатия.
Примечание: Overflow и деградация рассуждений — это разные проблемы, и следующий раздел посвящён второй из них. Overflow — это жёсткое ограничение: у вас заканчиваются tokens. Деградация — процесс постепенный: модель работает всё хуже. до достижения барьера. Нужно решать обе проблемы, и порог срабатывания выше защищает от жёсткого барьера.
Ограничение контекста - это ответственность обвязки, а не функция модели. Сжимайте по интервалам, не дожидаясь принудительного молчаливого усечения.
Скрытые сбои вызовов инструментов: когда агент "успешно" не делает ничего
Вызов инструмента возвращает пустую строку или мягкое сообщение "результаты не найдены", модель воспринимает это как корректный результат, и агент продолжает работу как ни в чём не бывало - внешне успешно, но фактически бездействуя. Решение - шлюз валидации для каждого возврата инструмента: проверять выходные данные по схеме или минимальным критериям до того, как их увидит модель, и сигнализировать о реальном сбое, который цикл должен обработать, а не о пустом успехе.
Это особенно коварно, потому что ничего не падает. Разработчик, описывавший скрытые режимы сбоев в продакшн-агентах , сформулировал прямо: модели воспринимают обобщённые пустые строки как корректные результаты без действий и продолжают выполнение, не осознавая сбоя. Запрос к базе данных, вернувший ноль строк из-за обрыва соединения, выглядит для модели абсолютно так же, как запрос, легитимно ничего не нашедший. Агент сообщает "подходящих записей не найдено" и движется дальше, а через неделю выясняется, что треть его запусков были тихо сломаны.
Шлюз валидации располагается между инструментом и моделью:
def gate_tool_result(tool_name: str, result):
# Reject empties and soft errors before the model can rationalize them
if result is None or (isinstance(result, str) and not result.strip()):
raise ToolFailure(f"{tool_name} returned empty -- treat as failure, not no-op")
if isinstance(result, str) and result.lower().startswith(("error", "exception")):
raise ToolFailure(f"{tool_name} returned a soft error: {result[:120]}")
return result # validated -- safe to hand back to the model
Суть не в конкретных проверках - ваши будут зависеть от того, что каждый инструмент законно возвращает. Суть в том, что непроверенное возвращаемое значение - это решение, переданное стохастической модели, а стандартное поведение модели - продолжать выполнение.
Непроверенный возврат инструмента - это скрытый сбой в ожидании своего часа. Проверяйте выход, не доверяйте вызову.
Деградация рассуждений при длинных контекстах: когда агент работает хуже по мере работы
Даже не достигая жёсткого предела контекста, качество рассуждений деградирует по мере его роста. Это эффект "потерянного в середине": модель стабильно обращает внимание на начало и конец длинного контекста, но теряет середину. Решение - сжатие в середине цикла с сохранением закреплённых ограничений: сжимать шум, защищать опорные инструкции.
У этого механизма есть название. Инженерный блог Anthropic называет его context rot: "по мере увеличения количества токенов в контекстном окне способность модели точно воспроизводить информацию из этого контекста снижается." Поскольку "каждый токен обращает внимание на каждый другой токен", вы получаете n² попарных связей для n токенов, и внимание модели становится всё тоньше с ростом контекста.
Этот уточняющий момент - защита опорных инструкций- это и есть суть всего, и существует задокументированный инцидент, показывающий почему. В задокументированном случаеагент OpenClaw массово удалил входящие письма пользователя в ходе уплотнения контекста, потому что инструкция безопасности ("не предпринимай действий, пока я не скажу") была исключена из активного контекста при сжатии истории. Ограничение, которое должно было остаться последним, было воспринято как обычная история и убрано в резюме.
Поэтому наивное "резюмировать всё старше N шагов" опасно. Сжатие должно знать, что нельзя выбрасывать ни при каких условиях:
PINNED = {"system_constraints", "safety_instructions", "active_task_spec"}
def compress_history(messages):
pinned = [m for m in messages if m.tag in PINNED] # never summarized
transient = [m for m in messages if m.tag not in PINNED]
summary = summarize(transient) # lossy is fine here
return pinned + [summary] # constraints survive intact
Это отличается от проблемы переполнения из предыдущего раздела. Переполнение - это нехватка места; деградация - это ухудшение работы модели при ещё достаточном месте. Можно быть на 60% заполнения окна и уже рассуждать плохо.
Примечание: Сжатие, уничтожающее ограничение безопасности, - это принципиально иной класс ошибок по сравнению со сжатием, которое теряет устаревший результат поиска. Помечайте ограничения, спецификацию задачи и любые инструкции "не делать X" как закреплённые и полностью исключайте их из алгоритма сжатия.
Сжатие, уничтожающее инструкцию безопасности, хуже, чем отсутствие сжатия. Защищайте закреплённые ограничения при сжатии.
Потеря состояния при перезапуске: когда аварийное завершение означает старт сначала
Когда долго работающий агент аварийно завершается - из-за перезагрузки, OOM-kill или обрыва сетевого соединения - по умолчанию нет возможности возобновить с контрольной точки. Цикл начинается сначала: повторяет уже выполненную работу и, что хуже, может повторно выполнить уже совершённые действия - например, отправить то же письмо дважды или снова сделать платный API-вызов. Решение - checkpointing: сохранять состояние цикла после каждого шага, чтобы при перезапуске восстановиться с места остановки, а не с нуля.
В LangGraph выбор бэкенда для контрольных точек - это выбор между разработкой и продакшном. Документация по персистентности LangGraph описывает SqliteSaver как "идеальный для экспериментов и локальных рабочих процессов" и PostgresSaver как "идеальный для использования в продакшне" - именно на нём работает сам LangSmith. Оба намеренно параллельны в коде, что делает разницу наглядной:
# Development -- single file, no server, do not ship this
from langgraph.checkpoint.sqlite import SqliteSaver
# Production -- survives the box it runs on
from langgraph.checkpoint.postgres import PostgresSaver
Две детали, которые часто подводят. Во-первых, пакеты для checkpointing устанавливаются отдельно от основного LangGraph (langgraph-checkpoint-sqlite и langgraph-checkpoint-postgres являются отдельными зависимостями), поэтому на новой машине Postgres saver не появится автоматически - нужно явно его добавить. Во-вторых, каждая операция checkpointing требует thread_id в конфигурации. Именно этот ID связывает конкретный запуск с сохранённым состоянием, и перезапуск без правильного thread_id ничего не восстановит.
Совет: Пакеты LangGraph для checkpointing устанавливаются отдельно.
langgraph-checkpoint-postgresне входит в базовый пакетlanggraphпоэтому добавьте его в production requirements заранее, чтобы не узнать об этом в разгар инцидента.
В n8n та же дихотомия разработка/продакшн, просто под другими названиями. Его встроенная опция памяти также называется Simple Memory (или Buffer Window Memory), а продакшн-путь - это Postgres Chat Memory node для состояния, которое должно пережить перезапуск. Встроенная память хранит разговор в работающем процессе: это нормально для тестирования, но риск для круглосуточной нагрузки. Практики, запускающие n8n-агентов в реальных условиях, сообщают о необходимости мигрировать на Postgres-хранилище после того, как внутрипроцессная память разрасталась настолько, что валила весь экземпляр. Если вы используете n8n и агент должен что-то помнить после перезапуска, подключите Postgres Chat Memory с самого начала.
SQLite для checkpointing - это удобство при разработке. Выживание в продакшне после перезапуска требует Postgres (LangGraph) или Postgres-бэкенда (n8n).
Шквал повторных запросов: когда ваши агенты кладут DDoS-ом упавший сервис
Когда нижестоящий сервис падает, наивные повторные попытки на уровне каждого выполнения превращают ваш флот агентов в самоуправляемую атаку типа "отказ в обслуживании". Решение состоит из двух частей: экспоненциальная задержка с джиттером на каждом агенте для распределения повторных попыток во времени и глобальный автоматический выключатель, который срабатывает после общего порога ошибок и не даёт всему стаду долбить явно упавший сервис.
Математика беспощадна. Как описывает статья о паттернах повторных попыток : при десяти параллельных агентах, каждый из которых делает десять попыток, вы отправляете сотню запросов к сервису, который уже лежит на полу, потому что задержка каждого агента - это задержка на уровне выполнения, а не глобальная. Задержка на уровне агента сама по себе не решает проблему. Десять агентов, каждый из которых вежливо ждёт, всё равно ждут синхронно, если все стартовали в одно время, - и повторяют попытки синхронизированными волнами. Джиттер разрушает синхронизацию, рандомизируя ожидание каждого агента; автоматический выключатель разрушает стадный эффект, разделяя одно состояние ошибки между всеми.
Половина с задержкой - это решённая задача в Python; библиотека tenacity обрабатывает экспоненциальный режим с джиттером чисто:
from tenacity import retry, stop_after_attempt, wait_random_exponential
@retry(wait=wait_random_exponential(multiplier=1, max=60), stop=stop_after_attempt(5))
def call_flaky_service(payload):
return downstream.post(payload)
Автоматический выключатель - это половина, которая должна быть глобальной: общей для всех агентов, а не создаваться заново при каждом выполнении. Когда ошибки превышают порог, он открывается, каждый агент немедленно отказывает вместо вызова наружу, а после задержки охлаждения пропускает один пробный запрос для проверки восстановления сервиса. Выключатель, живущий внутри процесса каждого агента, не защищает ничего, потому что ничего не является общим; упавший сервис всё равно получит все сто запросов.
Задержка на уровне выполнения позволяет десяти агентам синхронно атаковать упавший сервис. Автоматический выключатель должен быть глобальным, чтобы остановить стадо.
Все шесть сбоев в одном месте
Прежде чем перейти к инфраструктуре, вот весь каталог сразу: сбой, механизм, вызывающий его, исправление обвязки и место соответствующего параметра в каждом фреймворке.
| Режим отказа | Механизм | Исправление обвязки | Параметр фреймворка |
|---|---|---|---|
| Бесконечный цикл | Нет потолка шагов или проверки прогресса | Жёсткий потолок + детектор отсутствия прогресса | LangGraph recursion_limit (25) / n8n Max Iterations |
| Переполнение контекста | История растёт, пока окно не заполнится | Периодическое сжатие | На уровне приложения (сжатие при ~50% окна) |
| Скрытый сбой инструмента | Пустые/мягкие возвраты воспринимаются как корректные результаты без действий | Шлюз валидации для каждого результата инструмента | Обёртка инструмента на уровне приложения |
| Деградация рассуждений | Внимание деградирует по мере роста контекста ("context rot") | Сжатие в середине цикла с защитой закреплённых ограничений | На уровне приложения, с учётом ограничений |
| Потеря состояния при перезапуске | Нет контрольной точки; цикл стартует с нуля | Постоянный checkpointing | LangGraph PostgresSaver / n8n Postgres Chat Memory |
| Шквал повторных запросов | Повторные попытки на уровне выполнения каскадируют на упавший сервис | Задержка + джиттер + глобальный автоматический выключатель | tenacity + общее состояние выключателя |
Примечание для читателей, работающих с CrewAI, AutoGen, Dify или самописным Python-циклом: параметры фреймворков меняются, но шесть паттернов - нет. Дедупликация, периодическое сжатие, валидация по схеме, сжатие с учётом ограничений, checkpointing и глобальный автоматический выключатель - это концепции, не привязанные к конкретному фреймворку. Детали LangGraph и n8n здесь - это конкретные точки опоры, а не граница применимости паттернов.
Выбор размера продакшн-развёртывания агента
Все описанные паттерны предполагают, что вы контролируете менеджер процессов, базу данных и поведение при перезапуске. Checkpointing бесполезен, если упавший цикл никогда не поднимается снова, а глобальному автоматическому выключателю нужно место для хранения общего состояния. Именно этот контроль даёт самостоятельный хостинг и не даёт управляемый чёрный ящик, поэтому последнее решение - это выбор размера машины для круглосуточной работы.
Для большинства однагентных развёртываний (один агент, LLM-вызовы к внешнему API, базовый Postgres-checkpointing) достаточно небольшого инстанса: около 2 GB RAM, 1 vCPU, and 60 GB of NVMe storageТяжёлые вычисления находятся на стороне провайдера модели; ваша машина занимается оркестровкой, checkpointing и хранением состояния, а не выполнением инференса. Переходите примерно к 4 GB RAM, 2 vCPU, and 120 GB NVMe когда агент является многошаговым с сохранением состояния через Postgres-checkpointing плюс Redis для восстановления сессий, или когда вы запускаете параллельные воркфлоу на одном хосте.
Причина, по которой здесь нужен VPS под самостоятельным управлением, а не ограниченная платформа, та же, что делает исправления работающими: нужны права root. Собственный Postgres для checkpointing, собственный Redis для состояния сессий и настоящий менеджер процессов, например systemd or pm2, - чтобы когда цикл падает, супервизор его перезапускал и восстанавливал состояние с последней контрольной точки, а не начинал задачу заново. Вся эта история восстановления зависит от владения жизненным циклом процесса.
Поскольку мы запускаем n8n как приложение в один клик в нашем собственном маркетплейсе, эта часть настройки - самый короткий путь с нашей стороны: вы можете развернуть n8n на Cloudzy VPS с конфигурацией на базе Postgres, которая нужна для продакшна, на инстансе с правами root для добавления собственного Redis и управления процессами. Это та же самостоятельная инфраструктура, описанная выше, где вы владеете базой данных и поведением при перезапуске, - именно это делает checkpointing и автовосстановление реально работающими.
Паттерны обвязки надёжны ровно настолько, насколько надёжна машина, на которой они работают. Checkpointing бесполезен, если процесс никогда не перезапускается.
Часто задаваемые вопросы
Как остановить бесконечный цикл LangGraph-агента?
Используйте два механизма вместе. Установите recursion_limit как жёсткий потолок шагов (по умолчанию 25), чтобы зациклившийся агент не мог сжечь неограниченный бюджет, и добавьте детектор отсутствия прогресса, который хеширует каждый вызов инструмента с аргументами и завершает работу при повторе в недавнем окне. Потолок сам по себе - это стопор, срабатывающий после того, как ущерб уже нанесён, а не реальная защита от зацикливания. Детектор прогресса - это то, что реально останавливает застрявший цикл.
Каково правильное значение recursion_limit для LangGraph в продакшне?
Универсального числа нет. Задайте максимальное количество легитимных шагов, которые агент когда-либо должен сделать, плюс запас, и воспринимайте его строго как страховочный стопор по затратам. Повышение лимита не помогает зациклившемуся агенту сойтись. Если агент достигает высокого лимита, решение - детектор прогресса, а не более высокий потолок.
Почему мой n8n AI-агент постоянно достигает Max Iterations?
Достижение предела Max Iterations означает, что агент не сходится: он выполняет больше шагов, чем позволяет лимит, не достигая остановки. Повышайте лимит только если задача действительно требует больше шагов; в противном случае воспринимайте это как сигнал, что агент застрял. Остерегайтесь одной конкретной ловушки: GitHub issue #22771 сообщает, что когда лимит итераций достигнут при настройке "On Error: Continue", выполнение может маршрутизироваться к выходу Success вместо Error, так что ограниченный, неудачный запуск может выглядеть как успех в вашем воркфлоу.
Как сохранять состояние агента между перезапусками?
В LangGraph используйте PostgresSaver для checkpointing вместо SqliteSaver, который предназначен для локальной разработки. В n8n используйте Postgres Chat Memory node вместо встроенной внутрипроцессной памяти. Оба требуют постоянной базы данных, а в LangGraph каждая операция checkpointing требует thread_id который связывает конкретный запуск с его сохранённым состоянием.
Что вызывает деградацию рассуждений при длительных запусках агента?
Качество рассуждений падает по мере роста контекста, даже до достижения жёсткого лимита токенов. Это эффект "потерянного в середине": модель обращает внимание на начало и конец длинного контекста, но теряет середину. Инженерный блог Anthropic называет базовый механизм "context rot": поскольку каждый токен обращает внимание на каждый другой токен, вы получаете n² попарных связей, и внимание модели становится тоньше по мере удлинения контекста. Решение - сжатие в середине цикла, которое резюмирует устаревшую историю, сохраняя закреплённые ограничения и инструкции безопасности нетронутыми.