Legan Studio
Все статьи
~ 7 мин чтения

Персонализация ответов в боте MAX: сегменты, контекст, ML

Как персонализировать ответы бота в MAX: сегменты, RFM, динамический контент, привет по имени, рекомендательные системы, поведенческий триггеринг и AB-тесты.

  • MAX
  • персонализация
  • маркетинг

«Здравствуйте» одинаково для миллиона пользователей — это не персонализация. Реальная персонализация в боте MAX — это когда бот знает, что Иван — постоянный клиент, заказывает каждые 2 недели, любит итальянскую кухню, и в 19:30 в пятницу ему уместно предложить пасту со скидкой 15%. В этой статье — как технически реализовать персонализацию: сегменты и RFM, контекстные обращения, рекомендательные системы (collaborative filtering, content-based, embeddings), поведенческий триггеринг и измерение влияния через A/B-тесты.

Уровни персонализации

УровеньЧто меняетсяСложность
0. Имя и контекст«Здравствуйте, Иван»минимальная
1. Сегментсообщения по группам (новичок, лояльный, ушедший)низкая
2. Поведениереакция на действия (бросил корзину)средняя
3. Рекомендацииtop-N товаров под пользователясредняя-высокая
4. ML/AILLM с памятью предпочтений и контекставысокая

Уровень 0: имя и контекст

async def greet(user_id: int) -> str:
    user = await get_user(user_id)
    name = user.name or "там"
    h = datetime.now(tz=user.tz).hour
    if 5 <= h < 12:  greeting = "Доброе утро"
    elif 12 <= h < 18: greeting = "Добрый день"
    elif 18 <= h < 23: greeting = "Добрый вечер"
    else: greeting = "Доброй ночи"
    return f"{greeting}, {name}!"

Уже это даёт +5–15% к open rate сообщений по сравнению с «Здравствуйте, дорогой клиент».

Уровень 1: сегменты и RFM

Классическая RFM (Recency, Frequency, Monetary) сегментация:

WITH rfm AS (
  SELECT 
    user_id,
    EXTRACT(EPOCH FROM (now() - max(created_at)))/86400 AS recency_days,
    count(*) AS frequency,
    sum(total) AS monetary
  FROM orders WHERE status = 'paid'
  GROUP BY user_id
),
scored AS (
  SELECT user_id,
    NTILE(5) OVER (ORDER BY recency_days DESC) AS r,    -- 5 = самые свежие
    NTILE(5) OVER (ORDER BY frequency)         AS f,
    NTILE(5) OVER (ORDER BY monetary)          AS m
  FROM rfm
)
SELECT user_id, r, f, m,
  CASE
    WHEN r = 5 AND f >= 4 AND m >= 4 THEN 'champions'
    WHEN r = 5 AND f <= 2             THEN 'new'
    WHEN r >= 4 AND f >= 3            THEN 'loyal'
    WHEN r <= 2 AND f >= 3            THEN 'at_risk'
    WHEN r <= 1                       THEN 'lost'
    ELSE 'regular'
  END AS segment
FROM scored;

Для каждого сегмента — свой сценарий. champions получают рекомендации новинок и закрытые акции, new — приветственную серию, at_risk — реактивацию со скидкой, lost — последний шанс или удаление.

Уровень 2: поведенческий триггеринг

События, на которые реагирует бот:

СобытиеРеакция (через N минут/часов)
Зашёл в каталог, не положил в корзину 30 минут«Подсказать что-то конкретное?»
Положил в корзину, не оформил 2 часа«Завершить заказ — кешбэк 5%»
Купил, через 7 дней«Как впечатления? Поставьте оценку»
Не открывал бот 14 дней«Соскучились! Что нового у вас?»
Отметил отрицательный фидбек«Сожалеем. Что можем исправить?»

Реализация — событийная архитектура с очередью отложенных задач (Celery / Redis queue / Postgres-based queue):

async def on_cart_abandoned(user_id: int, cart_id: int):
    # Через 2 часа проверим, оплатил ли
    await schedule_task("check_abandoned", payload={"user_id": user_id, "cart_id": cart_id}, delay=7200)

async def task_check_abandoned(user_id: int, cart_id: int):
    cart = await get_cart(cart_id)
    if cart and not cart.paid:
        items_str = ", ".join(it.title for it in cart.items)
        await bot.send_message(
            user_id,
            f"У вас в корзине: {items_str}.\nЗакончить заказ — кешбэк 5% по промокоду CART5",
        )

Уровень 3: рекомендательные системы

Content-based (проще): «вам нравится X, вот похожие». Векторные эмбеддинги товаров через эмбеддер (text-embedding-3-small / bge-m3 / GigaChat embeddings), kNN-поиск:

