Разработчик в маске и робот-помощник считают токены на голографическом экране
Свой биллинг для ИИ-агента: как считать токены и не сойти с ума

Привет! На связи Сергей Смирнов, AI-инженер из LLMStart.ru. Мы продолжаем серию статей про нашего ИИ-консультанта по 1С:УНФ на базе LangChain. Мы уже обсуждали, как прошли путь от простого RAG-прототипа до продакшна и как настраивали контекст внутри агента. Сегодня копнем глубже — в инфраструктуру биллинга.

Наш заказчик — компания Айтон. Они внедряют и поддерживают 1С:УНФ. Наш бот обслуживает сразу два типа пользователей:

  • Свои сотрудники (консультанты Айтона) общаются с агентом в личных чатах. Для них это умная шпаргалка по 1С.

  • Клиенты Айтона задают вопросы в групповых чатах. Раньше там сидели живые люди, а теперь отвечает ИИ-агент.

Каждый ответ нейросети стоит денег (токенов). Чтобы экономика продукта сходилась, расходы на LLM нужно прозрачно контролировать и тарифицировать для каждой компании-клиента отдельно. Нам понадобился биллинг. Сначала казалось, что это просто задачка про калькулятор, но на деле пришлось выстраивать архитектуру всей системы вокруг понятия «Организация».

В этой статье покажу, почему мы отказались от стандартных callback handler’ов LangChain в пользу ContextVar+Mixin, и как считаем расходы в микрокредитах копейка в копейку с OpenRouter.

Что под катом:

  • Контекст: как устроена multi-tenant система и почему мы считаем деньги по организациям.

  • Биллинг: почему мы написали свой велосипед и зачем нам «микрокредиты».

  • Магия ContextVar+Mixin: как протащить ID организации через пять LLM-вызовов в одном запросе (даже если они идут мимо LangChain).


Контекст: как устроена система

Наш ИИ-консультант — это multi-tenant система. Каждая компания-клиент изолирована: она видит только свои вопросы, ответы агента и свой баланс денег. Чужие данные скрыты.

Вся архитектура держится на простой иерархии: организация → пользователи и чаты. Организация — это ядро системы, через которое проходят все связи.

Иерархия сущностей multi-tenant LLM-сервиса: организация в центре, под ней пользователи и чаты, журналы расходов привязаны к чатам, журнал пополнений — к организации
Архитектурная иерархия: организация в центре, под ней — пользователи, чаты и журналы

Мы приняли несколько важных архитектурных решений, которые сильно упростили нам жизнь:

  1. ОГРН вместо UUID. Идентификатором организации выступает ОГРН компании. Это естественный ключ, который сразу связывает базу данных с реальным миром без лишних маппингов.

  2. Роли зависят от организации. Мы обошлись всего тремя ролями: администратор, консультант и клиент. Попал в головную компанию (Айтон) — ты консультант и видишь всё. Попал в клиентскую — ты клиент и сидишь в своей песочнице. Вся логика ролей лежит в одном месте.

  3. Общий котел вместо кошельков по чатам. Изначально заказчик просил считать деньги по чатам. Звучит логично: чат тратит токены, значит, у чата должен быть свой кошелек. Но что, если у компании три чата? Как пополнять баланс? Как его распределять? Это путь к хаосу. Поэтому мы сделали один общий баланс на всю организацию. Пользователь пишет в любой чат → система смотрит, чья это организация → списывает деньги из общего котла.

Баланс — это не какая-то отдельная цифра в базе данных. Это простая математика: мы берем все деньги, которые организация закинула на счет (журнал пополнений), и вычитаем все деньги, которые потратили её чаты (журнал расходов).

Когда мы поняли, что деньги живут на уровне организации, стало понятно, как строить биллинг дальше. Писать ли свой биллинг? В чем считать: в токенах или в деньгах? Как протащить баланс через все нейросети? Об этом — дальше.


Биллинг: почему мы изобрели свои «микрокредиты»

Мы смотрели на готовые решения (вроде LiteLLM Proxy), но ни одно не подошло на 100%. Берешь готовое — миришься с его ограничениями. А благодаря LLM-кодингу написать свой велосипед стало проще простого. Поэтому мы написали свой биллинг.

Дальше встал вопрос: в чем считать расходы? Было три варианта.

