Почему счёт растёт, когда продукт стоит на месте
Знакомая картина: месяц закрыт, счёт за API ощутимо выше предыдущего, а в продуктовом дашборде ничего не поменялось — столько же пользователей, столько же доведённых до конца сценариев. Первая гипотеза обычно техническая и почти всегда неверная: «наверное, вырос тариф». Тариф не менялся. Изменилось количество попыток за каждым ответом.
За этой ошибкой стоит привычка из классического бэкенда. Там неудачный запрос почти ничего не стоит: потратили немного процессора, получили ошибку, пошли дальше. Мы приучены считать, что неудача бесплатна. В API языковых моделей единица биллинга — не результат, а попытка: провайдер списывает за токены, которые обработал, а не за те, за которые вы получили пригодный ответ.
Отсюда арифметика. Пусть типичный вызов несёт контекст размером C — подставьте своё: системный промпт, история диалога, выборка из базы знаний, описания инструментов. Если операция проходит со второй попытки, вход оплачивается дважды: 2×C вместо C. Если попыток четыре — множитель четыре. Всё это время пользователь не получил ничего, а монитор будет думать, что всё в порядке: операция ведь завершилась успешно, просто «чуть медленнее».
Слепота здесь системная: дашборд расходов показывает токены, продуктовый дашборд — ответы, и никто не делит одно на другое. Между этими двумя числами и живёт разница — и от неё зависит, с кем разговаривать: с провайдером о тарифах или со своим кодом о попытках.
Первый ход: подкрутить политику повторов
Как только рост замечают, рука сама тянется к настройкам повторов: урезают их число, растягивают паузы, добавляют джиттер, ставят предохранитель. Рефлекс честный — ретраи настраиваются строчкой конфигурации, значит, и чинить надо рядом.
Не помогает, потому что ручка не на той оси. Настройки повторов управляют временем: когда случится следующая попытка. Деньги списываются в момент начала попытки и от пауз между ними не зависят. Можно идеально разнести повторы по календарю — и так же стабильно оплачивать каждый.
Предохранитель? Он останавливает поток неудач, и счёт перестаёт расти. Вместе с ним перестаёт работать и фича: предохранитель не отличает «дорого» от «сломано», он просто рвёт цепь. Это не лечение — выдёргивание пациента из розетки.
Бывает и второй первый ход: перевести всё на модель подешевле, «чтобы компенсировать». Арифметика может выйти обратной: дешёвый вариант чаще проваливает проверку ответа, провал запускает повтор, повтор стоит денег. Цена попытки упала, число попыток выросло — произведение само по себе не уменьшается.
Где попытки умножаются, а не складываются
Одинокий повтор скучен: удвоение входа, посчитали, вздохнули. Настоящие потери начинаются там, где попытки перемножаются. Четыре типовых механизма.
Вложенные ретраи. HTTP-клиент со своими повторами, поверх него SDK со своими, поверх фреймворк агента, повторяющий шаги, и вдогонку ваш собственный код «ну попробуем ещё разок» на случай плохого ответа. Каждый слой по отдельности безобиден и написан из лучших побуждений. Вместе они образуют умножение: если внешний слой делает A попыток и в каждой запускает внутренний с B попытками, вниз уходит A×B вызовов, а не A+B. Причём внешний слой о внутреннем не знает: для него это была одна попытка. Пара слоёв, каждый из которых в отдельности выглядел разумной осторожностью, — и вот вам «вдруг вдвое» при неизменном продукте.
Глубина агента. В многошаговом агенте контекст тянется за шагами: в запросе на шаге k лежит всё, что накопилось к этому моменту. Повтор шага заново оплачивает всю эту сумму. Повтор на последнем шаге длинной цепочки стоит примерно как вход всей цепочки — ещё раз. А если фреймворк при ошибке начинает цепочку сначала, одна неудача на финише удваивает стоимость прохода целиком. Про счётчики в агенте у нас был отдельный материал — там речь шла о бюджете времени; с деньгами механика та же, только ставки выше.
Таймаут, который убивает самое дорогое. Долгие ответы и таймауты ходят в паре: чем длиннее генерация, тем ближе она к лимиту времени на запрос. Оборванная по таймауту генерация — это токены, которые модель уже породила; списывает ли их провайдер — вопрос тарификации, проверьте у своего. Если списывает, таймаут превращается в кнопку «заплатить и не получить», а повтор — в подписку на неё. Дальше хуже: типичный API не умеет докатывать оборванную генерацию, повтор начнётся с первого токена. И под нож идут не случайные запросы, а самые длинные ответы — то есть самые дорогие. Отбор по стоимости, чистая механика, без злого умысла.
Систематический провал валидации. Модель вернула невалидный JSON — сработал повтор. Повтор помогает ровно тогда, когда провал случаен и со второй попытки модель справилась бы. Если же провал систематический — схема, которую модель не тянет, ответ, обрезанный лимитом длины, двусмысленный промпт, инструмент, который отвечает не в том формате, — каждый повтор покупает вам ту же ошибку по полной цене. Это уже не ретрай — лотерея с известной ставкой и нулевым выигрышем. Классика жанра: лимит длины обрезает JSON на полуслове, повтор обрезается ровно там же. Лечится лимитом или схемой; политика повторов тут ни при чём.
Отдельно — про основание, на которое умножается всё это. Агентские сценарии и поиск по базе знаний устроены так, что тянут за собой тяжёлый контекст: чем умнее выглядит продукт, тем больше вход в каждом вызове. Вход обычно и есть главная статья счёта. Повтор на фоне коротких промптов был бы мелочью; повтор на тяжёлом контексте — уже строка в бюджете, и умножается именно она.
Что считать и что чинить
Начните с одной цифры, код подождёт. Возьмите за период все списанные вызовы и все ответы, доведённые до пользователя, и поделите одно на другое. Получится «попыток на ответ»: единица — вы платите ровно за то, что получаете; двойка — каждый ответ обходится вдвое дороже номинала. Чтобы это посчитать, каждой логической операции нужен идентификатор, который протаскивается через все попытки, включая тихие повторы HTTP-клиента, если они у вас есть. Без идентификатора делить нечего. Тот же показатель можно взять в токенах: все входные токены за период, делённые на входные токены успешных ответов.
Дальше разложите неудачи по классам, потому что лечение у них разное:
- Транспортные ошибки — серверные сбои, лимиты частоты запросов. Повтор оправдан, бэкофф уместен. Единственный класс, где учебная политика повторов работает как написано.
- Таймауты на генерации. Чинить здесь надо таймаут: лимит времени должен соотноситься с ожидаемой длиной ответа, иначе вы платите за обрыв.
- Провалы валидации. Перед повтором спросите себя: провал случайный или систематический? Случайный — повторяйте. Систематический — правьте промпт, схему или лимит; повтор здесь просто сжигает бюджет.
И развилка, о которой часто забывают: повтор автоматический или видимый пользователю. На транспортных ошибках автоматический повтор почти всегда дешевле потерянного пользователя. На дорогих генерациях иногда разумнее отдать ошибку наружу и показать кнопку «попробовать ещё»: клик пользователя — новое решение, и платите вы за него осознанно.
Пара коротких оговорок. При потоковой выдаче плохой ответ виден до конца — генерацию можно оборвать и сэкономить на токенах, которые иначе были бы дописаны; вход уже оплачен, спасти удаётся только выход. Идемпотентность, которая спасает от повторных сайд-эффектов в классических распределённых системах, тут бессильна: сайд-эффект и есть трата, и отката у неё нет. Кэш префикса снижает цену повторного входа, но только для неизменной части контекста и по тарификации провайдера — на повторную генерацию выхода он не распространяется.
И для агентов: повторяйте шаг с усечённым контекстом; полную перезагрузку цепочки оставьте на крайний случай.
Позиция редакции
Мы считаем: повтор в системах с LLM — это покупка, а не транспортная настройка. Любая политика повторов в переводе на деньги — мультипликатор на счёт, и её автор обязан знать его значение. Поэтому «попыток на ответ» должна быть метрикой первого класса: место ей на одном дашборде с расходами, а не в отладочных логах. Пока вы не знаете свой мультипликатор, вы не знаете цену собственной надёжности — возможно, щедро переплачиваете за неё, не подозревая об этом.
Мы неправы, если после подсчёта доля повторных попыток в вашем счёте окажется пренебрежимо малой. Тогда повторы — не ваш источник роста, копать надо в другое место, и честнее всего начать с анатомии самого вызова: из чего складывается цена инференса на реальной нагрузке, мы разбирали отдельно. И оговорка про таймауты: если ваш провайдер не списывает токены за оборванные генерации, этого механизма для вас не существует — проверьте до любых выводов.
Посчитайте свою долю. Окажется около единицы — статья была не про вас, и мы забираем слова назад. Окажется выше — у вас на руках конкретная цифра, с которой можно идти к счёту и разбираться, откуда она взялась.