from qdrant_client import QdrantClient

qdrant = QdrantClient("localhost")

async def recommend_similar(product_id: int, n: int = 5) -> list[int]:
    emb = await get_product_embedding(product_id)
    results = qdrant.search(
        collection_name="products",
        query_vector=emb,
        limit=n + 1,  # +1 потому что вернётся и сам продукт
    )
    return [r.id for r in results if r.id != product_id][:n]

Collaborative filtering: «пользователи, похожие на вас, покупали Y». Implicit ALS / matrix factorization из библиотеки implicit:

import implicit
from scipy.sparse import csr_matrix

# user-item matrix: 1 если купил, 0 нет
matrix = csr_matrix(rows)
model = implicit.als.AlternatingLeastSquares(factors=64, iterations=20)
model.fit(matrix)

def recommend_for_user(user_idx: int, n: int = 10) -> list[int]:
    items, scores = model.recommend(user_idx, matrix[user_idx], N=n)
    return list(items)

Обновляйте модель раз в сутки на ночном пайплайне. Результат кешируйте в Redis на 24 часа.

Уровень 4: LLM с памятью

ИИ-бот с системным промптом, в который подставляются предпочтения пользователя:

async def build_system_prompt(user_id: int) -> str:
    profile = await get_user_profile(user_id)
    history = await get_recent_orders(user_id, limit=5)
    return f"""Ты — ассистент магазина X в боте MAX.
Пользователь: {profile.name}, {profile.city}, клиент с {profile.first_order:%Y-%m}.
Последние заказы: {format_orders(history)}.
Предпочтения: {", ".join(profile.tags)}.
Используй эти данные тонко: не цитируй, но учитывай при рекомендациях.
Тон вежливый, на «вы», по-русски."""

Тонкость — не «спалить» персонализацию: фраза «Иван, помню, в прошлый раз вы заказывали Х» — звучит креепи. Лучше — рекомендация в духе того прошлого заказа.

A/B-тесты персонализации

Без замера — невозможно отличить «персонализация работает» от «работает на интуицию».

def variant(user_id: int) -> str:
    return "A" if user_id % 100 < 50 else "B"

async def send_recommendation(user_id: int):
    if variant(user_id) == "A":
        recs = await recommend_for_user(user_id, n=5)         # ML
    else:
        recs = await top_selling(category="любая")            # baseline
    await bot.send_message(user_id, format_recs(recs))
    log_experiment("rec_v1", user_id, variant(user_id))

# через 7 дней
async def measure():
    await db.execute("""
        SELECT variant, count(*) AS users,
               sum(CASE WHEN clicked THEN 1 ELSE 0 END)::float / count(*) AS ctr,
               sum(revenue) / count(*) AS arpu
        FROM exp_rec_v1 GROUP BY variant
    """)

Минимум — статзначимость по chi-square test или z-test для пропорций. Для CTR обычно нужно 2000–5000 пользователей в каждом варианте.

Хранение профиля и предпочтений

CREATE TABLE user_profile (
    user_id BIGINT PRIMARY KEY,
    name TEXT,
    timezone TEXT,
    city TEXT,
    language TEXT DEFAULT 'ru',
    first_seen_at TIMESTAMPTZ,
    last_active_at TIMESTAMPTZ,
    preferences JSONB DEFAULT '{}',
    tags TEXT[] DEFAULT '{}'
);

CREATE INDEX ix_profile_tags ON user_profile USING GIN(tags);

tags обновляются автоматически на основе действий: купил веганский продукт → vegan; смотрел детские товары → parent; пишет на ночь → night_owl.

Privacy и 152-ФЗ

Персонализация — это работа с ПДн. Минимум:

  • согласие на обработку, в том числе для рекламных рассылок;
  • возможность отключить рекомендации (/no_recs);
  • право на просмотр своего профиля (/my_data) и удаление;
  • хранение и обработка — на серверах в РФ;
  • никаких ПДн в логах рекомендательных моделей.

Common pitfalls

  1. Слишком явная персонализация — «Иван, мы заметили, что вы давно не покупали» звучит как stalking.
  2. Рекомендации только на покупках — для нового клиента ничего нет, нужны warm fallback (популярное в категории).
  3. Сегмент «лояльный» + спам каждый день — отписки растут.
  4. Персонализация без AB-теста — тратите время на ML, который не даёт +1% к выручке.
  5. Тег «vegan» по одному веганскому продукту — false positive. Ставьте теги по 2+ совпадениям.
  6. Время отправки в UTC — в 3 утра локально пользователю.

Итого