Три альтернативы единицы учёта расходов: наивные токены, взвешенные токены, микрокредиты — и их компромиссы по точности и операционной нагрузке
Альтернативы единицы учёта в биллинге и их компромиссы
  • Вариант 1: Наивные токены. Считаем все токены по одной цене. Звучит просто, но на деле это провал. Модель Gemini Pro стоит $2.00 за миллион токенов, а Flash — $0.50. Если мы считаем в «просто токенах», то запрос к Pro будет незаметно сжирать бюджет в 4 раза быстрее.

  • Вариант 2: Взвешенные токены. Придумываем коэффициенты. Например, 1 токен Pro = 4 токена Flash. Проблема: как только провайдер меняет цены, наши коэффициенты летят в мусорку. Придется пересчитывать балансы всех клиентов задним числом. Это больно.

  • Вариант 3: Микрокредиты (наш выбор). Мы ввели свою валюту — микрокредит (0.000001 доллара). Почему так мелко? Чтобы хранить деньги в базе данных целыми числами и не терять копейки на округлениях.

Мы берем реальный расход токенов из ответа нейросети (usage_metadata), умножаем на текущие цены провайдера и получаем сумму в микрокредитах. Провайдер поменял цены? Мы просто обновляем один файл с конфигом, и всё работает дальше. Никаких пересчетов старых балансов.

Мы проверили формулу на 18 вызовах к разным моделям. Результат сошелся с биллингом OpenRouter копейка в копейку.

И еще одна фишка: в журнал расходов мы записываем не только сумму списания, но и цены модели на тот момент (из файла pricing.yaml). Если провайдер поменял цены, а мы обновили конфиг только через неделю, мы всегда сможем поднять логи и посмотреть, по какому тарифу считали каждый конкретный запрос. Полный контроль.


Пять нейросетей в одном запросе: как протащить баланс через всё

Общая архитектура агента: главный агент маршрутизирует запрос — всегда вызываются классификатор и проверка на инъекции, опционально подключаются суммаризатор и эксперт-агент
Архитектура агента: до пяти LLM-вызовов на один HTTP-запрос — три обязательных, два опциональных

Когда пользователь пишет одно сообщение, под капотом может сработать до пяти нейросетей!

  • Всегда работают три: главный агент, классификатор и проверка на инъекции.

  • Иногда подключаются еще две: суммаризатор (если диалог стал слишком длинным) и эксперт-агент (если вопрос очень узкий).

Каждый вызов нейросети — это поход к провайдеру за деньги. Перед каждым вызовом нужно проверить баланс, а после — списать копеечку.

У LangChain есть штатный способ передать ID организации в нейросеть — поле config. Но проблема в том, что через граф LangChain идет только главный агент. Остальные четыре вызываются напрямую, в обход графа. Туда config просто не долетает.

Вот как это выглядит:

LLM

Получает контекст?

Почему / почему нет

Главный агент

Да

LangChain пробрасывает в config напрямую

Классификатор

Нет

Вызывается напрямую, вне графа

Суммаризатор

Нет

Вызывается напрямую, вне графа

Эксперт-агент

?

Зависит от проброса в tool, нестабильно

Проверка на инъекции

Нет

Вызывается напрямую, вне графа

Штатный механизм провалился. Нам нужно было перехватить каждый вызов на самом низком уровне — там, где модель генерирует ответ (метод _agenerate()).

Три тупиковых пути и один рабочий

Мы перебрали три варианта:

  • Вариант А: Переписать сигнатуры LangChain. Плохая идея. Мы не контролируем чужой код, всё сломается при первом же обновлении.

  • Вариант Б: Использовать callback handler. В LangChain есть коллбэки, но они срабатывают слишком поздно — когда запрос уже полетел в LLM. Если баланс пуст, откатывать уже нечего.

  • Вариант В: ContextVar + Mixin (наш выбор). Мы взяли ContextVar из стандартной библиотеки Python. Он умеет хранить данные, привязанные к текущему асинхронному запросу.

Мы сделали так: пришел HTTP-запрос → проверили баланс → положили ID организации в ContextVar → запустили все нейросети. Каждая нейросеть наследует наш класс Mixin, который перед генерацией ответа лезет в ContextVar, берет оттуда ID организации и после ответа списывает деньги.

