ИИ-консультант по 1С:УНФ, который команда LLMStart.ru строит для компании Айтон, работает в режиме multi-tenant: каждая компания-клиент видит только свои чаты, своих пользователей и свой баланс. Айтон внедряет и поддерживает 1С:УНФ, и бот обслуживает сразу два типа аудитории — собственных консультантов в личных чатах и клиентов Айтона в групповых. Раньше на вопросы в групповых чатах отвечали живые люди.
Когда каждый ответ нейросети стоит денег, встаёт вопрос: как прозрачно тарифицировать расходы для каждой компании отдельно? Первый очевидный ответ — завести кошелёк на каждый чат. Но у одной компании может быть три чата: как пополнять баланс, как распределять лимиты? Команда выбрала другую модель: один общий баланс на организацию. Пользователь пишет в любой чат — система определяет организацию и списывает из общего котла. Баланс при этом не хранится отдельной цифрой: это разность между журналом пополнений и журналом расходов.
| LLM | Получает контекст? | Причина |
|---|---|---|
| Главный агент | Да | LangChain пробрасывает в config напрямую |
| Классификатор | Нет | Вызывается напрямую, вне графа |
| Суммаризатор | Нет | Вызывается напрямую, вне графа |
| Эксперт-агент | Нестабильно | Зависит от проброса в tool |
| Проверка на инъекции | Нет | Вызывается напрямую, вне графа |
Идентификатором организации выбран ОГРН — естественный ключ, который не требует дополнительных маппингов между базой данных и реальным миром. Роли всего три: администратор, консультант и клиент. Принадлежность к головной компании (Айтон) автоматически даёт роль консультанта с полным доступом, принадлежность к клиентской — роль клиента в изолированной песочнице.
Вместо кошельков по чатам выбран общий баланс на организацию: баланс = сумма пополнений минус сумма расходов.
В чём считать расходы — отдельный архитектурный вопрос. Наивный подход — считать все токены по одной цене — не работает: Gemini Pro стоит $2,00 за миллион токенов, а Flash — $0,50, и запрос к Pro незаметно съедает бюджет в четыре раза быстрее. Взвешенные коэффициенты (1 токен Pro = 4 токена Flash) решают проблему, но при каждом изменении цен провайдера придётся пересчитывать балансы всех клиентов задним числом. Команда ввела собственную единицу — микрокредит, равный 0,000001 доллара. Такой масштаб позволяет хранить суммы в базе целыми числами без потерь на округлениях. Реальный расход токенов берётся из поля usage_metadata ответа нейросети, умножается на текущие цены из файла pricing.yaml — и получается сумма в микрокредитах. Если провайдер меняет тарифы, достаточно обновить один конфиг. В журнал расходов записываются не только суммы, но и цены модели на момент вызова — это позволяет поднять логи и восстановить тариф для любого конкретного запроса. Формула проверена на 18 вызовах к разным моделям: результат совпал с биллингом OpenRouter.
Самая нетривиальная часть — передать ID организации через все нейросети внутри одного запроса. Когда пользователь отправляет сообщение, под капотом запускается до пяти LLM: главный агент, классификатор, проверка на инъекции (три обязательных), а также суммаризатор и эксперт-агент (два опциональных — если диалог слишком длинный или вопрос узкоспециальный). Через граф LangChain проходит только главный агент — остальные четыре вызываются напрямую, и стандартное поле config туда не долетает.
Коллбэки LangChain (callback handler) тоже не подошли: они срабатывают уже после того, как запрос ушёл к провайдеру, — проверить баланс до вызова не получится. Решением стала связка ContextVar и Mixin. ContextVar — стандартный механизм Python для хранения данных, привязанных к конкретному асинхронному запросу. При входящем HTTP-запросе система проверяет баланс, кладёт ID организации в ContextVar и запускает все нейросети. Каждая из пяти моделей наследует класс BillingMixin, который перед генерацией ответа читает ID из ContextVar, а после — списывает сумму. Когда запрос завершается, ContextVar очищается автоматически, исключая утечки данных между запросами разных клиентов. Добавление шестой нейросети или новой аналитической системы не потребует изменений в логике передачи контекста — достаточно подмешать тот же Mixin.