Я сейчас работаю над автоматизированной системой A/B-экспериментирования заголовков и/или обложек статей и новостей на одной медиа-платформе в одиночку. Решил рассказать вам, как эта система работает и показать некоторые технические нюансы. Сразу оговорюсь, что название и сферу упоминать не стану, система находится в разработке, но есть, что рассказать.

Немного об экспериментах

Если вы писали статьи или работали с контентом, то наверняка сталкивались с ситуацией: текст неплохой, картинка симпатичная, но кликов почти нет. Выходит другой материал - вроде, ничем не лучше, но у него в разы выше трафик. Часто всё упирается в заголовок и обложку. Один заголовок может "выстрелить" и привести тысячи читателей. Редакции и авторы давно экспериментируют вручную: пробуют разные формулировки, меняют скриншоты или иллюстрации, часто это последовательные тесты. Но на больших потоках контента это превращается в хаос и споры "какой вариант лучше". Субъективность редакторов говорит одно, данные показывают другое. Стоимость промаха: неэффективный заголовок - статью не заметили.

Заголовок и визуал вместе формируют первое впечатление о статье: заголовок задаёт смысловую рамку и интригует, обложка усиливает его через эмоциональный контекст и визуальный сигнал. Лаконичный заголовок с минималистичной обложкой или яркий арт с цепляющей формулировкой - вариативность делает заголовки и визуал инструментом роста метрик.

Идея A/B-тестирования заголовков и обложек давно используется в крупнейших медиакомпаниях, например:

  • The New York Times ещё в 2010-х годах активно экспериментировал с десятками вариантов заголовков к одной и той же статье. Иногда редакция ставила десятки вариаций, чтобы найти оптимальную. Редакция регулярно тестирует различные формулировки, чтобы определить, какие из них привлекают больше читателей. Однако важно отметить, что NYT избегает использования кликбейта и придерживается высоких редакционных стандартов при проведении таких тестов;

  • Medium встроил A/B-механику прямо в платформу, авторы могли публиковать несколько вариантов заголовка, а система автоматически подбирала тот, что лучше работает для аудитории.

  • BuzzFeed, Washington Post, Bloomberg и другие крупные редакции делают ставку на "редакционную аналитику" - у них есть отдельные команды, которые занимаются только тестами под разные сегменты читателей.

Для них A/B-тесты - это уже не "интересный эксперимент" - повседневная практика.

Вариантов экспериментирования множества, но в статье вам расскажу о не совсем стандартном инженерном подходе и конкретном контексте.

A/B-эксперимент — это метод оценки эффективности изменений в продукте или сервисе, при котором аудитория делится на несколько когорт пользователей, которым показываются разные варианты (или одинаковые, в случаях A/A-экспериментов) контента.

A/B-эксперимент.
A/B-эксперимент.

Контент делится на несколько видов:

"Вечнозелёный" контент - направлен на долгосрочную эффективность. Такие материалы могут приносить трафик месяцами или годами, поэтому важно выбирать заголовки и визуалы, которые сохраняют привлекательность и релевантность в течение долгого времени. Метрики тестирования включают не только CTR, но и, например: удержание, глубину просмотра, органический поиск и повторные визиты. В данном виде контента подойдут стандартные A/B-эксперименты.

Новости - обычно ориентированы на быстрый эффект: новости живут короткий срок, и важно быстро определить, какой заголовок эффективен в первые часы или дни после публикации, где ключевыми метриками будут, например клики, охват и вовлечение в краткосрочной перспективе. Тесты проходят быстро, результаты часто используются для оперативных редакционных решений. В таких случаях нужны бандиты, которые позволят решить задачу минимизации затрат от варианта с отрицательно положительной эффективностью.

Multi-armed Bandits - это вариант эксперимента, у которого трафик динамически перераспределяется на вариант с лучшими результатами на основе промежуточных результатов (быстро оптимизирует результат). Но есть и некоторые ограничения, например сложнее оценить точный эффект каждого варианта и не всегда подходит для статистически чистых выводов, интерпретация сложнее, трудно формально доказать эффективность отдельных вариантов (совершить ошибку, на самом деле, не сильно страшно в новостных заголовках поэтому, этот риск с низким приоритетом и "не научность" в данном контексте, не важна).

Редакции и продуктовые команды используют A/B-тесты заголовков и обложек, чтобы снять главный риск контентных проектов - неопределённость реакции аудитории. По сути, A/B-тесты дают обеим сторонам общий язык: данные вместо догадок.

  • Редакции получают инструмент, который снимает споры "нравится/не нравится", и дают конкретные цифры. Это помогает авторам понять, как их тексты воспринимают в реальности, не "в голове редактора".

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

Целей множество, например:

  • Увеличение вовлечённости

  • Повышение CTR (click-through rate)

  • Оптимизация редакционных ресурсов

  • Поддержка продуктовых метрик

  • Снижение риска редакторской ошибки

  • Долгосрочная аналитика

Есть множество ограничений и о них я упомяну далее в статье.

Процессы в системе

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

Worflow публикации.
Worflow публикации.

