Алерты о бюджете не работают: психология дефицита и код. Узнайте, как резервирование средств до вызова API спасает от перерасхода. Действуйте сейчас!
Вы получили письмо: «Использовано 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 раза.Общая проблема: 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 — открытая Python-библиотека, оборачивающая это в декоратор вокруг вашего LLM-клиента. pip install baar-core, установите лимит — и она вернёт 402 до обращения к провайдеру, а не после списания токенов. Атомарное резервирование — это то, что я бы сделал неправильно сам, и именно оно останавливает конкурентные вызовы от совместного превышения лимита, который ни один из них не превысил по отдельности.
noburn.dev — это продукт для команд, построенный на этой основе: та же предварительная блокировка API-вызова до его выполнения, когда пользователь превысил бюджет, плюс индивидуальные лимиты и панель учёта.
if-выражения, разбросанные по семи местам, где восьмое (добавленное в спешке) не имеет защиты.Почему это важнее, чем кажется: алерты построены на предположении, что информация меняет поведение. Но исследования сигналов дефицита говорят, что информация меняет в основном ваше повествование о поведении. Я не проигнорировал письмо о 84%. Я прочитал, понял и всё равно сделал, объясняя себе, что этот запуск особенный. Каждый в вашей команде поступит так же, включая того, кто написал алерт.
Перестаньте отправлять предупреждения там, где нужна стена. Начните с резервирования бюджета до вызова — и вы не увидите счёт на $1,900.
Что сделать прямо сейчас: возьмите свой текущий код с алертом о бюджете и замените проверку после факта на резервирование с FOR UPDATE перед вызовом API. Если используете Python, попробуйте pip install baar-core и оберните клиент. Убедитесь, что при превышении лимита выбрасывается 402, а не просто пишется лог.
Хочешь закрепить знания на практике?
Решай задачи на Algolit — интерактивная платформа для обучения
Начать бесплатно →