Привет, Хабр! Меня зовут Виктор Плошихин, я руковожу ML‑лабораторией в Yandex Infrastructure, команде, которая создаёт платформу для разработчиков Яндекса. Мой коллега Константин Моксин @kamoksin работает разработчиком‑аналитиком в этой же лаборатории — и мы сами пишем очень много кода. Этой осенью мы запустили Yandex Code Assistant — помощник для работы с кодом — и открыли к нему бесплатный доступ в режиме тестирования на платформе Yandex Cloud. И нам было важно не просто научить нейросеть писать код, но и сделать так, чтобы разработчики были довольны работой этого помощника.
В статье расскажем, для чего мы сделали кодового ассистента, как начинали с нуля, и как замеряли его качество метрикой «Счастье разработчиков».
Любой продукт, основанный на коде, зависит от качества кода и скорости его написания. Многие процессы разработки направлены на то, чтобы обеспечить эти качество и скорость:
найм и развитие хороших специалистов;
код‑ревью такими же прекрасными специалистами;
использование навороченных IDE с индексацией кода;
покрытие кода тестами;
создание документации по проекту.
Всё это относительно простые способы. Да, в этих процессах замешаны люди, все склонны допускать ошибки, тем не менее, оно работает. Но недавно появились модные фантастические LLM и продукты на их основе, которые обещали существенно ускорить и упростить процесс написания кода и обучения разработчиков.
По сути все подобные продукты предлагают два инструмента. Наиболее используемый — это inline‑подсказки. Разработчик пишет код, ему серым шрифтом показываются какие‑то подсказки. Можно нажать Tab и принять подсказку, или нажать Esc и отвергнуть её. Это неплохо снижает количество рутинных действий, которые разработчик давно освоил.
Второй продукт — это чат, где можно спросить нейронку о более сложных вещах. Если наш разработчик сомневается, есть ли ошибка в большом фрагменте кода, можно спросить в чате, что здесь не так, фактически запросить ревью. Или можно попросить написать юнит‑тест.
Мы проанализировали собственный опыт, пообщались с коллегами‑разработчиками, выяснили, что inline‑подсказки правда востребованы, и в нашем продукте решили сосредоточиться именно на них. Сразу возник вопрос, как оценить полезность этих подсказок.
Мы обратились к доступным исследованиям. Вот, как оценивают использование таких инструментов в Accenture. Компания провела опросы среди своих разработчиков, насколько им полезен и удобен GitHub Copilot — один из наиболее известных инструментов.
Заметим, что здесь вообще нигде не говорится о деньгах, речь именно о счастье разработчика.
Сами создатели популярных кодовых ассистентов любят приводить эту статистику, говорить о «доказанной эффективности», «видимом улучшении». Но на самом деле все IT‑сервисы разные, и создающие их разработчики тоже разные, и пишут они на разных языках программирования. Кто‑то пишет код просто в Notepad, а кому‑то нужны навороченные IDE. Поэтому в случае любых подобных опросов важно понимать, кто именно в них участвовал.
Итак, наш проект начался в конце прошлого года со внедрения inline‑подсказок в работу наших внутренних команд в Яндексе. Как понять его успешность?
Начнём с верхнего, продуктового уровня. С одной стороны, когда мы провели кастдев и пообщались с кучей разработчиков, то услышали довольно разные ответы на вопрос, что им нужно от inline‑подсказок кодового ассистента. Такие ответы довольно трудно структурировать. Но с другой стороны, у нас есть очень понятная бизнес‑метрика — retention. В Яндексе нет принуждения к установке кодового ассистента. Нравится — используешь, не нравится — сносишь плагин. Такое голосование ногами: если на четвёртой неделе использования активность сохраняется, то пользователи считают продукт нужным для себя.
Так как мы сами пишем много кода, то хотели бы видеть законченные, не ломающие синтаксис подсказки. Желательно не очень длинные, чтобы не отвлекаться на их чтение, так как в процессе написания кода мы примерно представляем, что именно хотим написать, и важно написать это быстро.
Поэтому мы решили: давайте наш кодовый ассистент будет смотреть на текст, как на код. Построим из кода abstract syntax tree (AST) и на его основе будем предсказывать стейтменты — определённые конструкции с точки зрения языка программирования. С этой точки зрения все подсказки легко можно условно разделить на плохие и хорошие:
Первый пример — это хорошая подсказка, здесь показывается целиком завершенный стейтмент. А второй пример — не очень хорошая подсказка, так как стейтмент обрублен.
Есть ещё одно ограничение — скорость подсказок, нужно чтобы они выдавались за 500 мс, не больше.
Итак, мы определили target, в который будем учить модель — предсказание стейтментов. Как теперь проверить, что это действительно повлияет на retention?
У нашей бизнес‑метрики есть свойство: её нельзя посчитать быстро, надо ждать месяц. А двигаться вперёд надо быстро. Обычно для таких долгих метрик придумывают прокси‑метрику, которую можно посчитать в онлайне в рамках А/B‑эксперимента, и которая бы коррелировала с бизнесовой метрикой.
Но на старте проекта вся наша аудитория составляла чуть больше десятка early adopters, на которых retention считать нет большого смысла. Они и так пользуются продуктом даже с его текущим качеством.
Поэтому мы всерьёз озаботились вопросом офлайн‑приёмки качества моделей. Важно, чтобы метрика:
интуитивно отражала верность движения: чем больше значение метрики — тем лучше продукт;
охватывала разнообразный код;
считалась быстро;
была дешёвой.
Очень желательно, чтобы оценка проводилась без участия асессоров, автоматически. В противном случае это долго, дорого и самим асессорами зачастую трудно разобраться в чужом коде.
Кодовые LLM обычно оценивают по HumanEval и расстоянию Левенштейна. Мы их тоже смотрим, но этого немного не про наш кейс инлайн‑подсказок. HumanEval не подходит для нашего случая, потому что оценивает качество моделей в целом, а не конкретные подсказки в конкретном IT‑сервисе. Расстояние Левенштейна между кодом подсказки и оригинальным кодом тоже не очень подходит: одну и ту же логику можно закодить разными способами — в этом случае код будет корректный и рабочий, но расстояние большим.
Нам было важно, чтобы подсказка не сломала уже написанный код и бизнес‑логику. Поэтому для себя мы придумали метрику UnitTest, которую можно посчитать по коду из нашего репозитория. Идея проста:
Берём код юнит‑теста.
Находим пользовательский код, который проверяется этим юнит‑тестом.
Ставим воображаемый курсор разработчика в каком‑нибудь месте этого кода.
Маскируем ряд стейтментов справа от курсора и передаём в модель код выше курсора и код после этих стейтментов.
Просим модель предсказать замаскированные стейтменты.
Итоговый код с предсказанием модели пытаемся прогнать в юнит‑тесте в п.1.
Считаем долю успешно пройденных юнит‑тестов.
Чем это лучше расстояния Левенштейна:
мы предсказываем не просто синтаксически корректный код, а ещё и код, который выполняет бизнес‑логику программы;
мы можем прогонять эту метрику на больших объёмах тестов;
метрика вообще не требует человеческого вмешательства.
Поскольку у нас была маленькая продуктовая команда, то мы не могли позволить себе такую роскошь, как претрейн базовой модели. Нужно было файнтюнить уже существующие модели. А значит, нужно подготовить пул для обучения.
В качестве кода для файнтюнига использовался наш репозиторий. Для этого мы его немного подготовили:
Мы очищаем этот код от дублей, кода роботов, делаем какой‑то сэмпл.
Строим по коду AST.
Выделяем стейтменты и дальше делаем target.
Как мы формируем target? Так как в метрике UnitTest мы маскируем стейтменты, то мы хотим предсказывать стейтменты и потом оптимизировать метрику. Давайте в пуле для обучения мы тоже будем нарезать target в виде стейтментов.
Казалось бы, нам нужно сделать простую вещь: есть готовый код, и нужно по нему эмулировать какой‑то момент, когда его редактировал разработчик. Например, интуитивно, сделать split в произвольном месте и представить: тут стоит курсор, нужно продолжить следующий стейтемент. Как выяснилось по метрикам, такой вариант не очень хорошо работает.
Сработал другой подход. Для этого мы строим синтаксическое дерево с помощью библиотеки treesitter и выбираем стейтменты определённого типа. Берём не все подряд, а только экспериментально определённые стейтменты. Например, мы не рассматривали комментарии.
Далее уже с полученными стейтментами мы эмулируем действие пользователя. Тут два варианта:
Курсор стоит в начале стейтмента.
Курсор стоит в рандомном месте стейтмента.
И есть ещё один момент: иногда молчать лучше, чем говорить. Другими словами, нам надо уметь предсказывать пустой стейтмент, то есть не отвечать ничего.
Итак, с пулом для обучения более‑менее понятно. Текущая модель дообучена на популярных в Яндексе языках:
Python,
TypeScript,
С++,
YAML,
JSON,
Kotlin,
Java,
Go,
Swift,
Scala,
YQL/SQL.
Нам было важно проводить много экспериментов, которые бы обеспечили наше движение вперёд. Так, например, оказалось важно, чтобы модель умела в fill in the middle — когда LLM знает, что надо предсказывать текст посередине. В этом случае у нас есть текст выше курсора (префикс) и ниже курсора (суффикс).
Почему так: живой человек очень часто пишет код не сверху вниз, а «прыгая» от одного куска к другому. Какой‑то кусок написал в начале, потом прыгнул в середину и что‑то подправил там и так далее.
В итоге модели мы дообучили. Дальше нужно сделать runtime‑стек, где бы они применялись и пользователь видел ваши прекрасные подсказки.
Мы построили довольно простой и быстрый runtime‑стек. Но есть особенности.
Что интересно в первую очередь — у нас есть два бэкенда:
CPU‑бэкенд — шустрый и быстрый, где происходит вся бизнес‑логика. Здесь мы принимаем решение: имеет ли смысл вообще ходить за подсказкой, а если имеет, то дообогащаем контекст персональным кодом пользователя. Затем отправляем обогащённый контекст в GPU‑бэкенд.
На GPU‑бэкенде происходит инференс LLM. Результирующие ответы ранжируются первым бэкендом: если они проходят порог, то показываются пользователю.
Плагины в верхней части схемы — это просто прокси, которые берут код разработчика и отправляют его. Мы изначально придерживаемся парадигмы сосредоточения всей бизнес‑логики в бэкенде. Дело в том, что выкатка фиксов ошибок в плагинах — это на порядки более длительная история, чем выкатка фикса на бэкенде. Например, далеко не все пользователи в JetBrains включают автоапдейт, и посаженные в плагине баги могут жить очень долго.
В целом runtime‑контур получился довольно быстрый: в 99-м перцентиле мы укладываемся в ограничения в 420 мс.
Есть ещё одна важная деталь: после реализации runtime‑стека, у нас появилась возможность проверять качество моделей в онлайне. Как только в потоке появляются живые люди, которые пользуются продуктом, неплохо переходить с офлайн‑приёмки на онлайн — это всегда ближе к истинному состоянию продукта. Для этого мы проводим привычное в Яндексе А/B‑тестирование.
В таком тестировании важно выбрать иерархию метрик и понять, по какой метрике вы будете принимать ваш продукт. На старте мы выбрали главной Аccept Rate — отношение принятых подсказок к увиденным. Это интуитивная метрика. Есть и обратная ей — Discard Rate, который не должен расти быстрее, чем Аccept Rate.
В A/B‑экспериментах у вас есть экспериментальные выборки, в каждой из которых своя модель. Казалось бы — давайте разделим пользователей по этим выборкам и посчитаем Аccept Rate? Но не всё так просто.
На старте многих подобных экспериментов, как правило, очень мало пользователей: не хочется сильно раскручиваться, когда над качеством предстоит ещё много работы. Сначала хочется проверить всё на малых группах, но появляются вопросы, как сделать эксперименты статистически значимыми.
Разделение по пользователям для формирования выборок нам не подходит: выборки получатся слишком маленькие, или придётся долго ждать для накопления массы тестов. Поэтому мы пошли другим путём.
Один пользователь за сессию генерит много запросов в наш бэкенд. По сути, почти все нажатия на клавиши, если между ними более 400 мс — это такие запросы. Они позволяют набрать необходимое количество данных для тестирования, если разделять не по пользователям, а по запросам.
Мы показываем одному пользователю ответы от разных моделей в течении сессии и смотрим эффект. Сам эксперимент идёт, как правило, неделю, чтобы исключить сезонность в процессе написания кода.
Когда мы запустили онлайн‑эксперимент, то увидели, что с ростом Аccept Rate у нас растёт и Retention пользователей, о котором мы говорили в начале.
Здесь показано, как эксперимент развивался в первые четыре месяца этого года. Аудиторию мы старались растить постепенно, чтобы не выжечь её. Это заметно по последней строке, где MAU — это месячная аудитория нашего кодового ассистента относительно всех разработчиков Яндекса.
Также мы постепенно раскручивались в рамках сессии написания кода у разработчика: поначалу показывались лишь на 8–10% запросов.
Мы работали над качеством, растили Аccept Rate и расширяли поток запросов, на которые появляются подсказки. Но видно, что где‑то в марте‑апреле мы упёрлись в потолок. У нас вырос Аccept Rate, но случился отток аудитории.
Стало понятно, что Accept Rate — не совсем та метрика, которая отражает желания пользователей. Например, она не учитывает длину принятой подсказки. И в этом аспекте можно накрутить метрику: показать много коротких подсказок, бо́льшую часть из которых примут, вместо одной длинной, которую не примут. Настала пора поменять метрику.
В Яндексе мы стремимся повышать уровень счастья пользователей, поэтому и тут стали думать над такой метрикой. У нас получились такие вводные размышления:
Чем длиннее принятая подсказка — тем лучше. В идеале весь код надо написать за одно нажатие на Tab, так мы поможем лучше всего.
Принятие подсказки — это ненулевые трудозатраты. Поэтому само нажатие на Tab должно штрафовать модель. Должно быть невыгодно показывать короткие подсказки, которые можно было бы написать вместо нажимания на Tab.
Показ подсказки отвлекает. Нужно её прочитать, сделать какой‑то вывод. Так что все показы тоже штрафуются, всегда и независимо от действий пользователя.
Discard — это явный негативный фидбек. Discard должен штрафоваться сильнее, так как он раздражает и требует дополнительного нажатия на Esc.
В итоге удалось вывести такую формулу счастья разработчика:
Вот как мы её подбирали:
Берём выборки из уже прошедших онлайн‑экспериментов.
Определяем, какая выборка победила, а какая — проигравшая.
Магические коэффициенты подбираем с помощью линейной оптимизации параметров функции расчёта счастья на этих выборках.
При этом оптимизируем не дельту счастья, а логарифм дельты, чтобы разделять победителя и проигравшего на небольших дельтах. Так эксперименты будут быстрее набирать статистическую значимость.
В итоге был подобран вариант штрафов вблизи оптимума по чувствительности (дельта примерно −2%), но с максимальным штрафом за Discard. При этом все пары «победитель–проигравший» угадываются верно и статистически значимо с большим запасом.
После перехода на новую метрику мы начали её оптимизировать. Для понимания значений метрики счастья будем смотреть на привычные Аccept Rate и Retention, а за Baseline возьмём состояние на апрель. Вот что получилось дальше:
В июне мы вырастили счастье, но при этом уронили Аccept Rate. При этом кардинально выросла бизнесовая метрика retention!
В июле и августе мы решили подрастить Аccept Rate, не роняя счастье. Пожертвовав небольшой долей потока, мы набрали Аccept Rate в 20%. Это очень хороший результат.
После всех экспериментов нам захотелось прогнать эти метрики на других аналогичных продуктах. На этом нормированном графике видно, какой мы проделали путь, чтобы сравниться по метрике счастья пользователей с конкурентом А — одним из самых популярных в мире кодовым ассистентом.
Также провели сравнение с другим кодовым ассистентом, набирающим популярность в России. Эксперимент показал у него −100% к счастью и −50% к Accept Rate относительно нас.
Помните, как в начале мы говорили про метрики эффективности для бизнеса?
Мы пока не научились считать, сколько кода от Code Assistant попало в репозиторий, но посчитали сколько кода принимают разработчики в течение рабочего дня. Сегодня 15% от всего кода за день разработчик пишет с помощью ассистента, а в мегабайтах это 800 Мб в день. Решение протестировали тысячи разработчиков Яндекса, 60% из которых стали постоянными пользователями сервиса.
Итак, мы создали конвейер проверки гипотез и обучения моделей. С ноября прошлого года было около 3 000 запусков этого конвейера и более 40 внедрений в продакшн.
Но что еще нам дала работа над ассистентом? Конечно же опыт:
При создании нового всегда стоит думать, как это будет помогать вашим пользователям.
Важно определить продуктовую метрику.
Интуитивная и быстро рассчитываемая офлайн‑метрика — залог быстрого движения вперёд.
Если есть возможность — финальное решение о выкатке лучше принимать по онлайн‑метрикам.
Всегда стоит сомневаться в выборе приёмочных метрик.
Не стоит бояться менять метрики и сравнивать свой продукт с конкурентами.
Больше подробностей рассказываем на нашей ежегодной Practical ML Conf — увидимся там 14 сентября.