Где кончается агент
Когда агент зависает, рука тянется к таймауту. Когда крутится на месте — к лимиту шагов. Обе настройки выглядят параметрами в конфиге, но каждая — утверждение о том, что именно вы ограничиваете. И ограничивают они разное.
Сам цикл прост: собрать контекст, вызвать модель, исполнить инструмент, дописать результат в историю, вызвать модель снова. Выхода у цикла три: модель сказала «готово», кончился счётчик шагов, кончилось время. Первый — замысел, остальные два — страховка. Беда в том, что страховки страхуют друг от друга плохо: шаги и время — ресурсы разной природы, и лимит на один ничего не гарантирует по другому.
Шаги дискретны, их видно на счётчике. Время непрерывно и утекает между счётчиками: внутри шага, внутри вызова инструмента, внутри ожидания ответа. Вопрос «какой поставить таймаут» — на самом деле вопрос «какой ресурс я готов потерять первым».
Почему лимит шагов не ограничивает время
Шаг не атомарен по времени. Внутри одного шага живут два процесса несопоставимой цены: вызов модели и вызов инструмента. Вызов модели тянется тем дольше, чем больше контекста вы ей отдаёте, и ограничен сверху разве что вашим терпением. Вызов инструмента не ограничен вообще ничем: subprocess, поиск по кодовой базе, внешний сервис, вложенный агент — любой из них может прожить дольше всего остального прогона. Значит, лимит шагов ничего не обещает по времени: прогон из десятка шагов, где один — тяжёлый поиск, живёт дольше, чем прогон из сотни дешёвых.
Обратное тоже верно: время не ограничивает работу. Агент в цикле быстрых дешёвых шагов сжигает счётчик, почти не тронув часы; агент из немногих тяжёлых шагов сжигает часы, не тронув счётчик. Один бюджет не ловит отказ, который ловит другой.
Прежде чем вообще считать, договоритесь о единице учёта. Что такое шаг в вашем цикле — один вызов модели, один вызов инструмента, одна итерация целиком? В разных каркасах счётчики считают разное, и лимит, переписанный из чужого конфига, ограничивает не то, что вы думали. Бюджет без определённой единицы учёта — число без смысла.
Арифметика слоёв добавляет неприятное. Если у каждого шага свой таймаут, худший случай прогона — лимит шагов, умноженный на таймаут шага; в очередь с таким запасом не встроишься. Если внешний таймаут поставить под то, что готов ждать заказчик, он начнёт срабатывать посреди прогона, когда ни один внутренний ещё не тикнул. Слои не знают друг о друге, и каждый считает, что за время отвечает кто-то другой.
Сверху садятся повторы. Ретрай внутри инструмента, ретрай на уровне шага, ретрай на уровне оркестратора — каждый слой, умеющий повторять, умножает бюджет всего, что под ним. Счётчик при этом врёт в обе стороны: шаг с внутренним ретраем в счётчике один, по часам — несколько. Лимит шагов, сверенный с часами на тестах, разъезжается с ними ровно тогда, когда появляются повторы. Этому счёту у нас посвящён отдельный разбор — «Повторы, которые удваивают счёт».
Тупик: один таймаут на всё
Первое решение не требует менять код цикла: обернуть весь прогон одним внешним таймаутом. Кажется, что вопрос закрыт — зависший агент не зависнет навсегда. Гарантия действительно есть. Вот что она гарантирует:
- Убийство в случайной точке. Таймер не знает о шагах. Он сработает посреди вызова инструмента — посреди записи файла, посреди транзакции, посреди вложенного прогона. Состояние после этого не «ошибка», а «неизвестно»: часть работы сделана, часть нет, и нигде не записано, какая часть.
- Прогресс и цикл для него неотличимы. Прогон, которому осталось два шага, и прогон, который ходит по кругу с первого, для внешнего таймаута одинаковы. Он с одинаковой уверенностью убивает медленный успех и мёртвый цикл. Таймаут не спасает от зависания — он лишь назначает ему дату.
- Диагноза нет. После срабатывания известно одно: времени не хватило. Шагов было мало или много? Висел инструмент или долго отвечала модель? Ответа нет: таймаут живёт вне цикла и его внутренностей не видит.
- Побочные эффекты переживают убийство. Остановка вашего процесса не останавливает то, что вы успели запустить на другой стороне; убитый прогон может ещё дописывать данные, когда вы уже записали его в неудавшиеся.
Симметричная половина тупика — задрать лимит шагов так, чтобы он «никогда не срабатывал». Видимый отказ исчезает, остаётся невидимый: вечный прогон, который ест деньги и слоты очереди и ни разу не сообщает о себе. Лимит, который не срабатывает, — это отсутствие лимита.
Бюджет как проверка перед шагом
Рабочая схема переносит бюджет внутрь цикла. Не «убей через столько-то», а «не начинай то, что не влезает». Три части:
Оценка цены шага. Перед запуском шага известно, какой вызов модель собирается сделать, а для вызова известна худшая цена: таймаут инструмента или заявленная стоимость обращения к модели. Инструмент, который не умеет назвать свою худшую цену, — дыра в бюджете: либо дайте ему собственный таймаут, либо не пускайте его в прогон с общим бюджетом.
Сравнение с остатком. Если остаток бюджета меньше худшей цены шага, шаг не начинается. Решение принимается до того, как начата работа, — в этом всё отличие от таймера, который стреляет после. И проверка должна жить в коде цикла, а не в промпте: просить модель «следить за временем и не затягивать» не работает по устройству — модель не знает, сколько прошло, а посреди длинного прогона она и про инструкцию забывает. Бюджет — это то, что агент не может обойти.
Мягкое завершение. Вместо убийства — сообщение агенту: бюджет на исходе, закончи. Модель умеет заканчивать: выдать готовое, перечислить несделанное, оставить план продолжения. Частичный результат с планом дороже трупа. Лимит шагов устроен так же: сработал — дайте модели последний ход, чтобы она закрыла рассуждение, а не обрывайте сессию молча.
Как считать бюджет, если готовых чисел нет — есть способ. Худший по времени инструмент из тех, что разрешены агенту, умножить на лимит шагов: это честный худший случай одного прохода. Прибавить запас на повторы — столько, сколько раз в худшем случае может повториться самый дорогой шаг. Получился таймаут очереди. Если он не влезает в то, что готов ждать заказчик прогона, значит не влезает — и чинить надо не таймаут, а лимит шагов или состав инструментов. Таймаут — производная от остальных решений, а не самостоятельная настройка.
Чекпоинты сюда же: снимайте состояние после каждого закрытого шага, и остановка по бюджету будет стоить один шаг, а не весь прогон. Перезапуск с нуля — повтор, который вы устраиваете сами, и он умножает счёт как любой другой ретрай.
Дальше — иерархия таймаутов, и правило одно: срабатывать должен самый внутренний, за которым стоит код, умеющий прибрать за собой. Если внешний таймаут очереди короче внутреннего таймаута инструмента, вы получите родителя, убитого посреди работы ребёнка, — и ребёнка, продолжающего работать в сиротстве. Внутренние короче внешних, и каждый слой знает, что делать при своём срабатывании.
И про петли. Если состояние мира между шагами не меняется, а счётчик растёт, вы в цикле — и ждать исчерпания бюджета незачем. Признак «ничего не изменилось» дешевле и точнее грубого таймера: он отличает зависание от медленного успеха, чего таймауту недоступно.
Что ломается позже
Бюджет, подобранный на коротких задачах, не годится для длинных. На тестовых сценариях агент укладывается в лимит; в реальной работе задачи длиннее и неровнее, и отказ переезжает из разряда «настройки» в разряд «продакшен». Плывут не одни задачи: тяжелеют инструменты, меняется поведение модели на том же промпте, растёт контекст — худший случай уползает сам, без вашего участия. Это та же серия отказов, что в материале «Агенты в продакшене: что отказывает на второй неделе»: то, что не срабатывает в первый день, потому что в первый день никто не гоняет агента впритык к бюджету. Худший случай пересчитывают, когда меняется состав инструментов или длина задач, — иначе его пересчитает за вас очередь.
Позиция редакции
Мы считаем, что бюджет времени — это проверка внутри цикла: перед каждым шагом сравнить остаток с худшей ценой шага, не начинать то, что не влезает, завершать мягко, с частичным результатом и планом продолжения. Внешний таймаут при этом остаётся, но как предохранитель последней линии — на случай, если сама проверка зависла, — а не как основной механизм.
Мы неправы, если каждый шаг агента сам по себе ограничен и дёшев: инструмент не может работать дольше собственного таймаута, а вызов модели не тянет бюджет. Тогда внутренняя проверка — бюрократия поверх уже решённой задачи, и внешнего предохранителя достаточно. Второе условие: если прогоны почти ничего не стоят и их можно убивать и перезапускать без последствий, убивать дешевле, чем честно завершать. Пока инструменты у агента произвольные, а прогоны не бесплатны, позиция стоит.