Перейти к содержимому
PD
Разработка SaaS 8 мин чтения

Вы не знаете, сколько вам стоит каждый клиент: атрибуция AI-расходов в SaaS

Практическая схема учёта: events по использованию, денежный ledger, showback против chargeback и алармы по марже, которые находят убыточного клиента раньше, чем это сделает счёт от вендора.

PD

Pavel Duglas

AI Automation & MVP Architect

Почти каждый AI SaaS, который я аудировал за последний год, имел одну и ту же слепую зону. Основатель знает выручку с точностью до копейки. Спрашиваю: сколько стоит обслуживать десять крупнейших аккаунтов? В ответ пожатие плечами и скриншот одного счёта от OpenAI. Это не проблема бухгалтерии. Это продуктовая проблема, потому что нельзя ни ценообразовать, ни ограничить лимитами, ни выгнать клиента, которого вы не измеряете.

Ниже схема, которую я обычно ставлю за два-три дня работы, ещё до того как трогать оптимизацию промптов и роутинг моделей. Резать расходы без атрибуции - это гадание. Сначала атрибуция, потом оптимизация.

Вопрос, на который вы сегодня не ответите

Простой тест. Возьмите пять крупнейших аккаунтов и ответьте по прошлому месяцу:

  • Сколько ушло на inference по каждому аккаунту, в разбивке по моделям.
  • Какая gross margin по аккаунту после inference, хранения и сторонних API.
  • Какая фича самая дорогая на единицу использования.
  • Сколько денег сгорело на запросах, которые упали и были повторены.
  • Сколько вам стоил free tier.

Если на любой из этих вопросов нужно больше одного SQL-запроса, вы летите по приборам, которых нет. И AI-расходы ведут себя не как серверные. Серверы масштабируются от количества пользователей. LLM-расходы масштабируются от поведения пользователей, а оно разлетается на порядки. Один энтузиаст, который грузит в ваш суммаризатор 60-страничные PDF, стоит в 200 раз дороже медианного пользователя на том же тарифе за 49 долларов. Это не усредняется. Это концентрируется.

Метрить на границе, а не внутри логики

Самая частая ошибка: логирование использования размазано по бизнес-логике, по десятку мест вызова. Через шесть недель половина из них не передаёт tenant id, и цифры не сходятся ни с чем.

Сделайте одну обёртку вокруг любого платного внешнего вызова. Не только вокруг LLM. Вокруг всего, у чего есть цена: транскрибация, эмбеддинги, поисковые API, прокси для парсинга, OCR, генерация картинок. Эта обёртка - единственное место в коде, которому можно разговаривать с SDK вендора, и она всегда отдаёт usage event.

// единственная функция, которой разрешено звонить платному вендору
async function meteredCall<T>(ctx: CallContext, fn: () => Promise<VendorResult<T>>) {
  const startedAt = Date.now();
  try {
    const res = await fn();
    await usage.emit({
      request_id: ctx.requestId,        // ключ идемпотентности
      tenant_id: ctx.tenantId,
      user_id: ctx.userId,
      feature: ctx.feature,             // "doc_summary", "lead_enrich"
      vendor: ctx.vendor,               // "openai", "deepgram"
      model: res.model,                 // что реально отработало, а не что просили
      input_tokens: res.usage?.input ?? 0,
      cached_input_tokens: res.usage?.cachedInput ?? 0,
      output_tokens: res.usage?.output ?? 0,
      units: res.usage?.units ?? 0,     // минуты, страницы, изображения
      attempt: ctx.attempt,             // 1, 2, 3...
      outcome: "ok",
      latency_ms: Date.now() - startedAt,
    });
    return res.value;
  } catch (e) {
    await usage.emit({ ...ctx, outcome: classify(e), attempt: ctx.attempt });
    throw e;
  }
}

Две детали, которые важнее, чем кажутся.

Пишите модель, которая реально отработала. Если у вас есть роутер, fallback или провайдер, который молча подменяет snapshot, то запрошенная модель - не та, за которую вы платите. Читайте её из ответа.

Пишите номер попытки и исход. Упавшие вызовы в большинстве конфигураций всё равно стоят денег, и именно в retry прячется фантомный расход. Если считать каждую попытку как единицу использования клиента, вы его переплатите счётом. Если не считать падения вообще, ваша внутренняя себестоимость окажется ниже реальной, и маржа будет выглядеть красивее, чем есть. Держите оба числа: billable units и incurred cost - это разные колонки.

