ГлавнаяБлогПочему алерт о бюджете не остановит перерасход: резервируйте до вызова
Python

Почему алерт о бюджете не остановит перерасход: резервируйте до вызова

Алерты о бюджете не работают: психология дефицита и код. Узнайте, как резервирование средств до вызова API спасает от перерасхода. Действуйте сейчас!

Al
Редакция Algolitalgolit.ru
7 мин чтения29 августа 2026 г.

Почему предупреждение о бюджете не спасает от перерасхода

Вы получили письмо: «Использовано 84% месячного бюджета». Прочитали, переслали себе с пометкой «следить». И всё равно запустили пакетное задание на ночь. К утру счёт ушёл в минус на $1,900 при лимите $600. Ничего не сломалось: система оповещения сработала точно по спецификации — вовремя, нужному человеку, который всё понял. Это не история о дисциплине. Это история о том, как устроен человеческий мозг и почему алерты о бюджете не работают.

Психология дефицита: почему предупреждение усиливает желание тратить

Предупреждение о нехватке ресурса — это сигнал дефицита, а сигналы дефицита не вызывают осторожности. Наоборот, они повышают воспринимаемую ценность ресурса и подталкивают к его потреблению. Классический эксперимент 1975 года (Worchel, Lee, Adewole) показал: печенье из банки с двумя штуками оценивалось выше, чем из банки с десятью. Даже когда участникам объясняли, что печенья мало, потому что их берут другие, эффект не исчезал — наоборот, ценность росла ещё больше.

Ваш баннер «Осталось 16% бюджета» — структурно то же самое: сигнал дефицита с полным объяснением. Вы знаете, что порог настроен кем-то, что лимит произволен. Но знание меняет только то, что вы говорите о баннере, а не то, что вы делаете — обычно запускаете задачу.

Как выглядит мягкий лимит в коде: типичная ошибка

Вот версия, которую пишут почти все (на каком-нибудь диалекте):

spent = usage_store.month_to_date(user_id)
if spent > BUDGET * 0.9:
    log.warning("user %s at %.0f%% of budget", user_id, spent / BUDGET * 100)
    metrics.incr("budget.near_limit")

response = client.messages.create(
    model="claude-sonnet-5",
    max_tokens=4096,
    messages=messages,
)
usage_store.record(user_id, response.usage)  # учёт после выполнения

Каждая строка защитима на код-ревью. Но этот код проваливается тремя конкретными способами:

  • Учёт всегда отстаёт ровно на те вызовы, которые важны. record() выполняется после ответа. Во время всплеска трафика траты, которые нужно видеть больше всего, ещё не зафиксированы.
  • Конкурентность читает одно и то же устаревшее значение. Двенадцать воркеров вызывают month_to_date в одну секунду, все видят 92%, все проходят проверку, все отправляют запросы. Ни один не превысил лимит, но вместе они превысили его в 4 раза.
  • Агентные циклы работают быстрее любого агрегатора. Инструментальный агент, который натыкается на битую схему и ретраит, может сделать 40 вызовов за 90 секунд. Если агрегация запускается раз в минуту, цикл завершится раньше, чем график обновится. Мои $1,900 — это в основном ретрай-цикл без потолка, который раздувал контекст с каждой итерацией.

Общая проблема: if ничего не ограничивает. Он лишь наблюдает и уходит с дороги. Это очень дорогая строка лога.

Решение: резервируйте бюджет до вызова

Исправление скучное, но проверенное базами данных: не проверяйте баланс, а резервируйте средства.

class BudgetExceeded(Exception):
    status_code = 402

def reserve(user_id: str, estimated_cost: float) -> Reservation:
    with db.transaction():
        row = db.query(
            "SELECT spent, reserved, cap FROM budgets "
            "WHERE user_id = %s FOR UPDATE",
            user_id,
        ).one()
        if row.spent + row.reserved + estimated_cost > row.cap:
            raise BudgetExceeded(
                f"{user_id}: {row.spent + row.reserved:.2f} committed, "
                f"needs {estimated_cost:.2f}, cap {row.cap:.2f}"
            )
        db.execute(
            "UPDATE budgets SET reserved = reserved + %s WHERE user_id = %s",
            estimated_cost,
            user_id,
        )
        return Reservation(user_id, estimated_cost)

Точка вызова становится: сначала удержание, потом списание:

estimate = price_of(model, count_tokens(messages), max_tokens)
hold = reserve(user_id, estimate)  # бросает 402 до сетевого ввода-вывода
try:
    response = client.messages.create(...)
    hold.commit(actual_cost(response.usage))
except Exception:
    hold.release()
    raise

Две детали имеют решающее значение:

  • FOR UPDATE заставляет двенадцать конкурентных воркеров сериализоваться, а не читать одно и то же устаревшее значение. Это убивает гонку, которую мягкие проверки не видят.
  • Оценка — это верхняя граница, вычисленная из входных токенов плюс max_tokens. Вы резервируете худший случай, а при коммите возвращаете разницу. Перерезервирование — это погрешность округления. Недоезерв — это ваш «четверг».

Готовые инструменты: baar-core и noburn.dev

Многое из этого уже реализовано. baar-core — открытая Python-библиотека, оборачивающая это в декоратор вокруг вашего LLM-клиента. pip install baar-core, установите лимит — и она вернёт 402 до обращения к провайдеру, а не после списания токенов. Атомарное резервирование — это то, что я бы сделал неправильно сам, и именно оно останавливает конкурентные вызовы от совместного превышения лимита, который ни один из них не превысил по отдельности.

noburn.dev — это продукт для команд, построенный на этой основе: та же предварительная блокировка API-вызова до его выполнения, когда пользователь превысил бюджет, плюс индивидуальные лимиты и панель учёта.

Три правила, которые выдержат реальный инцидент

  1. Ограничивайте в одной точке. Один обёртка вокруг клиента, а не if-выражения, разбросанные по семи местам, где восьмое (добавленное в спешке) не имеет защиты.
  2. Резервируйте, а не наблюдайте. Если ваш лимит читает число, за обновление которого отвечает другой процесс, — это не лимит, а запаздывающий индикатор в костюме лимита.
  3. Падайте типизированно и громко. Возвращайте настоящий 402 с машиночитаемой причиной, чтобы вызывающие, ретрай-мидлвары и агентные циклы могли отличить «бюджет исчерпан» от «временной 500» и остановиться, а не отступать и пробовать снова. Строка лога — это совет. Исключение — это стена.

Практический вывод

Почему это важнее, чем кажется: алерты построены на предположении, что информация меняет поведение. Но исследования сигналов дефицита говорят, что информация меняет в основном ваше повествование о поведении. Я не проигнорировал письмо о 84%. Я прочитал, понял и всё равно сделал, объясняя себе, что этот запуск особенный. Каждый в вашей команде поступит так же, включая того, кто написал алерт.

Перестаньте отправлять предупреждения там, где нужна стена. Начните с резервирования бюджета до вызова — и вы не увидите счёт на $1,900.

Что сделать прямо сейчас: возьмите свой текущий код с алертом о бюджете и замените проверку после факта на резервирование с FOR UPDATE перед вызовом API. Если используете Python, попробуйте pip install baar-core и оберните клиент. Убедитесь, что при превышении лимита выбрасывается 402, а не просто пишется лог.

#бюджетирование#LLM API#резервирование#алерты#конкурентность
Al
Редакция Algolit

Пишем про алгоритмы, подготовку к собеседованиям и карьеру в IT — так, чтобы было понятно и полезно.

Хочешь закрепить знания на практике?

Решай задачи на Algolit — интерактивная платформа для обучения

Начать бесплатно →