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

Главная мысль автора заключается в том, что на проде вам вряд ли удастся добиться качества ответов всей системы выше 90%. Лучше изначально ориентироваться на этот показатель, поскольку некоторые проекты из-за высокой стоимости ошибки никогда не сойдутся.

Будем разбираться на примере определения кода услуги для 402-й формы. На первый взгляд задача простая: по названию услуги нужно выбрать соответствующий класс.

Например, в справочнике есть такие категории:

Код

Услуга

842

Разработка и установка программного обеспечения

7123

Грузовые перевозки автомобильным транспортом

Условие задачи: плоская класификация, 50 классов. Класс это код и формально обозначение услуги - "842 - Разработка и установка программного обеспечения". Каждое название соответствует только одному классу. (Это реальная задача, которой я занимался. Я поправил вводные оставив общий смысл)

Улучшение качества отдельной ноды

Генерализация

У LLM есть одно потрясающее свойство, слово из заголовка. Благодаря ему модель умеет решать задачи по аналогии: мы показываем ей несколько решений, и она улавливает общий принцип "как это решать".

И хотя это потрясающе, в каждом процессе есть исключения/эдж кейсы. Такие кейсы на которых общее правило не работает. Логику работы с ними приходится прописывать отдельно, и промпт начинает раздуваться.

Допустим, мы написали промпт для 402 формы. Его базовую версию - описали, какие бывают категории и как их определять. На внутреннем бенчмарке получили высокое качество, 98% правильных ответов. Вывели решение в прод, а на бою оказалось 73%.

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

“Просто все зафигачить в контекст”

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

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

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

Попробую пояснить, например, мы извлекаем из документа 50 атрибутов. Только для суммы у нас уже абзац правил: «Если это договор аренды, умножь стоимость месяца на срок договора в месяцах. Не включай неустойку в итоговую сумму…». И так для каждого атрибута. В такой ситуации стоит попробовать подбробить промпт на отдельные вызовы. 

Еще один риск способа "фигачить все в один промпт", подогнать решение под знакомые примеры. Мы разбираем ошибки на одном и том же бенчмарке, добавляем под каждую исключение и постепенно получаем промпт, который хорошо проходит именно этот набор данных. Добавляем 30 примеров и модель как бы выучивает эти примеры вместо общего правила (это похоже на переобучение/overfit в ML).

В нашем примере после экспериментов мы решили остановиться на 8 примерах в промпте. 

Получили 81%. Уже лучше, но для нашего бизнес-заказчика все еще низко. Думаем дальше.

Разделение на разные вызовы

Следующая идея - разделить наш промпт на два вызова.

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

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

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

Вот это уже похоже на полезное разделение.

Извлекаем услуги списком, каждую отдельно подаем на ноду определения кода для 402-й формы вместе с нужным контекстом документа. В конце собираем результаты, сохраняя для каждой услуги ее код.

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

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

Проделав этот трюк мы получили качество 89%.

Динамическая подстановка примеров

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

Делам таблицу соответствия: 1) вектор названия услуги и 2) класс, который нужно определить. Переводим актуально название услуги в вектор и добавляем из нашей таблицы и добавляем релевантные примеры прям в промпт.

Это тоже потенциально улучшает качество, но не дает 100 процентов правильных ответов. Потому что во-первых наша таблица не полная, во-вторых иногда вектора ложно сходятся и мы подставляем неправильные примеры.

И наконец, у нас качество 94%! Ура? Ну как сказать...

Улучшение качества цепочки

Если мы выводим наше решение в прод и оно начинает работать на тысячах запросов, очень сложно превысить порог в 90% по качеству ответов, ДАЖЕ для одного узла, одного запроса к LLM. Если же у нас цепочка последовательных запросов, то итоговое качество системы будет перемножением качества работы отдельных нод. Чтобы более явно проиллюстрировать свою мысль приведу другой пример, не про код услуг, а про контакт-центр.

Пример для автоматических ответов в контакт-центре:

Клиент пишет: «Войти в кабинет»

Система работает по цепочке:

  1. Переписывание вопроса пользователя — 95%. Преобразуем вопрос пользователя в более точный поисковый запрос для базы знаний: «Не могу войти в личный кабинет»

  2. RAG / поиск по базе знаний — 80%. Ищем релевантную инструкцию в базе знаний.

  3. Финальный ответ пользователю — 99%. Формируем понятный ответ на основе найденной информации.

Общее качество всей цепочки считается как произведение качества каждого шага (конечно, речь идет о фиксированным датасете, мы не можем проверять одно звено на одном наборе кейсов и другое на другом и перемножить вероятности):

0,95 × 0,80 × 0,99 = 0,7524 

Мы видим, что главная проблема RAG: часть нерелевантных фрагментов получает высокое векторное сходство и попадает в ответ.

И тут у нас возникает генальная идея (которая рано или поздно возникает у любой команды-интегратора LLM). “Мы добавим оркестрацию и вычистим эти 20% неправильных кейсов.” Команда разработки просмотрела ответы, собрала список проблемных вопросов и добавила проверку: после обработки запроса сравнивать его вектор со стоп-листом. Если вопрос похож на кейс из стоп-листа, то цепочка не запускается.

Значит ли это, что теперь будет 100% правильных кейсов? Нет, все еще нет. Мы не покрыли все случаи на которых RAG плохо работает, а оркестрация отсекает часть кейсов, но сама тоже ошибается. Мы не получаем идеальную систему, а добавляем ещё один шаг со своим качеством.

С оркестрацией

Оркестрация отсекается часть кейсов, с которыми RAG работает плохо. Поэтому качество RAG на оставшихся обращениях становится выше.

Например, раньше RAG работал на 80%, а после фильтрации оркестрацией RAG работает на выбранных кейсах, например, на 89%. Почему не на 100%? Потому что наш стоп-лист это только срез данных. Также наша оркестрация работает тоже со своим качеством, допустим 95% .

Тогда качество всей системы будет:

95% × 95% × 89% × 99% = 79,52%


Я собираю знания в методологию, еще будет много полезного, чтобы не потеряться - подпишитесь на тг https://t.me/s/Gpt_budni