При ручной/полуавтоматической настройке, редактору необходимо подобрать: время эксперимента (продолжительность, даты начала и окончания), объект тестирования (заголовок и/или обложка), вариант эксперимента (бандитский, многофакторный, стандартный), генеральную оптимизационную метрику, модель оптимизации метрики, модель оценки, варианты объекта, вариант подтверждения эксперимента (LLM, ручной, LLM с подтверждением), распределение на когорты (holdout-когорта, в которой будет запущен стандартный A/B эксперимент, "бандитская" аудитория (основной трафик) и когорта заблокированных (для других экспериментов) пользователей и других настроек.

Немного подробнее про генерацию вариантов и остальных настроек (если необходимо): в LLM отправляется специальный промпт, в который передаётся контекст (например, текст новости, активные эксперименты, история экспериментов), модель возвращает JSON объект с данными, которые парсятся в настройки. При генерации вариантов, надо учитывать важный аспект - этику, LLM может сгенерировать неприемлемые варианты (контролировать можно при настройке).

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

Распределение аудитории.
Распределение аудитории.

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

Далее сбор данных, логирование пользовательских действий и сбор метрик, например:

  • время чтения;

  • количество просмотров страницы;

  • процент чтения;

  • действия;

  • и множество других.

На данной задаче, возникает несколько технических аспектов, которые необходимо контролировать: кэширование (CDN может эксперименты сломать), боты.

Далее система анализирует результаты и принимает решение и делает это LLM (на основании промпта, система парсит JSON и делает определённые действия, в зависимости от ответов модели, записанных в системном промпте), либо отправляется в бота Telegram оповещение о завершённом эксперименте (с необходимыми данными) и с возможностью принять решение.

Как выполняется анализ:

Сначала идёт анализ стандартными методами, типа t-теста. Есть несколько тестов:

  • z-тест - часто применяется для процентных метрик, когда размер выборки достаточно большой и можно приблизительно оценить дисперсию.

  • t-тест - используется для непрерывных метрик (например, количество просмотров), но требует относительно нормальное распределение метрики.

  • Биномиальный тест - применяется для дискретных бинарных событий (клик / не клик, регистрация/нет) и особенно полезен при малых выборках.

Далее, проверка стабильности результатов методом bootstrap (но не совсем), где формируются выборки по определённой формуле, но чтобы не грузить математическими формулами, скажу: достаточно эмпирически делать выборку 10-30% от генеральной совокупности.

На самом деле система немного сложнее, например в определённых условиях, может применяться Mann-Whitney или Welch’s t‑test.

Для понимания приведу примерный код:

def compare_groups(df0: pd.DataFrame, df1: pd.DataFrame, label: str | None = None):
    results = {}

    for metric in df0.columns:
        try:
            t_stat, p_val = stats.ttest_ind(df0[metric], df1[metric], equal_var=False)

            results[metric] = {
                'group_0_mean': df0[metric].mean(),
                'group_1_mean': df1[metric].mean(),
                't_stat': t_stat,
                'p_val': p_val
            }

        except Exception as e:
            results[metric] = { 'error': str(e) }


    return pd.DataFrame(results).T

def bootstrap_compare(df0: pd.DataFrame, df1: pd.DataFrame, sample_size: int, iterations: int = 1024):
    comparisons = {}

    for i in range(iterations):
        g0_sample = df0.sample(n=sample_size, replace=False)
        g1_sample = df1.sample(n=sample_size, replace=False)

        comparisons[f'group_0_{i}, group_1_{i}'] = compare_groups(g0_sample, g1_sample)

    return comparisons

После завершения, система выполняет post-hoc анализ для других важных метрик, например: rolling/retention (определённых периодов), финансовые метрики. Удаляет пользовательские бакеты, чтобы они не блокировались для других экспериментов.

Пример отчёта.
Пример отчёта.

Упрощённая архитектура системы:

Упрощённая архитектура системы.
Упрощённая архитектура системы.

На рынке есть множество сервисов, которые позволяют проводить A/B-тесты без необходимости писать собственный движок, но все они не подходят по некоторым причинам, например: не все собирают необходимы метрики, некоторые не дают нужного уровня интеграции, ограничения на технологии (FastAPI и PostgreSQL). Среди классики - Optimizely, PostHog, которые предлагают гибкую настройку экспериментов и глубокую аналитику. Для новостных и контентных проектов удобны решения вроде Headline Studio, где можно тестировать разные формулировки и превью прямо в дистрибуции. Если статья публикуется в CMS, часто помогают плагины и расширения, например: Nelio A/B Testing. Для крупных редакций нередко интегрируются кастомные модули поверх Google Analytics. Выбор зависит от масштаба: для редакции с десятками статей в день подойдут специализированные инструменты, для небольшого блога проще взять готовый плагин или собирать аналитику через встроенные механизмы CMS.

Научности может мало, но даже "идеальный" A/B‑тест может быть искажён.

Но это всё ограничивается экспериментированием над определёнными и "захардкоженными" объектам, но у меня есть понимание, как сделать систему, которая сможет экспериментировать с теми объектами, которые посчитает нужными (например, отображением контента). Если есть инвестиции и предложения, я рассмотрю, пишите Telegram @ivanveriga. Или если есть работа, к слову, я сейчас ищу.

Спасибо за прочтение.

Пишите комментарии, что не хватает или ваше мнение.