Лиза Петровна — наш ИИ-ассистент, бесплатный бот в Telegram и MAX. В прошлой статье на Хабре я рассказывал про его архитектуру и роутинг моделей. Статью заслуженно заминусовали: получился обзор продукта, а не инженерный текст. Учёл.

Эта статья про одну конкретную вещь: как мы подключили бота к MAX так, чтобы он был тем же самым ассистентом, что в Telegram, с той же памятью и теми же агентами. И что из этого вышло, когда мы включили рекламу: за 2–5 ноября в бота пришли 416 человек, из них 311 в MAX и 126 в Telegram.

Исходная точка

К моменту, когда мы занялись MAX, ядро ассистента уже жило в Telegram. Ядро — это всё, что не про транспорт: секретарь-хаб, профильные агенты (дизайнер, видеограф, юрист, зрение для фото и документов и другие), память, LLM-слой, хранилище.

Задача была сформулирована жёстко: в MAX не должно появиться «второго бота». Ни отдельной логики, ни отдельной памяти. Меняется только то, как сообщение пришло и как ответ ушёл.

Схема

  Telegram Bot API                MAX Bot API
        │                              │
  ┌─────▼──────┐               ┌───────▼───────┐
  │ TG-адаптер │               │  MAX-мост     │
  │            │               │  (long-poll)  │
  └─────┬──────┘               └───────┬───────┘
        │   нормализованное сообщение  │
        └──────────────┬───────────────┘
                ┌──────▼───────┐
                │  ядро        │  доступ, онбординг, first_ref
                │  (pipeline)  │  секретарь → агенты
                └──────┬───────┘
                ┌──────▼───────┐
                │  LLM-роутер  │  DeepSeek / Claude
                └──────────────┘

Мост — отдельный сервис в той же docker compose-сборке, что и остальное ядро. Он не знает ничего про агентов. Он умеет три вещи: забрать событие из MAX, превратить его во внутреннее сообщение и отправить ответ обратно.

MAX Bot API: что понадобилось

Бот работает на официальном Bot API MAX (platform-api.max.ru). Бот зарегистрирован на юрлицо через партнёрский кабинет MAX.

События забираем long-poll’ом, без вебхуков. Для одного бота это проще: не нужен публичный HTTPS-эндпоинт и разбор подписи, а задержка нас устраивает. Токен передаётся в заголовке Authorization, а не в URL, как привыкли в Telegram.

Из всех типов событий нам по-настоящему нужны два:

  • bot_started — пользователь нажал «Начать», в том числе по ссылке с параметром старта;

  • message_created — обычное сообщение: текст, голос, фото, файл.

Упрощённо цикл моста выглядит так (псевдокод, имена методов условные):

async def poll_loop(api, core):
    marker = None
    while True:
        batch = await api.get_updates(marker=marker, timeout=30)
        for upd in batch.updates:
            try:
                await handle(upd, core)
            except Exception:
                log.exception("max: update failed")  # один битый апдейт не роняет цикл
        marker = batch.marker

async def handle(upd, core):
    if upd.type == "bot_started":
        uid = core.identity("max", upd.user.user_id)
        core.set_first_ref(uid, upd.payload)        # метка источника
        await core.on_start(uid, chat_id=upd.chat_id)
    elif upd.type == "message_created":
        msg = normalize(upd.message)                # текст / голос / вложения
        uid = core.identity("max", msg.sender_id)
        async for chunk in core.process_message_stream(uid, msg):
            await send_to_max(msg.chat_id, chunk)

Ключевое здесь process_message_stream. Это та же функция, которую вызывает Telegram-адаптер. Весь пайплайн агентов, память и роутинг моделей в MAX работают без единой строчки MAX-специфичного кода.

Идентичность

Внутренний id пользователя для гостя из MAX строится из его MAX-id. Для случаев, когда один человек сидит в обоих мессенджерах и хочет одну память, есть таблица маппингов: MAX-id → основной аккаунт. Мост перечитывает её по времени изменения файла, без рестарта.

Чего MAX Bot API не отдаёт — телефона. Поэтому склеить аккаунты автоматически по номеру нельзя. Если человек сообщает о проблеме и называет номер, найти его заявку можно только по времени и имени.

Доступ: от ручного одобрения к автоматическому

Первые месяцы бот в MAX был закрытым. Новый пользователь попадал в pending, а мне в Telegram прилетал алерт с инлайн-кнопками «Разрешить» и «Отклонить». Кнопку обрабатывал основной Telegram-бот, по одобрению пользователь получал сообщение обратно в MAX.

