Перейти к содержимому
PD
AI-автоматизация 8 мин чтения

Вашему LLM-классификатору нужна таксономия, а не «промпт получше»

Классификация на LLM ломается не из-за слабого промпта, а из-за неопределённых меток. Мой рабочий процесс: сначала таксономия, потом извлечение фактов вместо вердиктов, а решение принимает код.

PD

Pavel Duglas

AI Automation & MVP Architect

Раз в пару недель мне показывают одно и то же: промпт, который заканчивается строкой «Верни одно из: urgent, normal, spam», и жалобу «модель нестабильна». Я прошу двух человек из их команды вручную разметить одни и те же двадцать сообщений - и они расходятся в шести. Модель не нестабильна. Метки не определены. Невозможно выбить из промпта соблюдение правила, которого ещё не существует.

Классификация - самый частый реальный сценарий применения LLM в бизнес-автоматизации: триаж тикетов, скоринг лидов, модерация, категоризация спарсенных объявлений, роутинг входящих сообщений в Telegram-боте. И это же место, где люди пропускают всю скучную инженерную часть, а потом винят модель. Ниже - процесс, которым я реально пользуюсь.

Шаг 1: метка должна соответствовать разному действию

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

Плохая таксономия для тикетов поддержки:

  • billing, technical, question, complaint, other

Они перекрываются. Жалоба на неудавшийся платёж - это одновременно billing, complaint и technical. Модель вынуждена угадывать ваш приоритет, и во вторник она угадает иначе, чем в понедельник.

Нормальная таксономия, определённая через действие:

  • refund_request -> в очередь финансов, SLA 4 часа
  • access_blocked -> в on-call, SLA 30 минут
  • how_to -> автоответ со ссылкой на документацию, без человека
  • sales_inquiry -> в CRM
  • unclear -> в очередь ручного разбора

Правила, от которых я не отступаю:

  1. Взаимоисключаемость. Если сообщение честно тянет на две метки, добавьте явный tie-breaker: «если пользователь не может войти И просит возврат - ставим access_blocked».
  2. Полнота плюс аварийный выход. Всегда держите unclear или other. Без него модель начинает изобретать уверенность, которой у неё нет.
  3. Максимум 7-9 меток на уровень. Больше - делайте иерархию: сначала грубая метка, потом второй проход внутри ветки. Плоские таксономии на 40 категорий - это то место, где точность умирает.
  4. Пишите рубрику, а не слово. На каждую метку: строчка определения, два положительных примера, один пограничный отрицательный пример и правила разрешения конфликтов. Эта рубрика - ядро промпта и одновременно инструкция для людей-разметчиков.

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

Шаг 2: просите у модели факты, решение принимает код

Это единственное изменение, которое улучшило больше моих пайплайнов, чем любая смена модели. Вместо вердикта просите наблюдаемые факты, а бизнес-правила применяйте в коде.

Сравните. Стиль «вердикт»:

Определи приоритет тикета: urgent / normal / low.

Стиль «факты»:

{
  "mentions_payment": true,
  "mentions_cannot_access": false,
  "has_order_id": true,
  "asks_for_money_back": true,
  "customer_tone": "frustrated",
  "language": "ru",
  "quoted_error_code": null
}

А дальше:

def route(facts, account):
    if facts["mentions_cannot_access"] and account.plan == "paid":
        return "access_blocked"
    if facts["asks_for_money_back"]:
        return "refund_request"
    if facts["customer_tone"] == "frustrated" and account.mrr > 500:
        return "escalate_human"
    return "how_to"

Почему это выигрывает в продакшене:

  • Изменение политики не трогает промпт. Финансы решили, что возвраты свыше 200 долларов требуют согласования менеджера? Это if на Python, а не переписывание промпта плюс переоценка всей таксономии.
  • Отлаживается. Когда тикет ушёл не туда, вы видите, модель ли неверно прочитала текст или правило было кривое. Из односложного вердикта вы получаете одно слово и объяснение, которому нельзя доверять.
  • Дешевле и стабильнее. Извлечение фактов - узкая задача. Маленькая модель делает это надёжно. Токены вы тратите там, где реально нужны суждения.
  • Можно подмешать данные, которых модель не видит. MRR аккаунта, дата регистрации, число предыдущих обращений. Модели они не нужны, вашим правилам - нужны.

Именно это имеют в виду, когда говорят, что классификация - это feature engineering. LLM здесь экстрактор признаков из грязного текста. Решение остаётся в коде, где его можно протестировать.

Всегда форсируйте структурированный вывод: JSON-схема с enum и boolean, без свободного текстового поля под метку. Если провайдер умеет strict schema mode - включайте. Если нет - валидируйте, делайте один retry, а потом падайте в unclear, но не принимайте мусор.

Шаг 3: golden set до любой оптимизации

150-300 реальных примеров, размеченных руками того человека, который владеет действием ниже по потоку. Не синтетика. Не двенадцать кейсов, которые вы помните по памяти.

