
Когда-то, ещё лет десять назад, я был админом сервера World of Warcraft на движке MaNGOS (олдам привет). Для непосвящённых — это open-source эмулятор «мира», где можно поднять свой WoW-сервер.
Но именно там я впервые понял, что один процесс не вытянет всё сразу.
Авторизация (realmd) жила своей жизнью. Если она падала — мир продолжал работать, но игроки не могли зайти.
Мир (worldd) был самым тяжёлым: квесты, спеллы, рейды, скрипты. Малейший баг — и процесс жрал 100% CPU, лагали все.
База данных (MySQL) пухла с каждым новым патчем и логами игроков. Один неудачный запрос — и сервер уходил в ступор.
Веб-кабинет для регистрации и донатов мы держали отдельно, потому что PHP-скрипты конфликтовали с основным сервером.
Позже появился даже отдельный FTP для патчей и аддонов, потому что загрузки «клали» игровой сервер.
Тогда я впервые сформулировал правило:
«Каждый сервис должен жить отдельно, иначе падение одного тянет за собой всех».
На MaNGOS это выглядело как realmd + worldd + MySQL + вебкабинет. Сегодня, в проекте с ChatGPT-ботом в Телеграме, ситуация ровно такая же — только вместо эльфов и орков у нас платежи, AI-агенты и медиафайлы.
Когда смотришь на Telegram-ботов с ChatGPT, кажется: ну что там сложного? Взял API-ключ, накатал 50 строк на Python, задеплоил на бесплатный VPS — и готово.
Но как только бот начинает обрастать (ну будет обрастать) тысячами пользователей, простая картинка разваливается.
Банально: одному серверу сложно и слушать вебхуки Телеграма, и общаться с банком по реккурентным платежам, и держать базы, и тянуть видео-транскодинг. Поэтому мы ушли в кластерную архитектуру: каждый модуль живёт на своём железе.


Из WoW в Телеграм: те же грабли, только в другой обёртке
Опыт с MaNGOS стал для меня настоящей «боевой школой». Тогда я понял, что даже маленький личный сервер с сотней игроков требует раздельных процессов, мониторинга и планирования ресурсов. Сегодня, когда мы строим кластер под ChatGPT-бота в Телеграме, всё выглядит гораздо современнее, но логика осталась той же.
В WoW у нас было:
realmd — авторизация.
worldd — игровой мир.
MySQL — база для аккаунтов и мира.
вебкабинет + FTP для патчей.
В Телеграм-боте с AI у нас теперь:
Модуль авторизации — то же самое «realmd», только для пользователей. Встречает нового юзера, проверяет баланс и подписку.
Модуль платежей — аналог донат-модуля. Отвечает за деньги, чеки, реккурентные списания.
Две базы — PostgreSQL и MySQL, как когда-то world и accounts. Каждая со своей ролью.
Текстовый модуль — наш «worldd», только вместо квестов он гоняет запросы к LLM.
Видео, аудио, изображения — как отдельные «подсистемы», каждая со своим прожорливым характером.
S3-хранилище — вместо FTP, где мы когда-то держали патчи и клиент.
Mini-web-app — эволюция того самого вебкабинета, только теперь внутри Телеграма.
Тестовый сервер — аналог PTR (Public Test Realm). Всё новое сначала идёт туда.
Почему модульность — не блажь, а необходимость
Тут важно подчеркнуть мысль: разделение на модули — это не «модно» и не «по канонам микросервисов». Это проверенный временем способ не уронить всё разом.
Когда я админил WoW-сервер, игроки прощали лаги мобов, но не прощали падения авторизации или багов с донатом.
Когда мы делаем Telegram-бота, пользователи прощают задержку в генерации картинки, но не простят списание денег без услуги или вечный «Загрузка…» при старте диалога.
Вот почему у каждого критичного элемента — свой сервер. Так падает только он, а не весь проект.

