Посчитать стоимость в главе 43 недостаточно. Система должна физически останавливаться у заданной границы. Бюджет в Google Doc ничего не ограничивает; бюджет в коде отклоняет или упрощает слишком дорогой запрос.
Ситуация из реальной команды
Команда посчитала стоимость честно: $1200 в месяц на триаж тикетов. Через три недели фактический расход: $4100. Расследование:
- один корпоративный клиент стал присылать тикеты с полной перепиской на 30 страниц во вложении: «вход» вырос в 20 раз, и никто его не резал;
- баг в retry-логике: при таймауте провайдера задача ретраилась без лимита, ночью инцидента набежало 40 000 лишних вызовов;
- разработчик «временно» переключил шаг на дорогую модель для отладки и забыл вернуть.
Все три проблемы объединяет одно: у системы не было границы, на которую она могла бы наткнуться. Бюджет жил в Google Doc, а не в коде.
Бюджет как конфигурация use case
У каждого use case должен быть явный, версионируемый бюджет:
use_case: support_ticket_triage
max_input_tokens: 3000 # вход режется или отклоняется, а не «как получится»
max_output_tokens: 500
max_pipeline_steps: 3
max_retries_per_step: 1
max_cost_per_event_usd: 0.015 # превышение = ошибка, а не пожатие плечами
daily_budget_usd: 60
monthly_budget_usd: 1200
on_budget_exceeded: degrade_to_rules_and_manual_queueКаждое поле: это ответ на конкретный инцидент из будущего:
max_input_tokensловит 30-страничный тикет: вход либо суммируется дешёвой моделью, либо режется по осмысленной стратегии, либо честно уходит в ручную очередь;max_retries_per_stepловит ночной шторм ретраев;max_cost_per_event_usdловит и «забытую дорогую модель», и распухший контекст: это интегральная защита, которая срабатывает независимо от причины;daily_budget_usdограничивает ущерб любого неучтённого сценария одним днём.
Enforcement: три уровня
Уровень вызова. Клиент из главы 4 проверяет лимиты до отправки запроса и записывает фактический расход после:
class BudgetExceededError(LLMError): ...
async def guarded_call(self, budget: UseCaseBudget, **kwargs) -> LLMResult:
est_input = count_tokens(kwargs["system"] + kwargs["user"])
if est_input > budget.max_input_tokens:
raise BudgetExceededError(f"input {est_input} > {budget.max_input_tokens}")
result = await self.generate_text(
max_output_tokens=budget.max_output_tokens, **kwargs
)
cost = estimate_cost(result.usage, result.model)
if cost > budget.max_cost_per_event_usd:
# событие уже случилось: платим, но алертим и, при повторе, отключаем
report_budget_anomaly(budget.use_case, cost)
return resultУровень события. Пайплайн ведёт накопительный счётчик стоимости business event и останавливается при превышении: с сохранением состояния и передачей в fallback, а не молчаливой обрезкой качества.
Уровень периода. Ежедневный расход по use case агрегируется из llm_call_log. Превышение дневного бюджета переключает use case в деградированный режим: правила + ручная очередь, дешёвая модель, отложенная обработка. Что именно: решается при проектировании, а не во время инцидента.
Деградация: это фича, которую надо спроектировать
on_budget_exceeded: самое важное поле конфига. Варианты, от мягкого к жёсткому:
- переключиться на дешёвую модель;
- отключить необязательные шаги пайплайна (оценку тона: да, классификацию: нет);
- обрабатывать отложенно, ночным batch-ом;
- уйти в правила и ручную очередь;
- честно показать пользователю «функция временно недоступна».
Худший вариант всегда один: продолжать тратить, потому что «не хотелось усложнять».
Бюджет дисциплинирует архитектуру
Неожиданный побочный эффект: token budget работает как архитектурное ревью. Если use case не влезает в разумный бюджет, это почти всегда значит, что:
- в контекст едет лишнее (глава 18);
- шаг делает работу, которую должен делать код;
- большая модель стоит там, где хватит маленькой (глава 45);
- один prompt пытается решить четыре задачи (глава 13).
Если у AI-фичи нет бюджета, бюджетом станет всё, что она сможет съесть.
Правило главы
Token budget — конфигурация с enforcement на трёх уровнях: вызов, событие, период. Система должна упираться в границу, заданную кодом, и деградировать по заранее спроектированному сценарию, а не по фантазии дежурного в три часа ночи.
Чеклист главы
- У каждого use case есть явный бюджет: токены, шаги, retry, стоимость события, день, месяц.
- Вход ограничивается до отправки в модель; стратегия для превышения выбрана осознанно.
- Retry ограничены на уровне шага; шторм ретраев математически невозможен.
- Дневной расход агрегируется и сравнивается с лимитом автоматически.
- Сценарий деградации спроектирован и хотя бы раз протестирован руками.
- Превышение бюджета события алертится, повторные превышения отключают use case.
Практикум
Задача 44.1: бюджет с enforcement. ⭑⭑
Добавьте в сервис из задачи 24.1 конфиг UseCaseBudget и все три уровня enforcement: проверка входа до вызова, накопительная стоимость события, дневной лимит из llm_call_log.
Критерии приёмки:
- Тикет с текстом на 50 000 токенов не доходит до модели и попадает в ручную очередь с понятным статусом.
- При искусственно заниженном
daily_budget_usdсервис переходит в деградированный режим, и это видно в ответах API и в логе. - Возврат в нормальный режим происходит автоматически (новый день / поднятый лимит), без ручного рестарта.
- Бюджеты лежат в конфигурации (файл/таблица), меняются без деплоя, изменения логируются.
Подсказки:
- Дневной счётчик из PostgreSQL: это один SUM с индексом по
(use_case, created_at). Не тащите Redis, пока запросов меньше тысяч в минуту. - Проверку «а сработает ли» делайте тестом с замоканным клиентом: не жгите реальные токены на проверку лимитов.
Задача 44.2: учения по деградации. ⭑⭑⭑
Проведите «пожарные учения» для своего сервиса: напишите нагрузочный скрипт, который воспроизводит все три инцидента из начала главы (гигантский вход, шторм ретраев через мок падающего провайдера, подмена модели на дорогую).
Критерии приёмки:
- Все три сценария запускаются одной командой и завершаются без ручного вмешательства.
- По каждому: система ограничила ущерб, событие видно в логе/метриках, понятно, какой лимит сработал.
- Посчитан «предотвращённый ущерб»: сколько стоил бы каждый инцидент без лимитов при суточной длительности.
- Написан короткий runbook: как дежурному понять, что use case деградировал, и что делать.
Подсказки:
- Сценарий «забытая дорогая модель» ловится только интегральным лимитом
max_cost_per_event_usd: убедитесь, что он срабатывает, даже когда все остальные лимиты соблюдены.