Как я его собираю:

  • Стратифицированно по меткам, включая редкие. Случайная выборка из продакшена даст вам 90% how_to и ничего не расскажет про метки, которые действительно важны.
  • 20% специально тяжёлых кейсов: неоднозначные, многосоставные, те, из-за которых команда спорила.
  • 5% мусора: пустая строка, только эмодзи, не тот язык, вставленный стектрейс, HTML из сломавшегося парсера.
  • Хранить файлом в репозитории: id, сырой вход, ожидаемая метка, ожидаемые факты. Под версионированием.

Дальше - гоняем в CI. Любое изменение промпта, модели, температуры или схемы запускает golden set и печатает precision и recall по каждой метке. Не accuracy. Accuracy на несбалансированной выборке - метрика для самоуспокоения: если 85% элементов это how_to, классификатор, который всегда отвечает how_to, наберёт 85% и при этом не будет работать.

На что я смотрю:

  • Recall по меткам, пропуск которых дорог. Пропущенный access_blocked - это ушедший клиент. Пропущенный how_to не стоит ничего.
  • Precision по меткам, запускающим автоматические действия. Если refund_request автоматически создаёт заявку на возврат, false positive стоит денег.
  • Матрица ошибок. Обычно одна пара меток даёт большую часть ошибок. И в 80% случаев эта пара - проблема таксономии, а не модели.

Задайте в CI явные пороги: recall по access_blocked не ниже 0.95, precision по refund_request не ниже 0.90. Ниже - билд красный. Теперь ваш промпт это протестированный артефакт, а не удачно подобранная строка.

Шаг 4: дешёвая модель, потом эскалация

Трёхуровневый роутинг, который я переиспользую почти везде:

  1. Детерминированный пре-фильтр. Регулярки и ключевые слова снимают очевидные 20-40%. Точное совпадение с «отписаться» не требует ни одного токена. Пустой вход - тоже.
  2. Маленькая модель на извлечение фактов для всего остального. Она тянет основной объём.
  3. Большая модель только для того, что маленькая пометила как unclear, где факты противоречат друг другу, или где элемент переходит порог ценности (enterprise-аккаунт, заказ выше X).

Про уверенность: не верьте самоотчётному полю "confidence": 0.93. Модель с радостью выдаст эту цифру для полной галлюцинации. Работают две вещи:

  • Self-consistency. Прогнать маленькую модель три раза при температуре 0.7. Три совпавших ответа - сильный сигнал, расхождение - триггер эскалации. Стоит 3x на дешёвом уровне, всё равно дешевле, чем всегда звать большую модель.
  • Logprobs токена метки, если провайдер их отдаёт. Дешевле self-consistency и вполне калиброванно после того, как вы разложите их по бакетам на своём golden set.

И всегда оставляйте человеческую очередь. Классификатор, который отправляет 5% человеку и по остальным 95% прав, - это рабочая система. Тот, который делает вид, что закрывает 100%, - это будущий инцидент.

Шаг 5: логируйте как под аудит, следите за распределением

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

Этот процент переопределений (overturn rate) - лучшая продакшен-метрика, которая у вас есть. Это бесплатная разметка. Каждая правка человека - кандидат в golden set. Раз в неделю я выгружаю все переопределения плюс 1% случайной выборки и пятнадцать минут просматриваю в таблице.

Отдельно мониторьте распределение меток по дням, а не только ошибки. Если how_to три месяца держался на 60% объёма и за ночь упал до 30% - либо сломался продукт, либо изменился источник трафика, либо ваш парсер стал отдавать другой HTML. В парсинг-пайплайнах это почти всегда третье: сайт переехал на новую вёрстку, извлечённый текст превратился в навигационное меню, и классификатор старательно классифицирует меню. Алерт на сдвиг распределения ловит это за несколько дней до того, как кто-то заметит кривой роутинг.

И последнее: пиньте версию модели. «Latest» - это не версия. Тихое обновление на стороне провайдера может сдвинуть пограничную пару меток на несколько пунктов, и вы потратите день на поиск бага в собственном коде.

Коротко

Пишите таксономию через действия. Заставьте модель извлекать факты, а не выносить вердикты. Решения держите в коде. Соберите golden set на 200 строк и гоняйте метрики по меткам в CI. Эскалируйте вместо угадывания. Логируйте переопределения и смотрите на распределение.

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

Вопросы и ответы

У меня уже есть промпт с 30 категориями и он работает так себе. Переделывать всё с нуля?

Не с нуля, но таксономию - да. Возьмите лог за последний месяц, посмотрите фактическое распределение и выкиньте метки с долей меньше 1% или те, что ведут к тому же действию, что и соседние. Обычно из 30 категорий остаётся 6-8 осмысленных плюс второй уровень внутри одной-двух ветвей. Это займёт день и даст больше, чем неделя подбора формулировок в промпте.

Стоит ли файнтюнить модель вместо всего этого?

Только после того, как у вас есть стабильная таксономия и golden set, и вы упёрлись в потолок по качеству или по стоимости. Файнтюн на неопределённых метках просто зафиксирует вашу неразбериху в весах, и переобучать придётся при каждом изменении политики. Практический порядок: таксономия -> факты плюс правила -> каскад моделей -> и лишь потом файнтюн, если экономика на объёме сходится.

Как быстро собрать golden set, если у меня ещё нет продакшен-трафика?

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

Похожие статьи