Экономика: токены, батчи и маржинальность
аждый диалог с моделью — это не только входящий текст пользователя, но и выход, который генерирует ИИ. И если вход обычно короткий («привет», «напиши план проекта», «сделай резюме»), то выход может быть в десять раз длиннее. Именно за это OpenAI, DeepSeek и прочие берут деньги.
На практике это выглядит так: пользователь написал один короткий вопрос, а модель вернула целую простыню на полторы тысячи символов. В терминах токенов это 30 на вход и 200 на выход. Себестоимость такого диалога — копейки, но когда умножаешь на тысячи пользователей и сотни сообщений каждый день, счёт растёт лавинообразно.
Экономика и решения о которых забывают.
батчи
Это стратегия складывать несколько однотипных задач в общую партию и отправлять/обрабатывать их за один проход вместо десятков отдельных обращений. Цель — меньше накладных расходов на запрос, лучше утилизация модели/сети, ниже цена за токены (за счёт уменьшения служебного контекста и числа сетевых раундов).
Как это работает по шагам
Очередь: все мелкие задания попадают в одну (или несколько) очередей с метками типа задачи.
Агрегация: воркер «снимает» из очереди до N элементов или ждёт T миллисекунд, чтобы набрать партию.
Формирование партии: строится один запрос, где каждая подзадача имеет свой идентификатор (ID) и минимальный контекст.
Вызов модели: отправляем один запрос с массивом подзадач (логически — одно общение).
Разбор ответа: распределяем результаты по исходным ID, сохраняем/отправляем пользователям.
Недоставленные/ошибки: частичный успех — ок, «плохие» элементы улетают в повторную очередь/Dead-Letter Queue (DLQ).
Ключевые настройки
Политика набора: N (размер батча), T (макс. задержка набора). Баланс между задержкой и экономией.
Idempotency: каждому подпункту — устойчивый ID; повторная отправка не должна дублировать результат.
Пределы модели: следим за лимитами токенов/веса запроса; при переполнении — делим партию.
Сортировка: объединяйте действительно похожие задачи — модели легче «входить в ритм», меньше контекстного «мусора».
Мониторинг: собирайте метрики «экономия/латентность/ошибки» отдельно для батч-контуров.
Плюсы / Минусы
Плюсы: меньше сетевых раундов, меньше повторяющегося системного контекста, выше пропускная, дешевле.
Минусы: дополнительная задержка на набор партии; при всплесках — сложнее локализовать сбой внутри пакета.
Что такое «фоллбеки» (fallbacks)
Это маршрутизация запросов по уровням надёжности/стоимости, когда «дорогой» или «умный» путь заменяется «более дешёвым/простым/надёжным» при определённых условиях: ошибка, таймаут, перегрузка, лимиты, или задача слишком простая для heavy-модели.
Модель уровней (tiers)
Tier A (премиум/дорого): мощные модели/длинные контексты/лучшее качество.
Tier B (сбалансированный): средние по цене/качеству, покрывают 70–80% задач.
Tier C (эконом): дешёвые или локальные модели для «привет/как дела/суммируй 3 строки».
Варианты фоллбеков
По простоте задачи («routing by intent/complexity»): простые запросы сразу идут на Tier C; сложные — на A/B.
По SLA/таймаутам: если Tier A не ответил за X мс — переключаем на Tier B, затем на C.
По ошибкам/лимитам: при 429/5xx у провайдера — мгновенный переход вниз по лестнице.
По стоимости: если прогнозируемый объём ответа слишком велик — предлагаем пользователю краткую версию (эконом-вариант) или ведём на модель подешевле.
По безопасности/модерации: если модерация «подозревает» риск — принудительно через модель с более строгыми фильтрами.
Как это устроить по шагам
Классификация запроса: лёгкий/средний/сложный; нужен ли инструмент (код, поиск, RAG) или хватит «болталки».
План маршрута: первичный таргет (например, Tier B) + политика деградации (B→C, B→A, A→C и т. п.).
Ограничители: глобальные лимиты токенов, макс. длина ответа, температурные пресеты для каждого Tier.
Hedging (опционально): параллельный запуск двух дешёвых запросов с ранней отдачей первого успешного.
Объединитель результата: если есть частичный контекст или подсказка от «простого» варианта — используем её как черновик для «умного» (иногда наоборот).
Логирование: фиксируем маршрут, время, стоимость и качество (оценку), чтобы «учить» маршрутизатор.
Плюсы / Минусы
Плюсы: устойчивость к сбоям провайдера, предсказуемая стоимость, быстрые ответы на простые вопросы, возможность держать SLA.
Минусы: сложнее отладка и аналитика; риск «переэкономить» и ухудшить качество, если маршрутизация агрессивна.
Как это сочетается в одном проекте
Входящий поток сначала попадает в роутер (определяем сложность/намерение/ограничения).
Если задача мелкая и массовая → батч-контур (экономим сетевые раунды).
Если задача диалоговая и непредсказуемая → сразу в Tier B, с фоллбеком на Tier C по таймауту или стоимости; для «важных» запросов — наоборот, B→A.
Модерация/безопасность всегда может перехватить и изменить маршрут (например, отправить в более строгую модель).
Метрики: отдельно считаем выгоду батчей (экономия токенов/сетевых вызовов) и эффективность фоллбеков (доля переключений, спасённые запросы, прирост SLA, разница в цене).
Практические правила (коротко)
Батчируйте только однотипные микро-задачи; держите гибкий размер партии и предел по времени набора.
Всегда используйте идемпотентность (устойчивые ID) и DLQ для частичных сбоев в батчах.
В фоллбеке начинайте с Tier B как «рабочей лошадки», опускайтесь на C при перегрузке/лимитах, поднимайтесь на A для VIP-кейсов и «тяжёлых» задач.
Держите жёсткие таймауты и лимиты токенов на каждом Tier; логируйте маршрут и стоимость — это ваш «спидометр экономики».
Не забывайте про UX: если ответ «урезан» экономией — дайте пользователю кнопку «получить развёрнутый» (и осознанно потратить больше токенов).

