RuntimeNodes: как мы, ML‑щики, стали писать рантайм. backend.. backend. ml.. backend. ml. runtime.. backend. ml. runtime. ttm.
RuntimeNodes: как мы, ML‑щики, стали писать рантайм - 1

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

Всё становится ещё сложнее по мере прихода к итеративным сценариям. Бизнес‑логика, которая готовит входные данные для LLM и обрабатывает её ответы, всё сильнее и сильнее срастается с самой моделью.

На примере LLM‑ответов в поисковой выдаче Яндекса расскажу, как мы ускорили такие эксперименты, научились работать с бизнес‑логикой как с ML‑артефактом и зачем нам для этого понадобились ещё один DSL и представление вычислений в виде графа.


Цикл разработки

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

Один из продуктов, над которыми мы работаем, — быстрые ответы Алисы AI в поисковой выдаче.

RuntimeNodes: как мы, ML‑щики, стали писать рантайм - 2

Типов таких блоков много. Для популярных запросов используются заранее рассчитанные ответы, но значительную часть быстрых ответов Алиса AI формирует в рантайме. И сегодня мы поговорим о разработке алгоритмов (или пайплайнов — чаще будем применять это слово, потому что работа идёт на относительно высоких уровнях абстракции) построения таких ответов. 

Чтобы получать качественные ответы, нам нужно выполнить ряд условий:

  • Определить, подходит ли запрос для ответа с помощью LLM. Модель должна не просто суметь сформировать ответ — этот ответ должен быть полезен пользователю.

  • Понять, где и как искать данные. Часто достаточно документов, которые уже попали в поисковую выдачу. Именно так был собран базовый набор документов для заметной доли запросов в тестовой выборке. Затем отдельные алгоритмы выбирают из них релевантные фрагменты и отсеивают лишнее. Это необходимо, потому что токенный бюджет на входе модели ограничен. Кроме того, сокращение средней длины контекста снижает стоимость инференса.

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

  • Выбрать и настроить модели. Сюда входят обучение моделей и подбор параметров их применения.

И это далеко не полный список. По сути, перед нами стандартный R&D‑цикл, который упрощённо показан на следующей схеме.

RuntimeNodes: как мы, ML‑щики, стали писать рантайм - 3

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

Этим циклом улучшения (или полной перестройки) у нас занимаются команды, состоящие, как правило, из ML‑разработчиков и аналитиков. Они берут разные стадии подготовки входов, или пайплайн целиком, или отдельные артефакты и улучшают их. Так что хорошее качество ответов для наших пользователей требует огромной работы, большего числа экспериментов и циклов улучшений.

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

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

RuntimeNodes: как мы, ML‑щики, стали писать рантайм - 4

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

Офлайн‑прототипы у нас обычно строятся поверх MapReduce‑хранилища. В Яндексе для этого используется YTsaurus. Пайплайны, как правило, описываются средствами Nirvana, а большая часть бизнес‑логики — на YQL и Python. 

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

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

Вторая боль: а мы вообще верно переписали? Очевидно, будет много различий, которые надо свести к единому знаменателю: скажем, по качеству нравится одна версия, а в рантайм попадает другая. И ладно дело было бы только в ошибках и багах, но иногда вскрываются и архитектурные ограничения: что‑то, что мы совершенно легко проделываем в офлайне, может вообще не переноситься в рантайм. Если такого рода архитектурные ошибки вскрываются, формально нужно возвращаться к переработке офлайн‑прототипа.

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

Третья боль заключается в том, что нам всё ещё нужен офлайн‑процесс, но который работает на рантайм‑мощностях. Потому что для оценки рантайма хочется воспользоваться теми же корзинами данных и теми же процессами оценки ответов, которые использовались на R&D‑стадии. Тема о том, как прокачать рантайм‑контур, очень большая, так что вернёмся к ней позже. Сейчас просто констатируем, что рантайм должен уметь становиться офлайном.

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

RuntimeNodes: как мы, ML‑щики, стали писать рантайм - 5

Какие ваши предложения?

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

Можно выделить четыре таких этапа:

  • «Одноурловые» ответы. Система создавала несколько вариантов суммаризации отдельных документов, а затем классификатор выбирал лучший.

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

  • Чат и контекст диалога. Ответы стали учитывать предыдущие сообщения пользователя. Возможно, кто‑то ещё помнит, что на этом этапе ответы показывались с баннером «Нейро».

  • Переформулирование запроса. Перед поиском документов система начала создавать одну или несколько альтернативных формулировок исходного запроса.

В среднем одна крупная перестройка происходила примерно раз в полгода.

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

Более того, многим уже стало понятно (и причиняло боль) одно свойство офлайн‑прототипов: в них нельзя «потыкать». Офлайн‑пайплайн мог запросто преобразовать 1000 вопросов в 1000 ответов, но обработка одного нового вопроса занимала почти столько же времени. Из‑за накладных расходов результат приходилось ждать не меньше десяти минут. Такая задержка мешала тестировать продукт — и тем, кто принимал продуктовые решения, и специалистам по оценке качества. В какой‑то момент ML‑команды начали «на коленке» разворачивать собственные упрощённые рантаймы в dev‑окружении.

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

  • одной машины перестаёт хватать;

  • нужны деплоймент‑циклы;

  • рантайм иногда падает и нужно разбираться, что упало и как это поднять;

  • прокачивать это невозможно;

  • ML‑щики начинают ковыряться в рантайме, вместо того чтобы обучать модели.

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

  • создавать serverless‑рантаймы, которые можно сразу запустить и «потыкать»;

  • обходиться без циклов деплоймента и легко создавать форки;

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

  • начинать работу без глубокого знания рантайм‑систем;

  • быстро проверять изменения: одна итерация должна занимать секунды, а не минуты.

