Заметка

Агентный цикл под durable-оркестратором: где проходит граница

Агентный граф и движок durable execution — оба оркестраторы. Месяц на стенде показал, где резать границу между ними и сколько это стоит.

Куда девать цикл агента, если движок durable execution в системе уже есть? Развилка выглядит как выбор инструмента — агентный граф против движка, — а на деле оба класса инструментов оркестраторы: у каждого свой цикл, своё состояние, свои ретраи. Вопрос не «какой лучше», а где между ними провести границу и сколько эта граница стоит.

Ниже — что показал месяц работы стенда, где такая граница проведена: Temporal на Python, шлюз к моделям, трассировка вызовов, семь сценариев, включая агента-разработчика и автоматическое SRE-расследование.

Тезис, с которого всё началось

Правило приняли в первый день: один вызов модели — одна activity, граф принадлежит движку durable execution, а не агентному фреймворку.

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

Честная оговорка: это рассуждение, а не измерение. Альтернативу на стенде не строили, поэтому месяц проверил не сам тезис, а цену принятого решения. Вот она и есть данные.

Чего утверждать нельзя

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

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

Официальная интеграция движка с агентным SDK решает ровно это: цикл агента, выбор инструментов и передачи управления живут в workflow, а каждый отдельный вызов модели исполняется как activity — и потому не повторяется при реплее. Агентный фреймворк, запертый в одной activity, — плохо. Агентный цикл под оркестратором с ходом-на-activity — штатная поддерживаемая конструкция. Наивная формулировка тезиса это различие стирает.

Три уровня ретрая, которые нельзя складывать

Практика свелась к жёсткому разделению профилей activity, и разница между ними принципиальная.

ПрофильТаймаутПопытокПочему так
код: валидация, рендер, запись20–30 с3отказ дёшев и обычно транзиентный
вызов модели180–240 с1отказ не транзиентный, стоит денег
эффект в продеиз политики1тихий реплей мутации — решение, не умолчание

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

Эффект стоит отдельного слова. Он идемпотентен по ключу, и всё равно не ретраится инфраструктурой: идемпотентность делает повтор безопасным, но не делает его желаемым. Дедлайн у такой activity берётся из файла политики, а не из константы в коде, — чтобы записанный в политике таймаут был настоящим.

Граница агентности

Формула, к которой стенд пришёл: свобода внутри эпизода, детерминизм между эпизодами.

Единственное место, где модель решает, что делать дальше, — дочерний эпизод. Он выбирает, какие файлы открыть и в каком порядке, возвращает типизированное предложение и умирает. За его пределами не меняется ничего: снапшот, бюджет попыток, применение результата и решение о следующем заходе принадлежат родителю. У ребёнка нет ни write-инструмента, ни shell, а read-инструменты резолвят каждый путь от модели и отказывают, если он ведёт наружу.

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

Предел ходов нужен ещё по одной причине, которую лучше знать заранее. Модель, перечитывающая один и тот же файл, без ограничения потратит весь бюджет и не сдвинется. Так ведёт себя reasoning-модель, если её собственную историю переписывают у неё под ногами — например, санитайзер, токенизирующий пути в прошлых вызовах инструментов.

Отказ, которого не видит ни один слой

Главная находка стенда и причина, по которой «завернуть и забыть» не работает.

Генерация, завершившаяся ошибкой на стороне провайдера, приходит как HTTP 200 без контента и без вызова инструмента. Она сжигает десятки тысяч токенов на выходе и не несёт заголовка с ценой — счёт за неё не выставляют.

СлойНа что реагируетПочему молчит
fallback-цепочка шлюзаHTTP-ошибкаответ 200
ретрай activity оркестратораисключениеисключения нет
агентный SDKнекорректный ответответ корректен, просто пуст

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

Детектор пришлось строить по распарсенному ответу, а не по HTTP-телу: вычитать тело в хуке транспорта — значит сломать разбор в самом SDK. К моменту, когда SDK отдаёт результат, причина завершения уже потеряна, и остаётся проверять следствие: ход не произвёл ничего. Границы проверки важны. «Нет вызова инструмента» неверно для агентов, отвечающих прозой; «нулевой расход токенов» назвал бы отказом законно пустое завершение.

Замер одного прогона на дешёвом тире: 167 ходов, из них 97 вернулись без цены. Повтор запроса той же моделью спас 0 из 36 — ни разу. Сильный тир ответил все 36, ни одного пустого. Доля отвеченных случаев поднялась с 79 до 91 процента. Цена хода между тирами отличается примерно всемеро.

Что здесь важнее чисел: фикс обходит отказ, а не устраняет его. Дешёвый тир по-прежнему выдаёт пустой ход больше чем на половине вызовов. Третий путь — менять не тир, а сам запрос: меньшая схема инструментов, ослабленное требование обязательно вызвать инструмент. На стенде его никто не измерил, и он остаётся гипотезой.

Цена: шаг нельзя удалить

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

На стенде это встретилось при миграции метрик: две activity потеряли смысл, потому что данные стали писаться в durable-таблицу напрямую, — и остались в коде пустыми заглушками с комментарием «имя и сигнатура сохранены намеренно». Метрика не стоит слома совместимости, а удалить её вызов нельзя.

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

Когда что выбирать

СитуацияЧто братьЧто вы принимаете
пайплайн уже под движком durable executionход модели = activity, цикл в workflowшаг нельзя удалить; сигнатуры живут вечно
автономный агент, шаги дёшевы и обратимыагентный граф со своим персистомузлы после точки восстановления переигрываются вместе с вызовами моделей
есть эффекты во внешнем миредвижок с явными профилями ретраябольше кода вокруг каждого шага, зато ретрай под контролем
прототип, цена ошибки — только времяни то ни другое, простой цикл в кодевосстановления нет; переписывать при первом же проде

Чего этот опыт не доказывает

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

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

© 2026 axyi.ru · CC BY 4.0