ПРОБЛЕМА ВСЕХ БОТОВ - ТАМ НЕТ ВОЗМОЖНОСТИ ПЕРЕКЛЮЧАТЬСЯ МЕЖДУ ДИАЛОГАМИ А У НАС ВСЕ ЭТО ЕСТЬ!
Транспорт.
Telegram доставляет апдейты до нашего вебхука по HTTPS. Это не E2E, но трафик в пути защищён TLS.
Мы не полагаемся на Telegram для хранения истории: всё нужное сохраняем у себя.
Шифрование в БД (at rest).
Каждое сообщение (и метаданные) мы шифруем перед записью в БД, чтобы даже дамп БД был бесполезен без ключей.
-
Используем конвертное шифрование:
Генерируем случайный ключ данных (DEK) на диалог/юзера.
DEK оборачиваем (wrap) мастер-ключом из KMS/хранилища секретов (Vault/KMS).
Сами payload’ы шифруются AEAD (например, AES-GCM) с уникальным nonce на запись; в тег аутентичности включаем метаданные (время, user_id), чтобы защититься от подмены.
Ключи в памяти держим временно: при старте/завершении запроса, с авто-очисткой.
Опциональный «псевдо-E2E» режим.
Пользователь может задать свой passphrase. Мы через Argon2id/PAKE получаем материал ключа, дополнительно им шифруем DEK (двойная обёртка).
Без passphrase расшифровать диалог нельзя — даже нам; это компромиссный режим «повышенной приватности» для параноиков.
Жизненный цикл данных.
Для каждого диалога есть TTL/retention-политики: сырые сообщения, сводки, эмбеддинги.
Есть «инкогнито»: не храним сырое, оставляем только краткую сводку или вообще ничего.
Удаление — «в два шага»: логическое (сразу) и физическое (асинхронная очистка/затирание).
Как пользователь «продолжает диалог» и почему это дёшево
Главная идея: мы не шлём в LLM всю историю. Мы собираем тонкий контекст из трёх слоёв и строго держим бюджет токенов
1) Последние короткие реплики.
Держим N последних ходов (обычно 3–5), потому что они определяют локальный контекст.
Это дешёвый кусок: немного токенов, а пользы максимум.
2) Свертка старой истории в «роллинг-сводку».
Всё, что старше последних N ходов, регулярно сжимаем в короткий абзац: «о чём вообще чат, какие решения/факты уже приняты».
Сжатие делает дешёвая модель (типа 4o-mini/Qwen-small), мы запускаем её офлайн, а результат кэшируем и шифруем.
Таким образом, при «продолжить» мы подмешиваем 1–3 предложения вместо 5–50 сообщений. Экономия токенов колоссальная.
3) «Память-факты» (key–value).
Отдельно храним короткие «факты» о пользователе: имя, предпочтения, выбраны ли платные настройки, ID проектов — не как длинный текст, а как маленькие поля.
Перед запросом формируем узкий заголовок-память: 1–2 строки с этими фактами.
Это дешевле и надёжнее, чем таскать длинный диалог ради пары важных деталей.
даешь хабр эффект
ititkov
Слои - очень мощная вещь для экономии. Мы пошли дальше и для разных задач использовали разные LLM. Anthropic - для распознания команды, Gemini 2.0 для парсинга и Gemini 2.5 для умничанья. Дело не только в цене, но и во времени отклика