А если дополнительно окажется, что:

  • пайплайны можно создавать без штрафа эксплуатационных характеристик — задержки ответа и других параметров;

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

Вряд ли списки выше что‑либо объяснили, так как там буквально написано: «Чтобы было всё хорошее и не было ничего плохого». Так что попробую описать идеальную картину, которая на бумаге могла бы сильно снизить указанные ранее боли (и привнести новые, но об этом позже):

  • Пусть у нас есть веб‑интерфейс.

  • В нём можно описать граф вычислений, в котором есть возможность:

    • сходить в какое‑то хранилище данных (RAG),

    • подготовить вход в LLM, в том числе вызывая функции на Python,

    • сходить в LLM (да и в целом использовать ML‑артефакты),

    • обработать результаты.

  • Полученный граф, написанный на DSL, с Python, можно запустить в этом же интерфейсе и получить результаты выполнения.

  • Эти же графы без полного переписывания можно подготовить для prod‑окружения.

Вот именно таким и получился RuntimeNodes:

RuntimeNodes: как мы, ML‑щики, стали писать рантайм - 6
RuntimeNodes: как мы, ML‑щики, стали писать рантайм - 7

Начиная с весны 2025 года наш флоу выглядит так:

  1. Команды ML‑разработчиков и аналитиков создают рантайм‑прототипы в RN. 

  2. R&D‑цикл происходит с этими прототипами.

  3. Рантайм‑команда:

    • улучшает различные аспекты RN,

    • реализует вычислительные узлы,

    • проверяет пайплайны на продакшен‑пригодность,

    • встраивает пайплайны в продакшен‑окружение.

По моим субъективным ощущениям, у нас получилось приблизить процессы сильно ближе ко второй картинке и уйти от первой.

RuntimeNodes: как мы, ML‑щики, стали писать рантайм - 8
RuntimeNodes: как мы, ML‑щики, стали писать рантайм - 9

Технический раздел: модель данных и примеры

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

Мы используем довольно простую модель данных, в которой есть две сущности — узлы данных и вычислительные узлы.

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

Например, можно определить начальное состояние графа из двух файлов — input.json и script.py:

datas:
// input.json
{
    "a": 1,
    "b": 2
}
/ /script.py
def sumFunc(inputName):
AddDataToResult(
    "sum",
    import_data[inputName]["Payload"]["a"] + import_data[inputName]["Payload"]["b"],
    []
) 

И одного узла:

{
    "NodeId": "DoSum",
    "NodeType": "Python",
    "NodeOptionsJsonStruct": {
        "Script": "rn_data_id://script.py",
        "FunctionToCall": "sumFunc('input.json')"
    },
    "RequireFinalDataIds": ["input.json"]
} 

При запуске графа выполнится его единственный узел: все условия для этого соблюдены. Узел добавит в стейт третий дата‑итем с именем sum и значением 3.

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

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

А минусы будут?

Во‑первых, подход подразумевает дополнительные расходы. Переход из режима offline‑first в режим runtime‑first требует не только создания слоя описания бизнес‑логики: чтобы система работала в рантайме, все её компоненты должны быть онлайн. В офлайн‑процессах инференс можно запускать как map‑операцию: необходимые ML‑артефакты поднимаются на время обработки одной таблицы, а готовые инструменты позволяют легко подключать их новые версии. При переходе к подходу runtime‑first эту простоту важно сохранить. Только вместо map‑операции разработчик должен так же легко поднимать рантайм‑эндпоинт с нужным набором ML‑артефактов.

Эту проблему мы начали решать ещё до работы над RuntimeNodes и в итоге предоставили необходимый инструментарий. Но переход к онлайн‑режиму предъявляет к нему дополнительные требования. Их поддержка увеличивает стоимость разработки и, что ещё болезненнее, эксплуатации.

Во‑вторых, нужно иметь в виду, что писать на DSL будет менее удобно, чем на каком‑то одном языке. Например, python‑интерпретатор, который доступен в RuntimeNodes, мы намеренно сильно урезали по возможностям: самое заметное — запрет сетевого взаимодействия. 

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

Ещё раз уточню предыдущую мысль: общий наш подход в RuntimeNodes — сознательное ограничение выразительности и свободы разработки. Мы фактически заставляем использовать не самые привычные способы делать вроде бы обычные вещи по‑другому. Например, вместо стандартных средств Python для сетевых вызовов нужно использовать управляемый слой платформы. Для продвинутых пользователей это может быть существенным минусом. 

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

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

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

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

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

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

Заключение

Я постарался рассказать, к чему нас привели попытки уменьшить процессные боли. Важно понимать, что наш пример — не панацея, а трейд‑офф, где мы размениваем одни проблемы на другие. У нас не самые типичные условия: большая команда машинного обучения, значительные инвестиции в R&D и постоянная потребность в экспериментах для повышения качества продукта. Именно при таком масштабе собственная платформа оказалась для нас выгодной.

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

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

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

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

Надеюсь, дочитавшие нашли какие‑то интересные для себя моменты во всей этой истории. В будущем, думаю, расскажем очередную серию о новых вызовах.

Автор: ilnurKh

Источник

  • Запись добавлена: 24.09.2026 в 07:01
  • Оставлено в