«Здравствуйте» одинаково для миллиона пользователей — это не персонализация. Реальная персонализация в боте MAX — это когда бот знает, что Иван — постоянный клиент, заказывает каждые 2 недели, любит итальянскую кухню, и в 19:30 в пятницу ему уместно предложить пасту со скидкой 15%. В этой статье — как технически реализовать персонализацию: сегменты и RFM, контекстные обращения, рекомендательные системы (collaborative filtering, content-based, embeddings), поведенческий триггеринг и измерение влияния через A/B-тесты.
Уровни персонализации
| Уровень | Что меняется | Сложность |
|---|---|---|
| 0. Имя и контекст | «Здравствуйте, Иван» | минимальная |
| 1. Сегмент | сообщения по группам (новичок, лояльный, ушедший) | низкая |
| 2. Поведение | реакция на действия (бросил корзину) | средняя |
| 3. Рекомендации | top-N товаров под пользователя | средняя-высокая |
| 4. ML/AI | LLM с памятью предпочтений и контекста | высокая |
Уровень 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
- Слишком явная персонализация — «Иван, мы заметили, что вы давно не покупали» звучит как stalking.
- Рекомендации только на покупках — для нового клиента ничего нет, нужны warm fallback (популярное в категории).
- Сегмент «лояльный» + спам каждый день — отписки растут.
- Персонализация без AB-теста — тратите время на ML, который не даёт +1% к выручке.
- Тег «vegan» по одному веганскому продукту — false positive. Ставьте теги по 2+ совпадениям.
- Время отправки в 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 тыс. ₽ за инцидент, для повторного — несколько миллионов.