RecSys · учебник
Тренажёр Виджеты Повторение О проекте Все главы ← Блендинг Соответствия →

Дополнение · X-алгоритм · страница 10 из 11

Конфигурация: как система меняется, не меняясь

Сто девяносто один параметр читается из внешней системы конфигурации, а не зашит в код. Это позволяет менять поведение ленты без выкатки — и это же делает вопрос «а что сейчас в проде» нетривиальным. Разбираем механизм, его цену и реальную историю одного параметра, разложенную по дням.

Коротко

  • 191 параметр объявлен макросом param!: имя, тип, ключ во внешней конфигурации, значение по умолчанию.
  • Дефолты в репозитории синхронизируются с продовыми отдельным скриптом — это способ сделать открытый код правдивым, а не декоративным.
  • Эксперименты ставятся через выключатель стадии: каждая стадия пайплайна имеет метод «включена ли», внутри которого читается параметр.
  • Опубликована история одного изменения по дням — от запуска A/B-теста до финального значения, включая то, что тестировалось и не поехало.
  • Открытый код не равен «вот что у вас сейчас»: часть трафика всегда в экспериментах, и дефолт отражает основное значение, а не единственное.

1. Как объявляется параметр

param!(FavoriteWeight, f64, "rust_home_mixer_favorite_weight", 0.5);
param!(AuthorDiversityDecay, f64, "rust_home_mixer_author_diversity_decay", 0.5);
param!(EnableInventoryHoldout, bool, "rust_home_mixer_enable_inventory_holdout", false);

Фрагмент из param.rs · код X, Apache 2.0, коммит 28e414f

Четыре части: имя типа в коде, тип значения, ключ во внешней системе конфигурации и значение по умолчанию. Читается параметр так: query.params.get(FavoriteWeight).

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

Почему параметры, а не константы

Разница между const FAVORITE_WEIGHT: f64 = 0.5 и параметром — это разница между «изменить значит выкатить новую версию» и «изменить значит поменять число в конфигурации».

Для рекомендательной системы это принципиально. Проверка гипотезы «ответы стоит ценить выше лайков» с константой означает: написать код, пройти ревью, собрать, выкатить на канарейку, раскатить, подождать, собрать метрики, откатить если плохо. Дни. С параметром — минуты, и на любой доле трафика.

Заодно решается проблема отката. Плохое изменение в коде откатывается новой выкаткой; плохое значение параметра — возвратом старого числа, что происходит мгновенно и не требует сборки.

Обратная сторона в том, что 191 параметр — это 191 способ изменить поведение системы, минуя ревью кода. Дисциплина смещается из кода в процесс работы с конфигурацией.

2. Как дефолты остаются правдой

Если значения живут во внешней системе, то числа в репозитории — просто заглушки, и открытый код показывает не то, что работает. Эту дыру закрыли явно: в README сказано, что по расписанию запускается скрипт, приводящий дефолты в репозитории к основным продовым значениям.

Это стоит оценить: без такого механизма публикация кода была бы почти бессмысленной для самой обсуждаемой его части — весов. Вы бы видели структуру и не видели чисел.

Чего этот механизм всё равно не даёт

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

Отсюда правильная формулировка того, что вы читаете в коде: «так работает для большинства пользователей сейчас», а не «так работает». Разница существенна, если вы пытаетесь объяснить поведение конкретной ленты конкретного человека.

И ещё: синхронизация идёт по расписанию, а не мгновенно. Между изменением в проде и его появлением в репозитории есть лаг.

3. Эксперименты через выключатель стадии

Мы уже видели этот механизм на странице про пайплайн: у каждой стадии есть метод enable(query). Теперь понятно, зачем он получает запрос — чтобы прочитать параметры, назначенные именно этому пользователю.

fn enable(&self, query: &ScoredPostsQuery) -> bool {
    query.params.get(EnableBidirectionalFollowHydration)
}

Фрагмент из BIDIRECTIONAL_BOOST_CHANGE.md · код X, Apache 2.0, коммит 28e414f

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

А поскольку обёртка вокруг стадии считает метрики автоматически, вместе с экспериментом бесплатно появляется и измерение: сколько раз стадия отработала, сколько кандидатов добавила или выбросила, сколько времени заняла.

4. История одного параметра по дням

В репозитории есть отдельный документ, разбирающий реальное изменение — то самое, которое широко обсуждалось летом 2026 года. Это редкая возможность увидеть не результат, а процесс.

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

ДатаЧто произошло
10 июля 2026Запущен A/B-тест: небольшой доле пользователей случайно назначены значения прибавки 5, 10, 15 или 20. У большинства значение 0, то есть механизм выключен. Параллельно тестируется вторая прибавка — к весу времени задержки
13 июля 2026По первым результатам значение 20 раскатано на многих пользователей. Эксперименты с 0, 5, 10 и 15 продолжаются на других долях
24 июля 2026По итогам экспериментов и обратной связи от пользователей значение снижено до 15

Проверим, чем это кончилось, по самому коду. В param.rs сейчас:

param!(
    BidirectionalFollowReplyWeightBoost, f64,
    "rust_home_mixer_bidirectional_follow_reply_weight_boost", 15.0
);
param!(
    BidirectionalFollowDwellWeightBoost, f64,
    "rust_home_mixer_bidirectional_follow_dwell_weight_boost", 0.0
);

Фрагмент из param.rs · код X, Apache 2.0, коммит 28e414f

Первое значение — 15.0, ровно то, чем закончилась история. Второе — 0.0: прибавка к весу задержки тестировалась в том же эксперименте и, как прямо сказано в документе, широко не поехала. Нулевой параметр в коде — это след несостоявшегося изменения, а не забытая строка.

Что здесь по-настоящему поучительно

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

Разберём механику этой жалобы, она красивая. Прибавка усиливает взаимные подписки. Усиливая их, мы автоматически ослабляем всё остальное — включая посты от незнакомых аккаунтов, которые в этот момент обсуждают событие мирового масштаба. Метрики вовлечения при этом могли оставаться прекрасными: люди активно общались со знакомыми. Проблема была не в вовлечении, а в том, что лента перестала выполнять функцию «показать, что происходит в мире».

Это ровно тот сюжет про прокси-метрики: оптимизируемая величина растёт, а продукт становится хуже по измерению, которого нет ни в одной метрике. И ровно тот довод про разнообразие, что офлайн-метрики всегда голосуют против него.

Обратите внимание и на то, что величина прибавки — 15 при базовом весе ответа 5.0, то есть для взаимных подписок вес ответа вырастает вчетверо. Это не тонкая настройка, а очень сильное вмешательство, и то, что его калибровали публично и по шагам, само по себе показательно.

Частые ошибки и подводные камни

На чём спотыкаются
  • Читать дефолт как «так работает у всех». Это основное значение; часть трафика всегда в экспериментах с другими.
  • Считать, что открытый код показывает текущее состояние в реальном времени. Синхронизация дефолтов идёт по расписанию, лаг есть.
  • Забывать, что параметры обходят ревью кода. 191 параметр — это 191 способ изменить поведение продукта, минуя обычный процесс проверки изменений.
  • Судить об изменении только по метрикам вовлечения. В разобранном случае метрики были хорошими, а лента перестала выполнять свою функцию — это выяснилось из обратной связи, а не из дашборда.
  • Не замечать нулевые параметры. Ноль в коде часто означает «пробовали, не поехало», а не «забыли убрать».

Вопросы с собеседований

Как организовать конфигурацию рекомендательной системы, чтобы можно было экспериментировать?

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

Второе: каждая стадия пайплайна получает выключатель, читающий параметр. Это позволяет экспериментировать не только со значениями, но и с наличием стадии — новый источник или фильтр включается на проценте трафика.

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

Цена подхода — дисциплина смещается из кода в процесс работы с конфигурацией: изменение поведения продукта больше не проходит ревью кода.

Если значения живут во внешней конфигурации, какой смысл в открытом коде?

Сам по себе — небольшой: вы увидите структуру и не увидите чисел. Поэтому в разобранном репозитории отдельно оговорено, что дефолты приводятся к основным продовым значениям скриптом по расписанию.

Важно правильно понимать, что это даёт. Формулировка «так работает для большинства пользователей на момент последней синхронизации» — верна. Формулировка «так работает» — нет: часть трафика в экспериментах, и между изменением в проде и его появлением в коде есть лаг.

Расскажите про случай, когда хорошие метрики скрывали проблему.

Хороший пример есть прямо в этом репозитории. Летом 2026 года ввели прибавку к весу вероятности ответа для постов от людей с взаимной подпиской: сначала значение 20, потом снизили до 15.

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

Мораль ровно та, что в разговоре про прокси-метрики: оптимизируемая величина росла, а функция продукта — показывать, что происходит в мире — деградировала, и ни одна метрика вовлечения этого не показывала.

Как понять, что параметр в коде стоит нулём: это выключено или сломано?

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

Общее в трёх случаях то, что ноль — это положение ручки, а не отсутствие ручки. Код механизма написан, протестирован и готов; завтра значение может стать ненулевым без единой строки изменений.

Шпаргалка одним экраном

Объявление

param!: имя · тип · ключ конфигурации · дефолт. Читается по запросу, а не глобально.

Сколько

191 параметр — 191 способ изменить поведение без выкатки.

Правдивость дефолтов

Скрипт по расписанию приводит их к основным продовым значениям.

Что дефолт не значит

Не «так у всех»: часть трафика в экспериментах. Плюс лаг синхронизации.

Эксперименты

Через выключатель стадии, читающий параметр. Работает и для значений, и для наличия стадии.

Кейс

Прибавка за взаимную подписку: 10 июля тест 5/10/15/20 → 13 июля раскатили 20 → 24 июля снизили до 15.

Почему снизили

Не метрики, а обратная связь: усиление взаимных подписок ослабило всё остальное.

Масштаб вмешательства

Прибавка 15 при базовом весе ответа 5.0 — вес вырастает вчетверо.

Нули в конфиге

Обычно «пробовали и не поехало» либо «голова учится, но в скор не входит».

Первоисточники