Персонализация в боте MAX — это иерархия: имя и контекст приветствия (free), RFM-сегменты с разными сценариями (минимум, базовый ROI), поведенческий триггеринг по событиям (брошенная корзина, неактивность), рекомендательные системы (content-based на эмбеддингах в Qdrant + collaborative filtering на implicit ALS), LLM с памятью предпочтений на верхнем уровне. Каждый уровень — это отдельная итерация, замеряйте через A/B с 2000+ пользователей в варианте. Уважайте границы: 152-ФЗ, согласие, opt-out, не «креепи» персонализация. Окупаемость уровня 2 (поведенческий триггеринг) — обычно недели; уровня 3 (рекомендации) — пара месяцев; уровня 4 (LLM с памятью) — больше зависит от тонкости настройки и стоимости токенов.

Частые вопросы

С чего начать персонализацию в боте MAX?

Со самого простого: имя в приветствии и сегменты RFM. Это даёт +5–15% open rate и 10–25% к конверсии в продаваемых сегментах без ML и без сложной инфраструктуры. Достаточно SQL-запроса с NTILE по recency/frequency/monetary раз в сутки и материализованного представления user_segment. Под каждый сегмент пишутся свои сценарии: champions — закрытые акции, new — onboarding, at_risk — реактивация со скидкой, lost — последний шанс. После этого можно подключать поведенческие триггеры и ML-рекомендации.

Что такое RFM-сегментация и как её посчитать?

RFM — это сегментация по трём метрикам: Recency (давность последней покупки), Frequency (частота), Monetary (общая сумма). Для каждой считается NTILE(5) — квинтиль, где 5 — лучшие. Комбинация дает 125 сегментов, на практике их группируют в 6–8 рабочих: champions (R=5, F≥4, M≥4), loyal, new, at_risk, lost, regular. Считается одним SQL-запросом раз в сутки. Под каждый сегмент свой контент и периодичность контактов — это снижает отписки и повышает CTR в разы.

Какие рекомендательные алгоритмы выбрать для бота?

Для старта — content-based: эмбеддинги товаров (text-embedding-3-small, bge-m3, GigaChat embeddings) хранятся в Qdrant, рекомендации — kNN от последнего просмотренного товара. Не требует длинной истории покупок, работает с первого визита. Для зрелого магазина (10K+ активных покупателей) добавляется collaborative filtering через implicit ALS — «похожие на вас покупали». Гибридная схема (content + collaborative + popularity fallback) даёт лучший результат. Обновление моделей — раз в сутки ночью, кеш Redis на 24 часа.

Как реализовать поведенческий триггеринг?

Каждое значимое действие (вход в каталог, добавление в корзину, покупка, неактивность 14 дней) генерирует событие. Обработчик событий ставит отложенную задачу в очередь (Celery / Redis / PG-based queue) с delay. Через N времени задача проверяет, не выполнилось ли «целевое действие» (купил после брошенной корзины, открыл бот после 14 дней) — если нет, отправляет push. Для брошенной корзины оптимальный delay — 2 часа, для неактивности — после 14 дней с N+30 повторами.

Как замерить эффект персонализации?

Через A/B-тесты: бакетируете пользователей по user_id % 100, в варианте A работает персонализация, в B — baseline (общий контент / случайные рекомендации). Логируете показ, клик, конверсию, выручку с метаданными варианта. Через 7–14 дней считаете CTR, конверсию и ARPU по вариантам, проверяете статзначимость chi-square / z-test. Минимум для уверенного результата — 2000–5000 пользователей в варианте. Без A/B легко обмануться: «персонализация работает» оказывается шумом.

Что такое creepy personalization и как её избежать?

Когда персонализация воспринимается как слежка: «Иван, мы заметили, что вы вчера в 23:47 искали зимнюю резину» — даже если технически правда, эмоционально это пугает. Правила: не цитируйте действия пользователя дословно, не упоминайте время и точку; используйте контекст «непрямо» (рекомендуете товары категории, которую смотрел, без фразы «в той категории»); не сегментируйте по чувствительным критериям (религия, болезни, семейное положение); добавьте opt-out от персонализации. Граница «полезно — креепи» проходит примерно по тому, насколько неожиданно для пользователя ваше знание о нём.

Какие требования 152-ФЗ при персонализации?

Персонализация = обработка ПДн в маркетинговых целях. Нужно отдельное согласие на профилирование (галочка при регистрации/первом контакте, не «по умолчанию»). Возможность отключить персонализацию через /no_recs. Право на просмотр своего профиля и тегов через /my_data. Хранение профиля — на серверах в РФ. ML-модели на анонимизированных данных или на ID без раскрытия PII. Уведомление в РКН — обязательное при систематической обработке для рекламных целей. Штрафы 2026 года за нарушения — до 700 тыс. ₽ за инцидент, для повторного — несколько миллионов.