Схема рабочая, пока пользователей единицы. Грабли, на которые мы наступили:

  • Алерт мог не отправиться, и в логе оставалась пустая ошибка без ретрая. Человек висел в pending, а я не знал, что он вообще есть.

  • Алерт уходит в Telegram, а заявка живёт в MAX-мосте. Когда мост и Telegram-бот жили на разных хостах, approve-флоу рвался. Пришлось держать их вместе.

Сейчас доступ автоматический, с дневным лимитом на новых пользователей. Оба пути входа, bot_started и message_created, проходят одну и ту же проверку. Так доступ не зависит от того, с какого события человек начал: нажал «Начать» по ссылке или нашёл бота и сразу написал.

Метки источников: start → first_ref

Это самая полезная часть всей интеграции, и она маркетинговая, а не техническая.

Каждая наша ссылка на бота несёт метку: ?start=habr_nov, ?start=vc_nov, метки рекламных групп Директа и так далее. При первом старте ядро записывает её в поле first_ref пользователя. Только при первом, повторный старт по другой ссылке источник не перезаписывает.

Псевдокод:

REF_RE = re.compile(r"^[a-z0-9_]{1,48}$")

def set_first_ref(uid: int, payload: str | None) -> None:
    ref = (payload or "").strip().lower()
    if not REF_RE.match(ref):
        ref = "direct"                       # пустое или мусор — без метки
    db.execute(
        "UPDATE users SET first_ref = %s WHERE id = %s AND first_ref IS NULL",
        (ref, uid),
    )

Ограничение формата нужно не для красоты. Параметр старта приходит от пользователя, его можно подменить руками, и без валидации в аналитику прилетает что угодно. Заодно короткий латинский формат совместим с ограничениями Telegram на параметр start, и одна и та же метка работает в обоих мессенджерах.

Дальше любой отчёт — это GROUP BY first_ref. Именно так мы узнали, что прошлая статья на Хабре привела 6 человек, на vc.ru 1, а посевы в Telegram-каналах 43. И именно так считаем Директ: не по кликам из Метрики, а по тем, кто дошёл до бота и написал.

Диплинки: самое неприятное место

Ссылка вида https://max.ru/<bot>?start=label в десктопном браузере открывает веб-версию. На телефоне всё зависит от того, где её открыли.

У MAX есть своя схема: max://max.ru/<bot>?start=label. Она открывает приложение напрямую, если оно стоит. Если не стоит, пользователь видит ошибку или ничего.

На Android надёжнее intent-ссылка с пакетом приложения ru.oneme.app и фолбэком на веб:

intent://max.ru/<bot>?start=label
  #Intent;scheme=max;package=ru.oneme.app;
  S.browser_fallback_url=https%3A%2F%2Fmax.ru%2F<bot>%3Fstart%3Dlabel;end

Выбор ссылки под платформу упрощённо выглядит так:

function maxLink(bot, label) {
  const web = `https://max.ru/${bot}?start=${label}`;
  const ua = navigator.userAgent;
  if (/Android/i.test(ua)) {
    return `intent://max.ru/${bot}?start=${label}` +
      `#Intent;scheme=max;package=ru.oneme.app;` +
      `S.browser_fallback_url=${encodeURIComponent(web)};end`;
  }
  if (/iPhone|iPad/i.test(ua)) return `max://max.ru/${bot}?start=${label}`;
  return web;
}

Главная грабля оказалась не в синтаксисе, а во встроенных браузерах. В октябре мы пробовали VK Рекламу, и 70% трафика лендинга пришло из WebView Одноклассников. Из такого WebView переход в стороннее приложение работает плохо, кнопку нажали меньше 1% посетителей. Плюс VK не принимает ссылки на max.ru в объявлениях. Кампанию свернули.

Яндекс Директ на поиске ведёт людей в обычный мобильный браузер, и там диплинки работают как задумано.

Канал и модерация

Канал Лизы в MAX (max.ru/channel_lizapetrovna) создаётся не через интерфейс приложения, а через служебного бота «Каналы в MAX для бизнеса», и тоже от юрлица. Отдельно отмечу: это не та модель, к которой привыкли в Telegram, где канал за минуту делает любой пользователь.

С модерацией рекламы проще. Объявление «Нейросеть в MAX без VPN. Бесплатно» Директ пропустил. Оно же стало лучшим: 139 пользователей за первые сутки.

Роутинг моделей: DeepSeek на массовое, Claude на сложное

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

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

