AI-персонализация email в реальном времени: технологии и реализация
«Привет, {имя}!» - не персонализация. Это подстановка переменной. Настоящая персонализация - когда ML-модель за миллисекунды собирает для каждого получателя собственную версию письма: товарные рекомендации, текстовые блоки, CTA. Разберём архитектуру, алгоритмы и конкретные паттерны реализации.
Что значит «в реальном времени»
Классическая персонализация работает в момент сборки кампании. Маркетолог создаёт сегмент, выбирает контент, нажимает «отправить». Контент определён до отправки и одинаков для всего сегмента.
Real-time персонализация откладывает решение до последнего момента. Два варианта: send-time - модель выбирает контент в момент отправки, и open-time - контент рендерится, когда получатель открывает письмо. Разница принципиальная. Send-time дешевле и совместим с любыми почтовыми клиентами. Open-time позволяет учитывать события, произошедшие между отправкой и открытием, но требует серверного рендеринга через прокси-изображения.
На практике send-time покрывает 90% задач. Open-time оправдан для задач вроде таймеров обратного отсчёта или актуальных складских остатков.
Слой данных: что нужно модели
Любая рекомендательная система начинается с данных. Для email-персонализации нужны три категории сигналов:
Профильные данные. Регион, язык, устройство, дата регистрации, тарифный план. Меняются редко, хранятся в основной БД. Запрос - прямой SELECT или кэш.
Поведенческие данные. Открытия писем, клики, покупки, просмотры страниц, время на сайте. Поступают потоком через события (Kafka, RabbitMQ, webhook). Здесь объём на порядки больше: один подписчик генерирует десятки событий в день.
Контекстные данные. Время суток, день недели, погода, текущие акции, складские остатки. Не привязаны к конкретному пользователю, но влияют на релевантность контента.
Собирать всё в одну таблицу - путь к хаосу. Стандартный подход - feature store: промежуточный слой, который агрегирует сырые события в готовые фичи и отдаёт их модели за миллисекунды.
Поток данных: от событий до фичей
Events (clicks, opens, purchases)
|
v
Message Queue (Kafka / RabbitMQ)
|
+--> Stream Processor (Flink / Spark Streaming)
| |
| v
| Feature Store (Redis + Postgres)
| |
| +-- user_features: avg_open_rate, last_purchase_days,
| | preferred_category, device_type
| |
| +-- item_features: category, price_bucket,
| | popularity_score, margin
| |
| +-- context: time_of_day, day_of_week,
| active_promo_ids
|
+--> Batch Pipeline (daily retrain)
|
v
Model Registry (MLflow / Vertex AI)
Feature store решает две проблемы: гарантирует, что модель в продакшене получает те же фичи, на которых обучалась, и обеспечивает скорость - Redis отдаёт вектор фичей за 1-3 мс.
Модели рекомендаций: три подхода
Collaborative filtering. Основа: пользователи со схожим поведением интересуются похожими вещами. Матрица «пользователь - товар» разлагается на два набора эмбеддингов (user embedding и item embedding), скалярное произведение даёт прогноз интереса. Работает, когда накоплено много взаимодействий. Плохо справляется с новыми подписчиками (cold start).
Content-based filtering. Модель смотрит на признаки контента и профиль пользователя. Если подписчик кликал на статьи про Python - предложим ещё Python-статьи. Не зависит от поведения других пользователей, легко объясним. Недостаток: замкнутость - модель не предложит ничего за пределами привычных категорий.
Гибрид (two-tower, wide&deep). Стандарт в продакшене. Одна «башня» обрабатывает пользовательские фичи, другая - фичи контента. Выходные эмбеддинги сравниваются через dot product или MLP. Wide-компонент ловит простые корреляции (категория + регион), deep-компонент - нелинейные паттерны. Обучение на исторических кликах и конверсиях.
Two-tower модель: архитектура
User Features Item Features
[open_rate, last_click_days, [category, price, popularity,
preferred_cat, device, ...] margin, freshness, ...]
| |
v v
+-------------+ +-------------+
| User Tower | | Item Tower |
| FC -> ReLU | | FC -> ReLU |
| FC -> ReLU | | FC -> ReLU |
+------+------+ +------+------+
| |
v v
user_embedding (128d) item_embedding (128d)
| |
+------------- dot product --------+
|
v
relevance score (0..1)
|
v
top-K items for email
На базе от 50 000 подписчиков two-tower даёт ощутимый прирост CTR по сравнению с правилами. Ниже этого порога часто хватает content-based подхода с ручными правилами в качестве fallback.
Сборка письма: от скора к HTML
Модель выдала top-K рекомендаций. Дальше - рендеринг. Архитектура зависит от объёма рассылки, но общий паттерн один.
Pipeline рендеринга
Campaign trigger (scheduler / event)
|
v
Recipient Queue (batch of user_ids)
|
v
+------------------+
| Personalization |
| Service |
| |
| 1. Fetch user | +---------------+
| features ----+------> | Feature Store |
| | +---------------+
| 2. Score items | +---------------+
| via model ----+------> | Model Service |
| | | (gRPC / REST) |
| 3. Apply rules | +---------------+
| (freq cap, |
| blocklist) |
| |
| 4. Render HTML | +---------------+
| template ----+------> | Template Eng. |
| | | (MJML / Jinja)|
+--------+---------+ +---------------+
|
v
ESP / SMTP relay (Postfix, SES, Mailgun)
Шаблон письма содержит плейсхолдеры для динамических блоков: товарная карусель, подборка статей, персональный оффер. Каждый блок - отдельный компонент в шаблонизаторе. Personalization Service заполняет их данными из модели и передаёт готовый HTML в ESP.
Ключевое требование - скорость. На рассылке в 100 000 писем при лимите в один час каждое письмо нужно собрать за 36 мс. Это достижимо, если feature store в Redis, модель обслуживается через gRPC с батчингом, а шаблонизатор предкомпилирован.
Пример: динамические блоки в шаблоне
<mj-section>
<mj-column>
<!-- Static header -->
<mj-text>Weekly digest for {{ user.first_name }}</mj-text>
<!-- Dynamic: top-3 product recommendations -->
{% for item in recommendations[:3] %}
<mj-image src="{{ item.image_url }}" alt="{{ item.title }}" />
<mj-text>{{ item.title }} - {{ item.price }} RUB</mj-text>
<mj-button href="{{ item.url }}?utm_content=reco">
View
</mj-button>
{% endfor %}
<!-- Dynamic: content block chosen by model -->
{% if user.segment == 'educator' %}
{% include 'blocks/latest_article.mjml' %}
{% elif user.segment == 'buyer' %}
{% include 'blocks/discount_offer.mjml' %}
{% else %}
{% include 'blocks/popular_items.mjml' %}
{% endif %}
</mj-column>
</mj-section>Шаблон один для всей рассылки. Контент внутри - уникальный для каждого получателя. ESP получает уже отрендеренный HTML.
Паттерны реализации
Precompute vs. on-the-fly
Два подхода. Precompute: ночной батч прогоняет модель по всей базе и сохраняет top-K рекомендаций в Redis. В момент отправки - только подстановка. Быстро, предсказуемо, но рекомендации устаревают за часы.
On-the-fly: модель вызывается для каждого письма в момент рендеринга. Рекомендации актуальны, но нагрузка на инфраструктуру кратно выше.
Компромисс: precompute с TTL 4-6 часов и on-the-fly fallback, если кэш протух. Для большинства рассылок четырёхчасовой давности рекомендации - допустимый trade-off.
Frequency capping
Модель оптимизирует релевантность, но не знает про усталость. Если один товар попадает в пять писем подряд, подписчик начнёт игнорировать рассылку. Frequency cap - обязательное правило поверх модели: не показывать один item чаще N раз за K дней. Реализуется через Redis-счётчики с TTL.
Fallback-стратегия
Модель не всегда может выдать рекомендации. Новый подписчик без истории, сбой сервиса, таймаут. Нужен fallback: глобально популярные товары, редакционная подборка, случайная выборка из каталога. Письмо должно уйти в любом случае - пустой блок рекомендаций хуже, чем неперсонализированный.
Метрики: что измерять
Персонализация - не цель, а инструмент. Измеряем то, на что она влияет.
CTR по рекомендательному блоку. Главная метрика. Сравниваем с контрольной группой, которая получает статичный контент. A/B-тест обязателен: без него невозможно отличить эффект модели от сезонности.
Revenue per email (RPE). Для e-commerce - основной бизнес-показатель. Персонализированные рекомендации увеличивают RPE на 15-35% по сравнению с ручными подборками. Но только если base rate конверсии уже здоровый.
Recommendation coverage. Какой процент каталога попадает в рекомендации. Если модель гоняет одни и те же 50 товаров - она переоптимизирована на популярность и не выполняет свою задачу.
Latency p95. 95-й перцентиль времени сборки одного письма. Если p95 уходит за 200 мс - рассылка на 100k будет идти часами. Мониторим и алертим.
Cold start: первые письма без данных
Новый подписчик - нулевая история. Collaborative filtering бесполезен. Что делать:
Источник подписки. Форма на странице «Кроссовки для бега» - подписчик с высокой вероятностью интересуется беговой обувью. UTM-метки, landing page, реферер - это уже сигнал.
Preference center. Спросить напрямую. Первое письмо после подписки - опрос: «Что вам интересно?» Два-три варианта, один клик. Конверсия таких опросов - 30-50%, если они короткие и вшиты в тело письма.
Lookalike-сегменты. Модель находит похожих пользователей по профильным признакам (регион, устройство, источник) и подставляет их усреднённые предпочтения. Грубо, но лучше случайного контента.
После 3-5 взаимодействий (открытия, клики) данных достаточно, чтобы модель перешла на персональные рекомендации. Первая неделя - зона компромиссов.
Фундамент: качество базы
Модель рекомендаций бесполезна, если 20% базы - мёртвые адреса. Вы тратите GPU-циклы на персонализацию писем, которые никто не получит.
Персонализация работает на здоровой базе. Невалидные адреса искажают метрики: CTR занижается (мёртвые адреса не кликают, но попадают в знаменатель), модель обучается на шумных данных, bounce rate растёт и убивает репутацию домена.
Три точки, где валидация критична:
При подписке. Real-time проверка через API. Не пропускаем одноразовые адреса и очевидный мусор. Модель начинает работу с чистыми данными.
Перед рассылкой. Bulk-валидация за 12-24 часа до отправки. Адреса деградируют: люди меняют работу, почтовые ящики удаляются, домены перестают принимать почту.
В обучающем датасете. Перед ретрейном модели фильтруем события, связанные с невалидными адресами. Иначе модель будет учиться предсказывать интересы несуществующих получателей.
Валидация в ML-пайплайне
Signup Form Bulk Campaign
| |
v v
+-------------------+ +---------------------+
| Real-time API | | Batch validation |
| (uChecker single) | | (uChecker bulk API) |
+--------+----------+ +----------+----------+
| |
v v
Valid? ----No----> Reject Valid? ---No---> Exclude
| |
v v
Feature Store Send pipeline
| |
v v
Training data Personalized email
(clean labels) (reaches inbox)
Чек-лист внедрения
Порядок имеет значение. Начинать с модели рекомендаций, не подготовив данные, - значит потратить месяц на инфраструктуру и получить результат на уровне случайной выборки.
- Очистить базу. Удалить невалидные адреса, одноразовые ящики, спам-ловушки. Без этого шага всё остальное - построение на песке.
- Настроить сбор событий. Открытия, клики, покупки должны приходить в единый event bus с user_id и timestamp.
- Собрать feature store. Агрегировать сырые события в фичи. Начать с простых: last_click_days, top_category, purchase_count_30d.
- Запустить content-based модель. Для старта хватит. Collaborative filtering добавить после накопления данных.
- Подготовить шаблон с динамическими блоками. Один-два блока для начала. Товарные рекомендации - самый понятный первый шаг.
- A/B-тест. Контрольная группа со статичным контентом. Минимум две недели, чтобы исключить сезонный шум.
- Мониторинг. CTR, RPE, latency p95, recommendation coverage. Дашборд, алерты.
Инструменты
Не обязательно строить всё с нуля.
Managed-решения дороже, но снимают инфраструктурную нагрузку. Для команды из 2-3 инженеров managed feature store и model serving - рациональный выбор. Модель рекомендаций часто имеет смысл держать свою: бизнес-логика слишком специфична для коробочного решения.
Типичные ошибки
Обучение на грязных данных. Модель впитывает шум из невалидных адресов. Bounce и отсутствие открытий воспринимается как негативный сигнал, хотя подписчик просто не существует. Результат - заниженные скоры для целых категорий контента.
Отсутствие fallback. Сервис модели упал - рассылка не ушла. Или ушла с пустыми блоками. Fallback не опционален.
Переоптимизация на CTR. Модель научилась подсовывать кликбейт. CTR вырос, RPE упал, отписки увеличились. Оптимизируйте на downstream-метрику: покупку, активацию.
Игнорирование privacy. GDPR, ФЗ-152. Персонализация на основе поведенческих данных требует согласия. Данные для обучения модели - персональные данные. Правовой аудит перед запуском, а не после.
Итого
AI-персонализация email - не магия и не маркетинговая опция. Это инженерная система: feature store, ML-модель, рендеринг-пайплайн, мониторинг. Она даёт 15-35% прироста к кликам и конверсии, но только при чистых данных, здоровой базе и корректной инфраструктуре.
Начните с фундамента: чистая база, надёжный сбор событий, простая content-based модель. Усложняйте по мере роста. Two-tower и real-time инференс - это следующий шаг, не первый.
Перед тем как строить персонализацию - проверьте базу. uChecker покажет, сколько адресов в вашем списке реально живые, а сколько - шум, на котором модель будет учиться впустую.
