
Привет! На связи Сергей Смирнов, AI-инженер из LLMStart.ru. Мы продолжаем серию статей про нашего ИИ-консультанта по 1С:УНФ на базе LangChain. Мы уже обсуждали, как прошли путь от простого RAG-прототипа до продакшна и как настраивали контекст внутри агента. Сегодня копнем глубже — в инфраструктуру биллинга.
Наш заказчик — компания Айтон. Они внедряют и поддерживают 1С:УНФ. Наш бот обслуживает сразу два типа пользователей:
Свои сотрудники (консультанты Айтона) общаются с агентом в личных чатах. Для них это умная шпаргалка по 1С.
Клиенты Айтона задают вопросы в групповых чатах. Раньше там сидели живые люди, а теперь отвечает ИИ-агент.
Каждый ответ нейросети стоит денег (токенов). Чтобы экономика продукта сходилась, расходы на LLM нужно прозрачно контролировать и тарифицировать для каждой компании-клиента отдельно. Нам понадобился биллинг. Сначала казалось, что это просто задачка про калькулятор, но на деле пришлось выстраивать архитектуру всей системы вокруг понятия «Организация».
В этой статье покажу, почему мы отказались от стандартных callback handler’ов LangChain в пользу ContextVar+Mixin, и как считаем расходы в микрокредитах копейка в копейку с OpenRouter.
Что под катом:
Контекст: как устроена multi-tenant система и почему мы считаем деньги по организациям.
Биллинг: почему мы написали свой велосипед и зачем нам «микрокредиты».
Магия ContextVar+Mixin: как протащить ID организации через пять LLM-вызовов в одном запросе (даже если они идут мимо LangChain).
Контекст: как устроена система
Наш ИИ-консультант — это multi-tenant система. Каждая компания-клиент изолирована: она видит только свои вопросы, ответы агента и свой баланс денег. Чужие данные скрыты.
Вся архитектура держится на простой иерархии: организация → пользователи и чаты. Организация — это ядро системы, через которое проходят все связи.

Мы приняли несколько важных архитектурных решений, которые сильно упростили нам жизнь:
ОГРН вместо UUID. Идентификатором организации выступает ОГРН компании. Это естественный ключ, который сразу связывает базу данных с реальным миром без лишних маппингов.
Роли зависят от организации. Мы обошлись всего тремя ролями: администратор, консультант и клиент. Попал в головную компанию (Айтон) — ты консультант и видишь всё. Попал в клиентскую — ты клиент и сидишь в своей песочнице. Вся логика ролей лежит в одном месте.
Общий котел вместо кошельков по чатам. Изначально заказчик просил считать деньги по чатам. Звучит логично: чат тратит токены, значит, у чата должен быть свой кошелек. Но что, если у компании три чата? Как пополнять баланс? Как его распределять? Это путь к хаосу. Поэтому мы сделали один общий баланс на всю организацию. Пользователь пишет в любой чат → система смотрит, чья это организация → списывает деньги из общего котла.
Баланс — это не какая-то отдельная цифра в базе данных. Это простая математика: мы берем все деньги, которые организация закинула на счет (журнал пополнений), и вычитаем все деньги, которые потратили её чаты (журнал расходов).
Когда мы поняли, что деньги живут на уровне организации, стало понятно, как строить биллинг дальше. Писать ли свой биллинг? В чем считать: в токенах или в деньгах? Как протащить баланс через все нейросети? Об этом — дальше.
Биллинг: почему мы изобрели свои «микрокредиты»
Мы смотрели на готовые решения (вроде 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). Если провайдер поменял цены, а мы обновили конфиг только через неделю, мы всегда сможем поднять логи и посмотреть, по какому тарифу считали каждый конкретный запрос. Полный контроль.
Пять нейросетей в одном запросе: как протащить баланс через всё

Когда пользователь пишет одно сообщение, под капотом может сработать до пяти нейросетей!
Всегда работают три: главный агент, классификатор и проверка на инъекции.
Иногда подключаются еще две: суммаризатор (если диалог стал слишком длинным) и эксперт-агент (если вопрос очень узкий).
Каждый вызов нейросети — это поход к провайдеру за деньги. Перед каждым вызовом нужно проверить баланс, а после — списать копеечку.
У 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 — это магия вне Хогвартса
Он хранит только окружение запроса. ID организации, ID чата — это вещи, которые живут ровно один запрос.
Он обходит чужой код. Нам не пришлось лезть в потроха LangChain.
Он безопасен. Как только запрос отработал,
ContextVarочищается. Никаких утечек данных.Он масштабируется. Захотим добавить шестую нейросеть или систему аналитики — они просто возьмут данные из
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-запрос, до пяти нейросетей, и один общий контекст для биллинга.

Итог: почему свой биллинг — это правильный выбор
Мы построили биллинг, который идеально ложится на multi-tenant архитектуру. И это решение оказалось выигрышным по всем фронтам:
Балансы не размазаны: деньги считаются на уровне организации, а не по отдельным чатам.
Никаких утечек: ContextVar надежно изолирует данные каждого запроса.
Полный контроль: мы не зависим от ограничений готовых прокси-серверов и можем добавлять любую бизнес-логику прямо в метод генерации.
Система легко масштабируется. Пришла новая компания? Мы просто заводим новую организацию, и всё остальное подтягивается автоматически. На эту же архитектуру в будущем отлично ложится оценка качества ответов агента по каждой компании (multi-tenant evaluation) и A/B-тестирование разных нейросетей.
Если лень читать всё (TL;DR)
Организация — ось системы. ОГРН вместо UUID спасает от путаницы, а баланс считается на лету из журналов пополнений и расходов.
Микрокредиты рулят. Считаем расходы с точностью до копейки, не боимся смены тарифов провайдера.
ContextVar + Mixin — идеальный мост. Протаскиваем ID организации через пять нейросетей в одном запросе, не ломая чужой код.
Архитектура многошагового агента с пятью LLM-вызовами на один запрос и собственным биллингом — это реальный продакшн-опыт, который позволяет масштабировать ИИ-решения на десятки компаний без хаоса в коде и финансах.
В LLMStart.ru мы помогаем бизнесу решать такие задачи и получать реальный эффект от ИИ — от прототипа до промышленной эксплуатации. Приходите к нам за консультацией или разработкой под ключ, если уперлись в ограничения готовых решений.
Если хотите перенять опыт и научиться делать подобные системы самостоятельно, у нас есть Комбо из четырех курсов по AI-driven разработке и ИИ-агентам. Это полный гайд от AI-кодинга и первых ассистентов к AI-продуктам, RAG-системам, агентам и мультиагентным системам.
По любым вопросам пишите мне в личку: Telegram или ВК. Приглашаем также в наши соцсети про ИИ-кодинг ИИ-агентов: в ТГ-канал и ВК-сообщество