Привет! Это Иван Воробьев, ведущий инженер отдела базового качества рекомендаций в VK Видео (AI VK). 

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

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

Пайплайн рекомендаций

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

  • сбор данных о пользователе;

  • подбор документов‑кандидатов;

  • фильтрация;

  • ранжирование;

  • блендинг.

До внедрения стримов все этапы были общими. Кандидаты подбирались сразу для всего, что рекомендер должен сделать в запросе, дальше они проходили через одни и те же фильтры, ранжировались одним и тем же образом, и только в самом конце документы разбивались по меткам и смешивались в выдачу по квотам. То есть разделение задач в системе было, но происходило на последнем шаге.

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

Задачи внутри одного пайплайна

Среди задач, решаемых в рамках запроса к рекомендеру, есть следующие:

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

2. Исследование интересов пользователя. Показать что‑то новое, чтобы открыть новые темы. Например, если он лайкнул котика, это не повод показывать дальше одних котиков: стоит попробовать автомобили, спорт и другие темы, и посмотреть на реакцию.

3. Исследование документов. Показать пользователям новый или малоизвестный объект нужно, чтобы понять, кому его стоит рекомендовать дальше.

4. Поддержка авторов. Помочь новым и небольшим авторам найти свою аудиторию, чтобы платформа не замыкалась на тех, кто уже набрал охваты.

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

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

Связность и связанные с ней проблемы

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

На практике это выражалось так:

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

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

3. Эксперименты конкурировали за общие слоты. Разводить A/B по слоям так, чтобы они не пересекались на пользователях, умели и раньше. Проблема была в другом: один крупный параметр или флаг мог одновременно менять несколько смысловых частей рекомендера, поэтому эксперимент по одной задаче занимал слот в том же слое, что и эксперимент по другой. 

Разделение пайплайнов с помощью стримов

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

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

Разделить общий пайплайн на стримы можно по‑разному, в частности:

1. Вынести в отдельные стримы функциональные части. Например, упомянутые подбор по известным интересам и исследование интересов пользователя.

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

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

Конфигурация стримов

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

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

Польза стримов

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

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

Цена разделения

У разделения есть цена, и на неё стоит посмотреть отдельно. Несколько стримов потенциально создают дополнительные расходы: часть структур приходится создавать отдельно для каждого, а выполняются стримы последовательно.

Чтобы это компенсировать, под капотом сделали два изменения:

1. Кубики перестали пересоздаваться. Большая часть кубиков не создаётся заново ни между запросами, ни между стримами. Эта оптимизация полезна и без стримов, но со стримами она становится критичной.

2. Часть вычислений переиспользуется между стримами внутри запроса. Например, признаки: посчитанное для одного стрима не считается заново для другого. Это уже прямое сокращение накладных расходов на стримы.

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

Стримы на практике

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

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

Кроме того, новые механики в VK Видео теперь заводят сразу стримами. Вот несколько примеров.

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

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

3. Персональный стрим в рекомендациях к видео. Это рекомендации, которые человек видит при просмотре конкретного видео. Сначала добавили оценку, нужен ли персональный подбор для конкретного запроса, и вынесли его отдельным стримом. Реализация прошла в основном по конфигурации, хотя пару новых механик всё же пришлось добавить, а под задачу дополнительно обучили модели. По результатам экспериментов получили около 3% суммарного времени просмотра. Весь путь, вместе с несколькими итерациями экспериментов, занял квартал.

4. Стрим мягких подписок на главной. Задача продуктовая — помочь небольшим авторам находить свою аудиторию. Здесь тоже почти всё решилось конфигурацией, дописать пришлось только подготовку данных. В результате число авторов, у которых набралась аудитория хотя бы в тысячу мягких подписок, выросло на 15%, и на столько же выросло число уникальных авторов VK Видео.

Стримы изначально появились как механизм внутри отдельного рекомендера VK Видео, а затем были перенесены в Discovery‑платформу. Благодаря этому механизм в принципе доступен рекомендерам других продуктов платформы: подключение не требует отдельной интеграции, для использования стримов достаточно сконфигурировать их. Обратная совместимость при этом сохраняется: конфигурации без стримов продолжают работать как прежде.

В VK Клипах конфигурации для отдельных поверхностей тоже начали выносить в отдельные стримы. Разнообразие задач, решаемых в рамках одной поверхности VK Клипов, не столь велико, но различие конфигураций между поверхностями весьма значительно. Следующим на очереди стоит Дзен, а за ним и некоторые другие продукты.

Итоги стримов

Все изменения можно свести к двум состояниям:

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

2. Стало. Несколько стримов внутри того же рекомендера поверх общего набора кубиков, а результаты по‑прежнему собираются в одну выдачу: в рамках каждого из них кандидаты подбираются, фильтруются и ранжируются под конкретную задачу. 

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

1. Сцепленность набирается незаметно. Решение встроить очередную механику в существующий пайплайн почти никогда не выглядит ошибкой в тот момент, когда его принимают, поскольку так действительно дешевле. Цена появляется только в сумме и только через годы, поэтому предъявить её на ревью одного изменения не получается — её видно лишь по тому, что менять систему становится все более трудоемко.

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

3. Разделение ускоряет разработку, а не улучшает метрики. Качество растят механики, а не сама возможность их развести. Смысл разделения в другом: за тот же срок этих механик успевает появиться и доехать до продакшена заметно больше, и их становится проще мониторить и изменять.

А у вас внутри одного пайплайна тоже накопились разные по смыслу задачи? Расскажите в комментариях, как вы их разделяете и что при этом оставляете общим.

Комментарии (2)


  1. TheRaven
    20.10.2026 14:30

    Ребят, у вас там всё нормально? У вас пол-сайта нафиг не работает, а в блоге "всё хорошо прекрасная маркиза - глядите как мы подручиваем рекомендации".


  1. iBolitt
    20.10.2026 14:30

    Странно, почем нет рекомендаций на профильном для стримов вашем же сервисе ВК Видео Live? Хотя бы на уровне жанров игр. Смотришь стримы по определенным гоночным играм и в предложке показывать другие стримы по гоночным играм. Аналогично и по другим жанрам. На площадке есть четкое указание игра, по которым идет стрим. То есть жанры игр узнать не сложно.

    Площадке очень не хватает рекомендательных алгоритмов. Зритель не может знать все название игр. Но он смотрит определенные игры определенного жанра. Было бы отлично рекомендовать ему другие стримы игр этого же или аналогичного жанра.