Не доверяйте собственной арифметике по токенам

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

  • Prompt caching. Кэшированные input-токены часто стоят в разы дешевле. Игнорируя это различие, вы завышаете себестоимость самых тяжёлых и самых кэш-дружелюбных клиентов, то есть ровно той группы, ценообразование которой вас и волнует.
  • Reasoning-токены. На reasoning-моделях в output попадают токены, которых вы не видите в тексте ответа. Оценка по видимой строке недосчитывает сильно.
  • Цикл с тулами. Одно действие пользователя может породить семь вызовов модели. Если метрить действие, а не вызов вендора, агенты будут казаться прибыльными ровно до прихода счёта.
  • Двойной учёт на retry. Однажды я искал расхождение в 30% и нашёл его в двух retry подряд: внутренний внутри SDK и внешний в очереди задач, оба писали event. Ключ идемпотентности по request_id закрыл вопрос одним коммитом.

В конце каждого месяца сверяйте итоги своего ledger со счётом каждого вендора. Если расхождение больше 3%, не стройте на этом ценообразование. Сначала найдите протечку. Несверенный ledger - это не данные, а слухи.

От events к деньгам: ledger

Usage events сырые и объёмные. Деньги живут в отдельной, маленькой, append-only таблице.

create table cost_ledger (
  id bigserial primary key,
  day date not null,
  tenant_id uuid not null,
  feature text not null,
  vendor text not null,
  model text not null,
  billable_units numeric not null,   -- за что вы берёте деньги
  incurred_cost_usd numeric not null,-- что реально заплатили, с retry
  price_version text not null,       -- какой прайс применялся
  unique (day, tenant_id, feature, vendor, model, price_version)
);

Колонку price_version пропускают чаще всего и жалеют об этом. Цены вендоров меняются. Ваш анализ маржи за март должен воспроизводиться в сентябре. Держите прайс как данные с интервалом действия, а не как константы в коде, и штампуйте каждую строку ledger версией, по которой считали.

Сворачивайте events в ledger ночным джобом. Сырые events держите 30-90 дней для отладки, потом удаляйте. Ledger крошечный, его храните всегда.

Когда это есть, интересные запросы становятся однострочниками:

select tenant_id,
       sum(incurred_cost_usd) as ai_cost,
       sum(incurred_cost_usd) / nullif(mrr, 0) as cost_ratio
from cost_ledger join subscriptions using (tenant_id)
where day >= date_trunc('month', current_date)
group by tenant_id, mrr
order by cost_ratio desc
limit 20;

Этот отсортированный список - самый полезный отчёт в AI SaaS. Он говорит, с кем поговорить, кого ограничить лимитом и какой тариф посчитан неправильно.

Сначала showback, потом chargeback

У атрибуции два разных применения, и путаница между ними стоит дорого.

Showback - вы показываете клиенту, что он потребил, но счёт от этого не меняется. Внутри это же означает, что вы показываете командам и владельцам фич их расход, не перекладывая бюджеты.

Chargeback - потребление напрямую формирует счёт. Кредиты, overage, per-seat с лимитами, metered billing.

Начинайте всегда с showback. Сделайте панель использования: сколько документов обработано, сколько минут расшифровано, сколько прогонов агента, насколько израсходован лимит тарифа. Без долларов. Получите три выигрыша сразу. Клиенты сами себя регулируют, когда видят заполняющуюся полоску. Поддержка перестаёт угадывать. И вы узнаёте, корректен ли ваш учёт, до того как от него начнут зависеть деньги.

Признаки, что пора переходить к chargeback:

  • У распределения cost ratio длинный хвост, и несколько аккаунтов сидят выше 40% от собственного MRR.
  • Поддержка регулярно выторговывает исключения из невидимых лимитов.
  • Sales не может ответить на вопрос “а что будет, если мы утроим объём в следующем квартале”.

При переходе на chargeback берите единицу, которую клиент понимает и может спрогнозировать. Токены - отвратительная единица биллинга для бизнес-покупателя. “Проанализированных страниц”, “расшифрованных звонков”, “обогащённых лидов” - хорошие. Внутри вы конвертируете единицы в деньги через ledger, наружу продаёте целые действия. Держите запас в конверсии, потому что себестоимость действия будет двигаться вместе с моделями, а переписывать прайс каждый квартал вам не захочется.

