Ежемесячно клиентская поддержка продуктов Яндекса обрабатывает миллионы обращений. Мы регулярно проверяем диалоги вручную. Это помогает бороться, например, с опечатками и другими ошибками операторов. Но проверить все диалоги в таком режиме невозможно — их слишком много. Поэтому мы решили посмотреть в сторону LLM‑решений.

Привет! Меня зовут Дарья Шатько, я руководитель ML‑группы в Yandex Crowd. В этой статье я расскажу, как мы с моим коллегой Антоном Удаловым внедряли большие языковые модели в контроль качества клиентской поддержки. А именно — почему регулярки и BERT не взлетели, как мы собрали репрезентативный golden‑датасет, как победили лимит контекста, снизили ложные срабатывания через многоступенчатый LLM‑flow и в итоге покрыли проверками абсолютно все диалоги поддержки.

Что такое Yandex Crowd

Yandex Crowd — это инфраструктурный сервис Яндекса, который помогает разным командам компании ускорять продуктовые запуски, масштабировать процессы, повышать качество продуктов и оптимизировать затраты. Одно из направлений работы Yandex Crowd — поддержка пользователей более чем 70 сервисов Яндекса, в числе которых Яндекс Афиша, Яндекс Путешествия, Плюс и другие. Сотрудники службы поддержки обрабатывают в среднем 4,5 млн обращений в месяц, 4500 операторов ведут диалоги ЛИБО: в трёх каналах: это чаты, письма и звонки.

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

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

  • Через некоторое время после закрытия диалога другой специалист приходил оценить качество этой коммуникации.

  • 120 сотрудников вручную обрабатывали порядка 3% общей выборки из 4,5 млн диалогов в месяц.

  • Проверки проводились по чек‑листу, который включал в себя 12 категорий и более 80 классов проверки на каждый диалог (например: категория «Знание продукта», класс ошибки «Не задали нужные вопросы»).

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

Для проверяющего специалиста последовательность действий была такая: 

  • найти ошибку и определить категорию и класс;

  • выделить участок сообщения, на котором она была допущена;

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

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

Вот как могут выглядеть такие ошибки:

Категория ошибки: ошибка / опечатка / название компании в использовании имени. В этом примере сотрудник опечатался в имени кандидата. Комментарий по ошибке выглядит так: Допустили опечатку при написании имени пользователя. Написали «Игонрь» вместо «Игорь»
Категория ошибки: ошибка / опечатка / название компании в использовании имени. В этом примере сотрудник опечатался в имени кандидата. Комментарий по ошибке выглядит так: Допустили опечатку при написании имени пользователя. Написали «Игонрь» вместо «Игорь»

Какую задачу мы хотели решить с помощью модели

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

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

Что мы пробовали до LLM

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

Пример:

def contains_phrase(text, phrase):
      return bool(re.search(rf'(?<!\w)(?<!-){re.escape(phrase)}(?!-)(?!\w)', text,         re.IGNORECASE)) 

В данном случае phrase — это одно из стоп‑слов, которые мы хотели бы найти в text.

На следующем этапе мы протестировали различные подходы к классификации, включая иерархические модели, а также классификаторы на основе архитектуры BERT. Однако ключевым ограничением стало недостаточное количество данных для редких классов ошибок. Это не позволило нам достичь на тестах значения macro‑F1 выше 65%.

macro‑F1 — это среднее значение по метрикам F1, полученным для всех классов, в котором все классы имеют равный вес (то есть не учитываем частоту класса). Хорошо подходит для оценки качества в несбалансированных датасетах, чтобы справедливо учесть вклад даже редких классов. 

Мы сочли это значение малым и решили, что пришло время для LLM.

Как выглядел baseline в начале пути

Работа с LLM обычно начинается с подготовки промпта. Мы собрали один комплексный промпт, который включал полный текст диалога и список из 20 критериев оценки. Каждый критерий соответствует одному из интересующих нас классов ошибок. Для повышения точности мы дополнили промт few‑shot‑примерами, по 3–5 примеров на каждый класс. После скоринга выборки сообщений с помощью этого пайплайна результаты передавались на ручную верификацию нашим специалистам для оценки качества работы LLM. 

С какими ошибками столкнулись на бейзлайне

Тестирование первоначального решения выявило два критических недостатка, которые делали его неприменимым на практике.

  1. Техническое ограничение: значительная часть диалогов вместе с ёмким промптом превышала допустимый объём контекста LLM (на момент лета 2024 года это было 8 тысяч токенов).

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

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

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

Что нам помогло сделать правильно  

Прежде всего — датасет

В целом тезис довольно очевидный, ведь любую ML‑задачу стоит начинать с датасета. Но у нашего датасета был ряд особенностей.

Сначала мы подготовили данные: собрали всю доступную нам историю, отобрали из неё выборку по 300–350 примеров на класс и передали на ручную проверку. Нашим специалистам нужно было убедиться, что найденные ошибки и комментарии к ним действительно корректные (такая перепроверка проверки ошибок).

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

Было желание максимально улучшить процесс. Так мы пришли к разделению на три вида оценок вместо двух (1 (ок) / 0 (не ок)).

Мы используем оценки вида 0, 1 и 2: 

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

  • 1 — сомнения, то есть по какой‑то причине проверяющий не считает, что ответ достаточно хорош для получения оценки 2. Например, ему нравится, что модель определила класс, но вот сам комментарий его не устраивает. Возникали случаи, когда был подставлен неверный класс, но в комментарии описывалась точная ошибка.

  • 0 — намёк, что нам пора что‑то изменить в текущем пайплайне.

