Product Evaluation: как измерить эффективность AI-подсказок
В предыдущих главах мы разобрали несколько AI-функций, которые помогают разработчику писать код. Autocompletion продолжает код рядом с кареткой. Next Edit Suggestions пытается предсказать следующее изменение. Inline Edit редактирует выбранный участок по инструкции. Commit Generation описывает подготовленный git diff.
Несмотря на различия, все эти функции устроены похожим образом: система предлагает некоторый результат, а пользователь решает, что с ним делать.
контекст
↓
LLM
↓
подсказка
↓
пользователь принимает, изменяет или отклоняет еёДо этого мы в основном обсуждали левую часть схемы: какой контекст передать модели, как построить промпт, какую модель использовать и как обработать её ответ. Но после появления подсказки начинается не менее важная часть жизненного цикла.
Пользователь может принять результат полностью, выбрать только его часть, отредактировать после принятия или через несколько минут удалить. Подсказка может оказаться правильной, но прийти слишком поздно. Или быть полезной, но настолько длинной, что разработчик потратит больше времени на проверку, чем на самостоятельное написание кода.
Поэтому качества модели недостаточно, чтобы оценить качество продукта.
Разберём, какие метрики можно использовать для AI-функций и почему простой доли принятых подсказок почти всегда оказывается мало.
Acceptance Rate
Начнём с самой очевидной метрики - Acceptance Rate, доли принятых подсказок:
[ AcceptanceRate = \frac{\text{количество принятых подсказок}} {\text{количество показанных подсказок}} ]
Представим, что пользователь реализует уже знакомое нам создание возврата:
import { returnService } from "@/services/returnService";
async function createReturn(request: ReturnRequest) {
// cursor
}Модель предлагает следующий код:
return await returnService.submit({
orderId: request.orderId,
reason: request.reason,
requestedBy: request.userId,
});Пользователь нажимает Tab. Подсказка принята, и событие попадает в статистику:
shown = true
accepted = trueЕсли за день система показала 1000 подсказок, из которых 300 были приняты, её Acceptance Rate составит 30%.
На первый взгляд кажется, что этого достаточно: чем чаще пользователь принимает результат, тем лучше работает функция. На практике эту метрику довольно легко улучшить, почти не увеличив пользу продукта.
Например, система может предлагать только короткие и очевидные продолжения:
);Такие подсказки часто принимают, но одно принятие экономит всего несколько нажатий клавиш. Другая модель может предлагать целые строки и блоки кода. Её Acceptance Rate будет ниже, хотя суммарно она напишет за пользователя больше.
Можно выбрать и другую стратегию: показывать подсказки только в тех случаях, когда модель почти уверена в ответе. Доля принятых результатов вырастет, но помогать система станет гораздо реже.
меньше показов
↓
остаются только очевидные подсказки
↓
Acceptance Rate растётОбратная проблема появляется, если продукт показывает подсказки слишком агрессивно. Абсолютное количество принятых результатов может вырасти, но вместе с ним вырастет количество подсказок, которые отвлекают пользователя и перекрывают код.
Acceptance Rate описывает только один переход: что произошло сразу после показа. Он не говорит, насколько длинной была подсказка, сохранилась ли она в коде и помогла ли быстрее выполнить задачу.
Что считать показом
Даже знаменатель в формуле Acceptance Rate не так прост, как кажется.
Пользователь печатает быстро. Подсказка может успеть появиться в редакторе на несколько миллисекунд, после чего исчезнуть из-за следующего введённого символа. Технически система её отрисовала, но пользователь не успел ни прочитать результат, ни осознанно его отклонить.
Если считать каждый такой случай показом, Acceptance Rate будет зависеть не только от качества подсказок, но и от скорости набора текста, debounce, latency модели и особенностей UI.
При измерении code completion внутри Google подсказка считалась показанной, только если оставалась видимой более 750 миллисекунд. Для видимых таким образом подсказок измерялись acceptance rate, средняя длина принятого продолжения и доля кода, добавленного с помощью модели.
У другого продукта порог может отличаться, но сам принцип остаётся тем же:
Пользователь может отклонить только ту подсказку, которую он действительно успел увидеть.
Поэтому между генерацией и принятием появляется ещё один этап:
подсказка сгенерирована
↓
прошла фильтры
↓
была показана достаточно долго
↓
пользователь мог принять решениеЗапрос к модели может устареть, результат может быть отфильтрован, а пользователь может продолжить печатать до появления ответа. Все эти случаи полезны для оценки системы, но не должны автоматически считаться осознанным отказом от подсказки.
Принятая подсказка не обязательно осталась в коде
Вернёмся к нашему примеру. Пользователь принял предложенный вызов:
return await returnService.submit({
orderId: request.orderId,
reason: request.reason,
requestedBy: request.userId,
});Затем заметил, что поля userId в request не существует, и изменил код:
const userId = authService.requireUserId();
return await returnService.submit({
orderId: request.orderId,
reason: request.reason,
requestedBy: userId,
});Подсказка была полезной: модель правильно выбрала метод submit, подготовила структуру аргумента и заполнила два поля. Но первоначальный результат не сохранился целиком.
В другом случае пользователь может принять подсказку, запустить тесты, обнаружить ошибку и полностью удалить добавленный код. Для Acceptance Rate эти два случая выглядят одинаково:
accepted = trueПоэтому после принятия нужно продолжить наблюдение за подсказкой:
shown
↓
accepted
↓
edited
↓
retained
↓
committedRetained означает, что хотя бы часть предложенного результата осталась в коде спустя некоторое время или после следующего значимого события: сохранения файла, запуска тестов, создания коммита.
Например, для автодополнения можно измерять не только количество принятых подсказок, но и количество принятых и сохранившихся символов. Именно accepted-and-retained characters, наряду с Acceptance Rate, количеством показов и latency, GitHub использует при A/B-тестировании моделей Copilot. Эта метрика исключает код, который пользователь сначала принял, а затем удалил.
Получается более длинная воронка:
eligible
↓
generated
↓
shown
↓
accepted
↓
retained
↓
shippedНа первом шаге возникает ситуация, в которой функция потенциально могла помочь. Затем система генерирует и показывает подсказку. Пользователь принимает её полностью или частично. Через некоторое время часть результата остаётся в коде, а затем попадает в commit или pull request.
Каждый этап отвечает на отдельный вопрос:
generated → смогла ли система построить ответ
shown → оказался ли ответ достаточно быстрым и допустимым
accepted → показался ли ответ полезным пользователю
retained → выдержал ли ответ последующую проверку
shipped → стал ли результат частью итогового измененияOffline-оценка
Собирать продуктовые метрики дорого. Для A/B-теста нужно выпустить новую версию реальным пользователям, дождаться достаточного количества событий и убедиться, что изменение статистически значимо.
Поэтому при разработке сначала используют offline evaluation: заранее подготовленный набор задач, на котором можно быстро сравнить модели, промпты и алгоритмы сбора контекста.
Простейший benchmark для code completion можно построить так:
берём готовый файл
↓
удаляем из него строку или блок
↓
передаём модели prefix и suffix
↓
сравниваем ответ с удалённым кодомНо точное совпадение с исходным фрагментом - слабая метрика. Одну задачу часто можно решить несколькими способами.
Например, исходный код использовал промежуточную переменную:
const result = await returnService.submit(command);
return result;Модель могла предложить эквивалентный вариант:
return await returnService.submit(command);Ответ не совпал с эталоном, хотя поведение программы осталось тем же.
Более надёжный способ - вставить предложенный код в проект и запустить тесты. Такой подход используется в RepoMasterEval - benchmark от исследователей ByteDance для оценки repository-level code completion. Из реальных Python- и TypeScript-репозиториев удаляются фрагменты кода, модель восстанавливает их с учётом контекста проекта, а корректность проверяется существующими и дополнительно усиленными тестами.
Особенно важен не сам способ построения benchmark, а то, как авторы проверили его полезность. В течение месяца они сравнивали изменения offline pass rate с изменениями acceptance rate реального продукта. Улучшения на RepoMasterEval положительно коррелировали с улучшениями в production. При этом результаты на более общих задачах вроде HumanEval не всегда отражали качество модели в реальном code completion.
Это даёт важное требование к offline-оценке:
Хорошая offline-метрика должна предсказывать изменение продуктовых метрик.
Высокий результат на популярном benchmark сам по себе не означает, что пользователи будут чаще принимать подсказки. Benchmark должен воспроизводить контекст, ограничения и типичные задачи конкретной функции.
Для code completion это может быть код вокруг каретки и контекст репозитория. Для Inline Edit - инструкция, выделенный участок и остальная часть файла. Для NES - последовательность предыдущих изменений. Для Commit Generation - реальный git diff и дополнительные данные, доступные продукту.
Offline и online evaluation
Offline- и online-оценки решают разные задачи.
Offline evaluation позволяет быстро проверять большое количество вариантов:
модель A или модель B
другой системный промпт
новый способ retrieval
другой context budget
новый post-processingНо она работает на заранее собранном наборе примеров. В нём нет реального пользователя, который спешит, отвлекается, меняет намерение, игнорирует UI или не доверяет слишком длинной подсказке.
Online evaluation показывает поведение функции в настоящем рабочем процессе, но проводить такой эксперимент для каждого изменения слишком долго и рискованно.
Поэтому эти подходы обычно соединяются в один цикл:
offline benchmark
↓
внутреннее тестирование
↓
A/B-тест
↓
анализ принятых и отклонённых подсказок
↓
новые примеры для offline benchmarkOffline-оценка отбрасывает заведомо неудачные решения. Внутреннее тестирование помогает найти заметные проблемы с UX и поведением модели. A/B-тест проверяет влияние на реальных пользователей. Неудачные production-примеры затем возвращаются в benchmark.
Так offline-набор постепенно начинает отражать реальные проблемы продукта, а не абстрактные способности модели.
Code Completion
Для code completion основной единицей взаимодействия является продолжение кода рядом с кареткой.
Offline можно измерять:
exact и partial match с исходным продолжением; синтаксическую корректность; возможность собрать проект; прохождение тестов; корректность используемых API; latency генерации.
Latency здесь является частью качества. Даже правильная подсказка бесполезна, если пользователь уже написал код самостоятельно. Sourcegraph при разработке Cody рассматривал задержку как одно из главных ограничений всего completion-пайплайна: retrieval, генерация и обработка должны укладываться во время между действиями пользователя.
Online можно измерять:
shown rate
acceptance rate
partial acceptance rate
accepted characters
accepted lines
retained characters
undo rate
p50 / p95 latencyКоличество принятых символов позволяет отличить полезный блок кода от принятой закрывающей скобки. Retention отделяет код, который действительно пригодился, от результата, удалённого сразу после принятия.
В качестве основной продуктовой метрики можно использовать, например:
[ RetainedCharactersRate = \frac{\text{принятые и сохранившиеся символы}} {\text{количество символов, введённых пользователем}} ]
Но использовать её отдельно тоже опасно. Модель может начать предлагать слишком длинные продолжения и увеличить количество сгенерированного кода ценой лишних ошибок и времени на проверку.
Поэтому рядом должны оставаться guardrail-метрики:
latency
hide rate
undo rate
доля некомпилируемых подсказок
количество показовОптимизируется не одно число, а баланс между объёмом полезного кода и стоимостью взаимодействия с системой.
Next Edit Suggestions
В code completion место следующей подсказки уже известно: она появляется рядом с кареткой.
Для Next Edit Suggestions системе нужно решить сразу три задачи:
когда предложить изменение
+
где предложить изменение
+
какой код предложитьПоэтому offline-оценку можно разделить на несколько частей.
Сначала проверяется позиция:
совпал ли файл
совпал ли диапазон
каково расстояние до реального измененияЗатем оценивается сам edit:
exact match
partial match
применимость patch
компиляция
прохождение тестовОтдельным правильным результатом должно быть отсутствие предложения. После некоторых действий пользователя никакого очевидного следующего изменения нет. Модель, которая всегда пытается что-то исправить, может иметь высокий coverage, но быстро начнёт раздражать.
Эта особенность хорошо видна в A/B-тестах GitHub Copilot NES. В одном из обновлений количество показанных подсказок уменьшилось на 24,5%, но Acceptance Rate вырос на 26,5%, а Hide Rate снизился на 25,6%. Функция стала вмешиваться реже, но её оставшиеся предложения оказались уместнее.
меньше подсказок
+
выше Acceptance Rate
+
ниже Hide Rate
=
более точный выбор моментаДля NES количество показов само по себе не является положительной метрикой. Система должна не максимизировать активность, а выбирать моменты, когда вероятность полезного следующего изменения достаточно высока.
Online можно измерять:
shown rate
acceptance rate
hide rate
переход к предложенной позиции
partial edit acceptance
retained edits
stale suggestions
latencyЕсли NES сначала перемещает пользователя к другой части файла, а затем предлагает edit, полезно разделять принятие позиции и принятие самого изменения. Пользователь может согласиться, что редактировать нужно именно это место, но написать код самостоятельно.
Inline Edit
В Inline Edit пользователь сам вызывает функцию, выбирает код и формулирует инструкцию:
выделенный код
+
инструкция
↓
предложенный patchЗдесь почти не нужно оценивать правильность момента показа: пользователь явно запросил изменение. Зато возрастает важность понимания инструкции и соблюдения границ редактирования.
Представим запрос:
Получи userId через authService и используй его в requestedByМодель может корректно изменить основную функцию, но заодно переименовать аргументы, переформатировать соседние методы или удалить комментарий. Тесты продолжат проходить, хотя patch содержит незапрошенные изменения.
Поэтому offline-проверка должна учитывать не только итоговое поведение:
выполнена ли инструкция
проходят ли тесты
изменён ли только нужный участок
не потерян ли существующий код
не появились ли побочные измененияBenchmark тоже должен воспроизводить реальный интерфейс функции. В EDIT-Bench задачи собраны из настоящих пользовательских запросов к инструменту редактирования. Модель получает не только инструкцию, но и текущий код, выделенный участок и позицию каретки. Изменение доступного контекста влияло на результат моделей вплоть до 11 процентных пунктов.
Если в продукте пользователь выделяет код, а offline benchmark передаёт модели только текстовую инструкцию, мы оцениваем уже другую функцию.
Online для Inline Edit можно измерять:
доля принятых patches
partial acceptance
regeneration rate
follow-up instructions
undo / revert rate
время до принятого результата
retention после commitПолезной метрикой становится расстояние между сгенерированным patch и итоговым кодом пользователя:
исходный код
↓
AI patch
↓
ручные исправления
↓
финальный кодЧем меньше изменений потребовалось внести после генерации, тем большую часть работы выполнила система.
Однако edit distance тоже не описывает всё качество. Пользователь может принять большой patch без изменений, но потратить несколько минут на его проверку. Поэтому для длинных Inline Edit важны время до подтверждения, количество просмотренных hunks и результаты последующих тестов.
Commit Generation
Commit Generation отличается от предыдущих функций тем, что генерирует не код, а текстовое описание изменений.
Для одного git diff может существовать несколько хороших сообщений:
feat: add return submissionfeat(returns): create returns through returnServiceAdd authenticated return creationСравнивать такой результат с единственным эталоном через точное совпадение бессмысленно. Метрики вроде BLEU или ROUGE тоже могут поставить низкую оценку хорошему сообщению только потому, что оно использует другие слова.
При этом Acceptance Rate для commit message трудно определить однозначно. Пользователь может нажать кнопку генерации, получить сообщение, изменить одно слово и создать commit. Это не полное принятие, но генерация явно была полезной.
JetBrains предложил использовать в качестве online-метрики количество изменений, внесённых пользователем между сгенерированным и реально закоммиченным сообщением. Затем исследователи проверили, какие offline-метрики лучше всего коррелируют с таким поведением. На их наборе данных edit distance показал наиболее высокую корреляцию, тогда как BLEU, METEOR, ROUGE и другие популярные метрики коррелировали с online-качеством слабо.
Для Commit Generation можно собирать следующие online-метрики:
commit без исправлений
количество добавленных символов
количество удалённых символов
normalized edit distance
regeneration rate
полная замена сообщения
отказ от генерации
время до создания commitНо небольшое количество правок ещё не гарантирует правильность текста. Пользователь может не заметить, что сообщение описывает изменение, которого не было в diff.
Поэтому offline evaluation должна дополнительно проверять:
соответствие сообщения реальным изменениям; отсутствие выдуманных фактов; наличие основной сути изменения; соблюдение формата команды; корректность type и scope; отсутствие лишних технических деталей.
Для этого можно использовать человеческую разметку, LLM-as-a-Judge с фиксированным набором критериев или структурированные проверки отдельных полей. Главное, чтобы offline-метрика была сопоставлена с реальными пользовательскими исправлениями.
От подсказки к продуктивности
Даже retained-код ещё не означает, что разработчик стал работать быстрее.
Модель могла написать десять строк, но пользователь потратил две минуты на их чтение. Или подсказка позволила быстрее закончить функцию, но добавила ошибку, которую обнаружили на code review.
Можно представить полезность AI-функции как разницу между сэкономленной работой и стоимостью взаимодействия:
Product Value =
saved work
- review cost
- correction cost
- interruption cost
- latency costТочно посчитать каждую часть сложно, поэтому продукт использует наблюдаемые приближения:
saved work → retained characters, edit distance
review cost → время до принятия, размер patch
correction cost → undo, follow-up edits, regeneration
interruption cost → hide rate, dismiss rate
latency cost → время от действия до показаСледующий уровень - метрики рабочего процесса:
время выполнения задачи
количество итераций build / test
время до успешной сборки
время до commit или pull request
количество исправлений после reviewНапример, Google при внедрении ML-based code completion измерял не только Acceptance Rate. В эксперименте с более чем десятью тысячами разработчиков функция имела Acceptance Rate 25-34%, создавала более 3% кода и сокращала среднюю продолжительность coding iteration примерно на 6%.
Это уже связывает взаимодействие с подсказкой и изменение реального рабочего процесса:
подсказки принимают
↓
часть кода остаётся
↓
между сборками проходит меньше времениПодобные метрики сложнее интерпретировать. На них влияют язык, тип задачи, опыт разработчика, размер проекта и множество других факторов. Поэтому их обычно используют не вместо продуктовой воронки, а вместе с ней.
Что измерять для разных функций
Для всех рассмотренных функций можно сохранить общую схему:
eligible
→ generated
→ shown
→ accepted
→ retained
→ shippedНо основная единица пользы будет отличаться:
Code Completion
→ принятые и сохранившиеся символы
Next Edit Suggestions
→ принятые и сохранившиеся edits
с учётом правильности позиции
Inline Edit
→ расстояние от AI patch
до финального пользовательского patch
Commit Generation
→ расстояние от сгенерированного сообщения
до реально созданного commit messageРядом с основной метрикой должны находиться ограничения:
Code Completion
→ latency, undo rate, compilation errors
Next Edit Suggestions
→ shown rate, hide rate, stale suggestions
Inline Edit
→ scope violations, regeneration, test failures
Commit Generation
→ factual errors, policy violations, abandonmentОдной универсальной метрики для всех функций нет. Но есть общий принцип: измерять нужно не только первый ответ модели, а весь путь результата через продукт.
Как построить оценку функции
Практический процесс можно разделить на несколько шагов.
Сначала определяем ожидаемую пользу:
какую работу функция должна экономить?Для completion это ручной набор кода. Для NES - поиск следующего места и внесение связанного изменения. Для Inline Edit - самостоятельное преобразование существующего кода. Для Commit Generation - написание сообщения.
Затем строим продуктовую воронку и собираем события:
invoked / eligible
generated
shown
accepted
edited
retained
committedПосле этого выбираем основную метрику и guardrails. Например:
основная метрика:
accepted-and-retained characters
guardrails:
latency
hide rate
undo rate
ошибки компиляцииСледующим шагом создаём offline benchmark, который воспроизводит реальный вход функции и проверяет те свойства, которые важны пользователю.
Наконец, связываем offline и online evaluation. Если рост benchmark score не приводит к улучшению production-метрик, значит benchmark измеряет не совсем ту задачу, которую решает продукт.
Получается замкнутый цикл:
пользовательские взаимодействия
↓
продуктовые метрики
↓
анализ ошибок
↓
offline benchmark
↓
новая версия системы
↓
A/B-тестВ предыдущих главах мы рассматривали, как улучшить подсказку: добавить контекст, найти определения через LSP, выбрать релевантные фрагменты, изменить промпт или использовать другую модель.
Теперь появляется способ проверить, действительно ли это улучшение помогло.
Хорошая AI-функция - не та, чьи ответы получают самый высокий score на абстрактном benchmark. И даже не обязательно та, чьи подсказки чаще принимают.
Она должна вовремя предложить изменение, которое пользователь сможет быстро проверить, почти не будет исправлять и сохранит в итоговом коде.
Источники
- RepoMasterEval: Evaluating Code Completion via Real-World Repositories - Qinyun Wu et al., 2024. Benchmark ByteDance для repository-level completion и исследование корреляции offline pass rate с production acceptance rate.
- The lifecycle of a code AI completion - Sourcegraph, 2023. Описание жизненного цикла Cody Autocomplete и роли latency, retrieval, генерации и A/B-тестирования.
- ML-Enhanced Code Completion Improves Developer Productivity - Google, 2022. Результаты внедрения code completion для более чем десяти тысяч разработчиков.
- The road to better completions: Building a faster, smarter GitHub Copilot with a new custom model - GitHub, 2025. Использование accepted-and-retained characters, acceptance, shown rate и latency для оценки completion.
- Evolving GitHub Copilot’s next edit suggestions through custom model training - GitHub, 2025. A/B-метрики для NES: shown rate, acceptance rate и hide rate.
- EDIT-Bench: Evaluating LLM Abilities to Perform Real-World Instructed Code Edits - Wayne Chi et al., 2025. Benchmark Inline Edit, собранный из реальных пользовательских инструкций и контекстов.
- Towards Realistic Evaluation of Commit Message Generation by Matching Online and Offline Settings - Petr Tsvetkov et al., 2024. Исследование JetBrains о связи offline-метрик с пользовательскими исправлениями commit messages.