- BrainTools - https://www.braintools.ru -
Лиза Петровна — наш ИИ-ассистент, бесплатный бот в Telegram и MAX. В прошлой статье на Хабре я рассказывал про его архитектуру и роутинг моделей. Статью заслуженно заминусовали: получился обзор продукта, а не инженерный текст. Учёл.
Эта статья про одну конкретную вещь: как мы подключили бота к MAX так, чтобы он был тем же самым ассистентом, что в Telegram, с той же памятью [1] и теми же агентами. И что из этого вышло, когда мы включили рекламу: за 2–5 ноября в бота пришли 416 человек, из них 311 в MAX и 126 в Telegram.
К моменту, когда мы занялись MAX, ядро ассистента уже жило в Telegram. Ядро — это всё, что не про транспорт: секретарь-хаб, профильные агенты (дизайнер, видеограф, юрист, зрение [2] для фото и документов и другие), память, LLM-слой, хранилище.
Задача была сформулирована жёстко: в MAX не должно появиться «второго бота». Ни отдельной логики, ни отдельной памяти. Меняется только то, как сообщение пришло и как ответ ушёл.
Telegram Bot API MAX Bot API
│ │
┌─────▼──────┐ ┌───────▼───────┐
│ TG-адаптер │ │ MAX-мост │
│ │ │ (long-poll) │
└─────┬──────┘ └───────┬───────┘
│ нормализованное сообщение │
└──────────────┬───────────────┘
┌──────▼───────┐
│ ядро │ доступ, онбординг, first_ref
│ (pipeline) │ секретарь → агенты
└──────┬───────┘
┌──────▼───────┐
│ LLM-роутер │ DeepSeek / Claude
└──────────────┘
Мост — отдельный сервис в той же docker compose-сборке, что и остальное ядро. Он не знает ничего про агентов. Он умеет три вещи: забрать событие из MAX, превратить его во внутреннее сообщение и отправить ответ обратно.
Бот работает на официальном 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.
Схема рабочая, пока пользователей единицы. Грабли, на которые мы наступили:
Алерт мог не отправиться, и в логе оставалась пустая ошибка [3] без ретрая. Человек висел в pending, а я не знал, что он вообще есть.
Алерт уходит в Telegram, а заявка живёт в MAX-мосте. Когда мост и Telegram-бот жили на разных хостах, approve-флоу рвался. Пришлось держать их вместе.
Сейчас доступ автоматический, с дневным лимитом на новых пользователей. Оба пути входа, bot_started и message_created, проходят одну и ту же проверку. Так доступ не зависит от того, с какого события человек начал: нажал «Начать» по ссылке или нашёл бота и сразу написал.
Это самая полезная часть всей интеграции, и она маркетинговая, а не техническая.
Каждая наша ссылка на бота несёт метку: ?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 пользователей за первые сутки.
Подробно роутинг я разбирал в прошлой статье, здесь только то, что изменилось и как это выглядит в цифрах.
Принцип прежний: по умолчанию отвечает дешёвая быстрая модель, на дорогую уходит только то, что под неё подходит по явным триггерам (сложный запрос, длинный контекст). Профильные агенты живут со своей моделью из устава.
За последние 30 дней распределение ответов такое:
DeepSeek V4.1 Flash — около 5 700 ответов, ≈84%;
Claude Sonnet 5.5 — 825 ответов, ≈12%;
остальное — DeepSeek через CLI и vision-модели.
Для бесплатного продукта, у которого дневная аудитория за неделю выросла с 13–21 до 82–111 человек, это не вопрос вкуса [4]: массовый поток должен быть дешёвым, а дорогая модель нужна там, где без неё ответ будет плохим.
Обратная сторона, которую мы видим в живых диалогах: дешёвая модель иногда отвечает в дизайнере английской служебной «преамбулой» перед делом. Новый пользователь, который пришёл за картинкой, видит это как поломку. Генерация картинок у дизайнера удаётся примерно в 87% случаев, остальное — сбои вида «попробуй позже». Это наш текущий технический долг номер один, потому что дизайнер оказался главным сценарием для пришедших из MAX.
Цифры рекламы за 2–5 ноября:
|
|
|
|---|---|
|
Показы |
11 178 |
|
Клики |
1 293 |
|
Расход |
6 964 ₽ |
|
Пришли в бота |
416 (MAX ≈310, Telegram ≈110) |
|
Написали хотя бы раз |
211 |
Около 17 ₽ за пришедшего и около 33 ₽ за написавшего.
Интересно, что за активного пользователя MAX не дешевле: группа «ИИ в MAX без VPN» вышла в 38 ₽, «Бесплатно — Telegram» — в 34 ₽. Разница в объёме. Моё объяснение, и это гипотеза, а не измерение:
Конкуренции мало. В Telegram ИИ-ботов сотни. В MAX человек, который ищет нейросеть, находит единицы.
Аргумент попадает в боль [5]. «Без VPN» отвечает на вопрос, который у пользователя MAX уже есть. В Telegram тот же аргумент не цепляет, там работает «бесплатно».
Порог входа. Бот в 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. Привлекать людей в него дёшево. А вот поводов возвращаться пока мало, и это следующая задача, уже не про мост.
Бот бесплатный. Ссылки с меткой этой статьи, по ней я посчитаю, сколько человек пришло с Хабра, и напишу в комментариях.
Telegram: https://t.me/LizaPetrovna_bot?start=habr_nov [6]
Если вы тоже делаете ботов под MAX, напишите в комментариях, как вы решали диплинки из встроенных браузеров. У нас это до сих пор самое хрупкое место.
Автор: mrPetrukovich
Источник [8]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/38056
URLs in this post:
[1] памятью: http://www.braintools.ru/article/4140
[2] зрение: http://www.braintools.ru/article/6238
[3] ошибка: http://www.braintools.ru/article/4192
[4] вкуса: http://www.braintools.ru/article/6291
[5] боль: http://www.braintools.ru/article/9901
[6] https://t.me/LizaPetrovna_bot?start=habr_nov: https://t.me/LizaPetrovna_bot?start=habr_nov
[7] https://max.ru/id9705235960_3_bot?start=habr_nov: https://max.ru/id9705235960_3_bot?start=habr_nov
[8] Источник: https://habr.com/ru/articles/1103862/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1103862
Нажмите здесь для печати.