Ещё важно отметить, что при использовании оценок 0, 1, 2 у нас была возможность в моменте обогащать golden‑датасет единицами, а примеры с двойками отправлять в отдельный пул для регрессионных тестов. Здесь мы на своём опыте поняли, что в идеале хочется иметь не менее 70% двоек, не более 20% единиц и не более 10% нулей для каждого класса перед запуском.

LLM-пайплайн

Мы отказались от одного жирного промта и стали проверять разные классы ошибок разными промптами, добавлять различные пред‑ и постобработки, в том числе с помощью LLM.Таким образом, у нас сложились многоступенчатые пайплайны поиска ошибок, или по‑научному LLM‑flow. Сейчас на проверку одного диалога у нас уходит порядка 7 обращений к LLM. Подробнее мы обсудим примеры реализации дальше. 

Версионирование промптов

Разработка многоступенчатых LLM‑пайплайнов сопряжена с двумя серьёзными вызовами: 

  • сложностью атрибуции: практически невозможно определить, какое из десятка изменений в промптах привело к падению или росту метрик;

  • разрывом между качеством на тестах и в реальной работе: успешное прохождение golden‑датасета не защищает от неожиданной деградации качества в проде.

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

Версионируя имеющийся у нас промпт, мы превращаем обычный текст в настоящий воспроизводимый тестируемый артефакт. 

Применение LLM as a Judge для ускорения экспериментов

Для ускорения проверки гипотез, особенно в задаче генерации комментариев, мы внедрили подход LLM as a Judge (LLM‑J). Он позволил нам автоматизировать первичную оценку качества и быстрее итерироваться.

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

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

В качестве LLM‑судьи использовали наиболее сильные модели согласно арене, а при работе с чувствительными данными — локальные YandexGPT/DeepSeek. Наш рецепт по настройке LLM‑J следующий:

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

  2. Чтобы избежать position‑bias и других перекосов, проверяем стабильность промпта (переставляя варианты, вводя «шум»), добиваясь ≥90% согласованности. Потом фиксируем финальный промпт и температуру модели. 

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

Примеры ошибок

Для наглядности — несколько примеров ошибок, которые LLM ловит и обрабатывает, оставляя оператору поддержки обратную связь.

Неправильное использование эмодзи

Пример промпта

Тебе дан текст и эмодзи в формате::эмодзи::. Оцени корректность использования эмодзи в контексте текста.

Правила:

  • В тексте должны использоваться только позитивные эмодзи (например,:smile:,:thumbsup:,:party:).

  • Грустные или нейтральные эмодзи (например,:disappointed:,:pensive:,:neutral_face:) не должны использоваться.

  • Если текст негативный или нейтральный (например, отказ, сожаление, извинение), эмодзи вообще быть не должно.

Если есть ошибки в использовании эмодзи, выведи 1. Если ошибок нет, выведи 0.

Текст:

...

Python‑парсер извлекает эмодзи, YandexGPT оценивает их уместность
в контексте всего диалога. Модель учитывает содержание текста и тон общения. Эмодзи ? в сообщении о невозвратном номере размечается как нарушение.

Повторный шаблон

Как выглядит вывод алгоритма:

{
 'main_lines'   : [...],  # фрагмент-кандидат на «повторный шаблон»
 'history_lines': [...],  # совпавшие строки из прошлого
 'avg_similarity': 0.x, # «насколько строки похожи» (1 — полное совпадение, 0 — ничего общего)
 'variables'    : N,     # сумма всех числовых переменных
 'info'         : M      # признак того, что ответ был персонализирован
}

SequenceMatcher вычисляет сходство между ответами. С установленным порогом алгоритм оставляет в работе только настоящие дубликаты. Ошибочных срабатываний не более 3%, а 95% проверок укладываются в секунду.

Не использовали или не адаптировали имя пользователя 

Regex выделяет ФИО, GPT‑NER подтверждает разбор, а затем LLM сверяет форму обращения. Как итог — 97% точности даже на неструктурированных диалогах.

Финальная архитектура проекта сейчас выглядит так:

Мы реализовали проверки для 23 наиболее часто встречающихся классов ошибок. Как мы это делаем?

  • Очищаем и прогоняем каждое сообщение оператора через регулярки и Python‑скрипты для выбора правильной ветки проверок.

  • Проверяем наличие ошибок с помощью LLM.

  • Спеллер (LLM, дообученная на поиск ошибок, подробнее коллеги описывали в статье) находит орфографические ошибки и предлагает варианты исправления.

  • Анализируем комментарии LLM, вешаем теги ошибок и пишем о них саммари, если это необходимо.

Выводы, советы и итоги

Сейчас наш пайплайн закрывает ~50% проверок в почтовом и чатовом каналах клиентской поддержки. За счёт расширения покрытия (по сравнению с изначальными 3%) у руководителей появилось больше информации, где и у каких операторов возникают трудности, что позволяет им дорабатывать обучения и инструкции по работе с продуктами. Такие доработки необходимы, чтобы повышать качество поддержки для пользователей.

И вот три основных вывода по автоматизации этого процесса: 

  1. Не стоит недооценивать важность данных. Репрезентативный golden‑датасет и регулярная перекалибровка экспертов — единственный способ доверять цифрам.

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

  3. Лучшая архитектура — гибрид. Детерминированная логика закрывает тривиальные случаи, тогда как LLM справляются с более узкими и нестандартными ситуациями.

Если вы занимались чем‑то похожим и вам есть что добавить или уточнить — буду рада комментариям.

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