За последние 30 дней распределение ответов такое:

  • DeepSeek V4.1 Flash — около 5 700 ответов, ≈84%;

  • Claude Sonnet 5.5 — 825 ответов, ≈12%;

  • остальное — DeepSeek через CLI и vision-модели.

Для бесплатного продукта, у которого дневная аудитория за неделю выросла с 13–21 до 82–111 человек, это не вопрос вкуса: массовый поток должен быть дешёвым, а дорогая модель нужна там, где без неё ответ будет плохим.

Обратная сторона, которую мы видим в живых диалогах: дешёвая модель иногда отвечает в дизайнере английской служебной «преамбулой» перед делом. Новый пользователь, который пришёл за картинкой, видит это как поломку. Генерация картинок у дизайнера удаётся примерно в 87% случаев, остальное — сбои вида «попробуй позже». Это наш текущий технический долг номер один, потому что дизайнер оказался главным сценарием для пришедших из MAX.

Почему три четверти новых пришли из MAX

Цифры рекламы за 2–5 ноября:

Показы

11 178

Клики

1 293

Расход

6 964 ₽

Пришли в бота

416 (MAX ≈310, Telegram ≈110)

Написали хотя бы раз

211

Около 17 ₽ за пришедшего и около 33 ₽ за написавшего.

Интересно, что за активного пользователя MAX не дешевле: группа «ИИ в MAX без VPN» вышла в 38 ₽, «Бесплатно — Telegram» — в 34 ₽. Разница в объёме. Моё объяснение, и это гипотеза, а не измерение:

  1. Конкуренции мало. В Telegram ИИ-ботов сотни. В MAX человек, который ищет нейросеть, находит единицы.

  2. Аргумент попадает в боль. «Без VPN» отвечает на вопрос, который у пользователя MAX уже есть. В Telegram тот же аргумент не цепляет, там работает «бесплатно».

  3. Порог входа. Бот в MAX открывается по одной кнопке, без регистрации и без VPN. В сумме с диплинками это даёт короткий путь от объявления до первого сообщения.

Грабли, которые стоили нам больше всего

Десять дней простоя, которые никто не заметил. При переезде ядра на новый прод MAX-мост в новой сборке включался отдельным профилем docker compose. Профиль не включили. Telegram работал, а MAX-бот молчал десять дней, и никто этого не заметил. После этого у моста появился отдельный сторож.

Вывод: если у вас несколько транспортов к одному ядру, мониторить надо каждый транспорт отдельно. Здоровье ядра ничего не говорит о здоровье моста.

Лендинг съел кнопки MAX. При миграции лендинга со SPA на статику кнопки MAX рендерились на клиенте и в статическую версию не попали. Пока это не заметили, MAX-трафика с сайта не было. Теперь кнопки в статике, и при каждой правке лендинга я проверяю, что все точки входа на месте.

Онбординг. Длинное согласие и три кнопки-примера на старте. В MAX из 161 человека, увидевшего согласие, 106 приняли, 14 отказались, 41 ушли молча. 2 ноября мы оставили короткое приглашение и одну кнопку. Это не про MAX, но в MAX это видно сильнее, потому что туда идёт основной поток.

Что не решено

Удержание. Из прошлой рекламной волны, 23–26 октября, около 190 человек, на седьмой день вернулись 14, это ~7%. Ядро, которое возвращается хотя бы два дня в неделю, около 36 человек.

Технически ассистент в MAX работает так же, как в Telegram. Привлекать людей в него дёшево. А вот поводов возвращаться пока мало, и это следующая задача, уже не про мост.

Если хотите посмотреть

Бот бесплатный. Ссылки с меткой этой статьи, по ней я посчитаю, сколько человек пришло с Хабра, и напишу в комментариях.

Если вы тоже делаете ботов под MAX, напишите в комментариях, как вы решали диплинки из встроенных браузеров. У нас это до сих пор самое хрупкое место.

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


  1. milinsky
    05.11.2026 12:04

    Я вот сколько не читаю таких статей, всё время задаюсь вопросом - а кто-нибудь законы вообще читал? Вам известно, что если вы создаёте публичных ботов, и если предположительно некий россиянин введёт в него свои ПД, а при этом используется внешняя модель, например Claude, то вы автоматически получаете статью? Вы про запрет трансграничной передачи ПД что-нибудь слышали вообще? Рекомендую почитать, перед тем как запиливать ИИ ботов во всё и вся. Успехов!


  1. Domestomag
    05.11.2026 12:04

    Анжела Олеговна по прежнему требует с чем то согласится и отказывается работать. Ни алиса, ни гигачад в телеге так не делают, не надо быть святее папы римского.