Почему ContextVar — это магия вне Хогвартса

  1. Он хранит только окружение запроса. ID организации, ID чата — это вещи, которые живут ровно один запрос.

  2. Он обходит чужой код. Нам не пришлось лезть в потроха LangChain.

  3. Он безопасен. Как только запрос отработал, ContextVar очищается. Никаких утечек данных.

  4. Он масштабируется. Захотим добавить шестую нейросеть или систему аналитики — они просто возьмут данные из ContextVar.

Покажите мне код

Сначала Mixin. Он подмешивается ко всем пяти нейросетям:

class BillingMixin:
    """Подмешивается ко всем пяти LLM. Перехватывает каждый вызов модели."""

    async def _agenerate(self, messages, **kwargs):
        # 1. Берем ID организации из ContextVar
        ctx = billing_context.get()

        # 2. Идем в API провайдера за ответом
        response = await super()._agenerate(messages, **kwargs)

        # 3. Списываем микрокредиты за потраченные токены
        await record_spend(ctx.org_id, response.usage_metadata)

        return response

А вот так мы оборачиваем весь HTTP-запрос:

@asynccontextmanager
async def billing_request(org_id, request_id, chat_id):
    # 1. Проверяем баланс. Денег нет? До свидания.
    balance = await check_budget(org_id)
    if balance <= 0:
        raise BudgetExhausted(org_id)

    # 2. Кладем ID организации в ContextVar
    token = billing_context.set(
        BillingContext(org_id=org_id, request_id=request_id, chat_id=chat_id)
    )

    try:
        # 3. Здесь работают до пяти нейросетей. Каждая спишет деньги через Mixin.
        yield BillingRequestResult(org_id=org_id, balance=balance)
    finally:
        # 4. Железобетонно очищаем ContextVar
        billing_context.reset(token)

Операции с ContextVar занимают микросекунды. Зато мы получили изящную архитектуру: один HTTP-запрос, до пяти нейросетей, и один общий контекст для биллинга.

Один HTTP-запрос превращается в до пяти LLM-вызовов, прошитых одним контекстом через ContextVar — биллинг работает на каждом
Архитектура биллинга: один запрос, до пяти LLM последовательно, один контекст через ContextVar

Итог: почему свой биллинг — это правильный выбор

Мы построили биллинг, который идеально ложится на multi-tenant архитектуру. И это решение оказалось выигрышным по всем фронтам:

  • Балансы не размазаны: деньги считаются на уровне организации, а не по отдельным чатам.

  • Никаких утечек: ContextVar надежно изолирует данные каждого запроса.

  • Полный контроль: мы не зависим от ограничений готовых прокси-серверов и можем добавлять любую бизнес-логику прямо в метод генерации.

Система легко масштабируется. Пришла новая компания? Мы просто заводим новую организацию, и всё остальное подтягивается автоматически. На эту же архитектуру в будущем отлично ложится оценка качества ответов агента по каждой компании (multi-tenant evaluation) и A/B-тестирование разных нейросетей.

Если лень читать всё (TL;DR)

  • Организация — ось системы. ОГРН вместо UUID спасает от путаницы, а баланс считается на лету из журналов пополнений и расходов.

  • Микрокредиты рулят. Считаем расходы с точностью до копейки, не боимся смены тарифов провайдера.

  • ContextVar + Mixin — идеальный мост. Протаскиваем ID организации через пять нейросетей в одном запросе, не ломая чужой код.


Архитектура многошагового агента с пятью LLM-вызовами на один запрос и собственным биллингом — это реальный продакшн-опыт, который позволяет масштабировать ИИ-решения на десятки компаний без хаоса в коде и финансах.

В LLMStart.ru мы помогаем бизнесу решать такие задачи и получать реальный эффект от ИИ — от прототипа до промышленной эксплуатации. Приходите к нам за консультацией или разработкой под ключ, если уперлись в ограничения готовых решений.

Если хотите перенять опыт и научиться делать подобные системы самостоятельно, у нас есть Комбо из четырех курсов по AI-driven разработке и ИИ-агентам. Это полный гайд от AI-кодинга и первых ассистентов к AI-продуктам, RAG-системам, агентам и мультиагентным системам.

По любым вопросам пишите мне в личку: Telegram или ВК. Приглашаем также в наши соцсети про ИИ-кодинг ИИ-агентов: в ТГ-канал и ВК-сообщество

Комментарии (0)