Суть Чтение реальных сниппетов agent-цикла — диспетч инструментов, лимиты шагов, обрезка контекста, error recovery — предскажите поведение и выберите фикс с наибольшим рычагом.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min
Agent-цикл — это место, где тратятся деньги и прячутся баги. Читайте каждый сниппет так, как читали бы его на код-ревью сервиса, который тарифицируется по токенам, и выбирайте фикс, который сеньор делает первым.
Цель
Отработать цикл, который вы прогоняете на каждом агенте: прочитать поток управления, предсказать, где он крутится вечно, переполняется или молотит (thrash), и взяться за структурный ограничитель прежде, чем винить модель.
Сниппет 1 — цикл с единственным выходом
def run_agent(task, tools): messages = [SYSTEM, {"role": "user", "content": task}] while True: resp = model(messages, tools) # THINK if not resp.tool_calls: return resp.content # единственный выход: модель перестаёт звать инструменты messages.append(resp) for call in resp.tool_calls: result = dispatch(call, tools) # ACT messages.append(result) # OBSERVE
Викторина
Completed
Это корректный ReAct, но небезопасный для продакшена. Какой один ограничитель важнее всего добавить и почему именно его первым?
Heads-up Обработка ошибок важна, но вторична — а отдача ошибок назад без лимита сама может вызвать бесконечный retry. Первый дефект в том, что ничто вообще не ограничивает число итераций.
Heads-up Кэш срезает стоимость, но не останавливает runaway-цикл; агент, молотящий новые-но-бесполезные состояния, всё равно крутится вечно. Нужен жёсткий предел на итерации и траты.
Heads-up Стриминг меняет, как приходит вывод, а не то, завершается ли цикл. Не хватает ограничителя termination, а не смены транспорта.
Сниппет 2 — лимит шагов, прячущий баг
for step in range(MAX_STEPS): # MAX_STEPS = 100 resp = model(messages, tools) if not resp.tool_calls: return resp.content messages.append(resp) for call in resp.tool_calls: messages.append(dispatch(call, tools))# вывалились из цикла: лимит достигнутreturn "Извините, не удалось это выполнить."
Викторина
Completed
Телеметрия показывает, что ~30% прогонов вываливаются и возвращают извинение. Лимит делает свою работу — какой вывод делает сеньор?
Heads-up Поднятие лимита позволит квадратичному циклу сжечь куда больше токенов до срабатывания, а цикл, которому нужны сотни шагов, обычно потерял нить. Лечат причину сбоя, а не дают больше верёвки.
Heads-up Вернуть частичный результат бывает разумно, но это не объясняет, почему треть прогонов не может завершиться. Столь частое срабатывание лимита указывает на структурный дефект выше.
Heads-up 30% упоров в лимит — громкий сигнал thrashing или сломанного инструмента, а не базовая норма. Лимит это маскирует; нужна диагностика по трейсам.
Сниппет 3 — обрезка контекста
def trim(messages, budget=8000): # держим самые свежие сообщения, пока укладываемся в бюджет токенов kept = [] total = 0 for m in reversed(messages): total += count_tokens(m) if total > budget: break kept.append(m) return list(reversed(kept))
Викторина
Completed
Это держит цикл под окном, но на долгих задачах всплывает один режим отказа. Какой и каков фикс?
Heads-up Приблизительный счёт нормален с запасом — это не структурный баг. Реальный дефект в том, что безусловное «держим самые свежие» выселяет инструкции, от которых зависит задача.
Heads-up Два разворота — это O(n) и пренебрежимо рядом с вызовами модели. Содержательный баг — что именно выселяется (закреплённый контекст), а не механика списка.
Heads-up Держать только старые — значит отбросить свежие результаты инструментов, нужные модели прямо сейчас. Верный ответ — закрепить инструкции и обрезать/суммаризировать середину, а не менять конец, который держим.
Сниппет 4 — error recovery
for step in range(MAX_STEPS): resp = model(messages, tools) if not resp.tool_calls: return resp.content messages.append(resp) for call in resp.tool_calls: try: result = dispatch(call, tools) except ToolError as e: result = {"role": "tool", "content": f"Error: {e}"} # отдаём ошибку назад messages.append(result)
Викторина
Completed
Отдача ошибки назад позволяет модели адаптироваться — но нагрузочный тест показывает прогоны, где один и тот же инструмент падает идентично десятки раз. Какой ограничитель закрывает брешь?
Heads-up Ловля большего числа исключений делает цикл устойчивее к падению, но склоннее к thrashing — он бесконечно отдаёт сбои назад. Не хватает лимита на идентичные/неудачные retry.
Heads-up Backoff помогает при транзиентных/rate-limit ошибках, но для детерминированного сбоя (плохие аргументы, не найдено) он лишь оттягивает тот же идентичный сбой. На уровне цикла всё равно нужен dedup/лимит retry.
Heads-up Падение выбрасывает способность модели восстановиться из исправимых ошибок. Цель — ограниченный recovery (отдавать назад, но с лимитом идентичных retry), а не отсутствие recovery.
Итог
Любой инцидент агента читается в цикле: единственный выход «модель остановилась» небезопасен, поэтому добавляют жёсткий лимит шагов плюс бюджет по wall-clock/токенам; лимит, срабатывающий регулярно, — это ремень, ловящий реальный баг, а не повод его ослабить; наивная обрезка контекста выселяет закреплённые системные/задачные сообщения и заставляет агента забыть задачу, поэтому инструкции закрепляют, а середину суммаризируют; а error-feedback без лимита retry/dedup на инструмент превращает recovery в бесконечный цикл из валидных вызовов. Прочитайте поток управления, найдите неограниченный путь, добавьте структурный ограничитель и перепрогоните под нагрузкой для подтверждения.