Одно правило миграции, на котором я настаиваю: гоняйте showback и chargeback параллельно минимум один полный биллинговый цикл. Показывайте клиенту счёт, который он получил бы. Ничто не сжигает доверие быстрее, чем внезапный metered-счёт, построенный на учёте, который никто ни разу не сверял.

Аларм по марже

Бюджеты, которые проверяют раз в месяц, - это не контроль, а вскрытие. Два аларма стоит поставить в первый же день:

  1. Дневная аномалия по тенанту. Если дневной AI-расход тенанта превышает медиану за 14 дней в 3 раза и при этом выше минимального порога в несколько долларов, дёргайте человека. Чаще всего это легальный массовый импорт. Иногда - бесконечный цикл, утёкший API-ключ или кто-то, кто использует ваш free tier как batch-пайплайн.
  2. Потолок расхода на фичу. У каждой фичи жёсткий дневной лимит трат. При его достижении фича деградирует аккуратно: ставит работу в очередь, переключается на более дешёвую модель или возвращает честное “мы догоняем очередь”. Ни одна фича не должна молча съесть весь месячный бюджет.

Деградируйте, а не падайте. Задача в очереди с честным ETA - нормальный продуктовый опыт. 500-я ошибка - нет.

Типичные ошибки

  • Атрибуция только по пользователю. В B2B нужны tenant, user и feature. Без фичи вы знаете, кто дорогой, но не знаете почему.
  • Фоновая работа без себестоимости. Ночной re-embedding, плановые прогоны агентов, прогрев кэша - всё это тоже принадлежит тенанту. Помечайте системным актором и тем тенантом, на который работает.
  • Free tier без потолка. У бесплатных пользователей должен быть жёсткий лимит в коде, а не вежливая фраза в документации.
  • Средние в презентации по ценам. Медианная себестоимость аккаунта почти бесполезна. Смотрите p90 и p99: именно там живут лимиты, вызывающие churn, и дырки в марже.
  • Внедрение учёта после всплеска роста. Атрибуция, прикрученная в кризис, - это атрибуция, которой никто не верит. Сейчас это два дня, потом две недели.

Что сделать на этой неделе

Одна обёртка вокруг платных вызовов. Одна таблица usage events с ключом идемпотентности. Один ночной rollup в ledger с версионированным прайсом. Один запрос, сортирующий тенантов по отношению себестоимости к MRR. Одна панель showback в интерфейсе. Один аларм по аномалии в Slack.

Вот и всё. Сделайте это до того, как оптимизируете первый промпт, потому что как только вы увидите маржу по клиентам, обычно выяснится, что лечение - это смена тарифа или лимит, а не более дешёвая модель.

Вопросы и ответы

Считать себестоимость по токенам из ответа API или оценивать локальным токенайзером?

Всегда берите usage из ответа вендора и пишите его как есть, разделяя обычные и кэшированные input-токены. Локальный токенайзер годится только для предварительной оценки перед вызовом, например чтобы решить, какую модель выбрать. Он систематически врёт на reasoning-моделях, где часть output-токенов вы вообще не видите в тексте, и на кэшировании, где цена за токен другая. В конце месяца сверяйте итоги своего ledger со счётом вендора: расхождение выше 3% значит, что сначала надо искать протечку, а не строить на этих данных ценообразование.

На каком этапе стоит переходить от showback к metered-биллингу?

Когда появляются три сигнала: у вас есть аккаунты, которые съедают больше 40% своего MRR на inference, поддержка регулярно договаривается об исключениях из невидимых лимитов, и sales не может спрогнозировать себестоимость при росте объёма. До этого лучше жить на тарифах с лимитами и showback-панелью, она сама гасит значительную часть перерасхода. При переходе обязательно прогоните минимум один полный биллинговый цикл в режиме "вот счёт, который вы бы получили", чтобы поймать ошибки учёта до того, как их увидит клиент.

Куда относить расходы на фоновые задачи вроде ночного re-embedding или прогрева кэша?

На тенанта, ради которого работа делается, с отдельным системным актором в user_id и своей feature, например "nightly_reindex". Тогда в отчёте по марже такая работа не исчезает и не размывается в общих накладных. Если задача действительно общая для всех, как прогрев глобального кэша или прогон evals, ведите её как отдельного внутреннего тенанта: это ваш R&D-расход, и его полезно видеть отдельной строкой, а не смешивать с себестоимостью обслуживания клиентов.

Похожие статьи