Урок 4. Оценка классификации

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

  • Software Bug — баг в софте
  • Hardware Malfunction — поломка железа
  • User Error — ошибка пользователя
  • Feature Request — запрос на новую функцию
  • Service Outage — сервис лежит

Например, жалоба «Сайт полностью лёг, не открывается ни одна страница» должна получить категорию Service Outage.

Иногда категорий может быть две. «Кажется, я что-то не так установил, и теперь компьютер вообще не включается» — это одновременно User Error и Hardware Malfunction.

Классификация — идеальный случай для проверки кодом. У каждого входа есть эталонная метка (golden answer) — правильный ответ, который мы записали заранее вручную. Коду остаётся сравнить ответ модели с эталоном: никакого субъективного «хорошо ли написано» здесь нет.

Размеченный набор данных

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

Каждый элемент — словарь с ключами complaint (текст жалобы) и golden_answer (список правильных категорий):

eval_data = [
    {"complaint": "The app crashes every time I try to upload a photo",
     "golden_answer": ["Software Bug"]},
    {"complaint": "My printer isn't recognized by my computer",
     "golden_answer": ["Hardware Malfunction"]},
    {"complaint": "I can't figure out how to change my password",
     "golden_answer": ["User Error"]},
    {"complaint": "The website is completely down, I can't access any pages",
     "golden_answer": ["Service Outage"]},
    {"complaint": "It would be great if the app had a dark mode option",
     "golden_answer": ["Feature Request"]},
    {"complaint": "The software keeps freezing when I try to save large files",
     "golden_answer": ["Software Bug"]},
    {"complaint": "My wireless mouse isn't working, even with new batteries",
     "golden_answer": ["Hardware Malfunction"]},
    {"complaint": "I accidentally deleted some important files, can you help me recover them?",
     "golden_answer": ["User Error"]},
    {"complaint": "None of your servers are responding, is there an outage?",
     "golden_answer": ["Service Outage"]},
    {"complaint": "Could you add a feature to export data in CSV format?",
     "golden_answer": ["Feature Request"]},
    {"complaint": "The app is crashing and my phone is overheating",
     "golden_answer": ["Software Bug", "Hardware Malfunction"]},
    {"complaint": "I can't remember my password!",
     "golden_answer": ["User Error"]},
    {"complaint": "The new update broke something and the app no longer works for me",
     "golden_answer": ["Software Bug"]},
    {"complaint": "I think I installed something incorrectly, now my computer won't start at all",
     "golden_answer": ["User Error", "Hardware Malfunction"]},
    {"complaint": "Your service is down, and I urgently need a feature to batch process files",
     "golden_answer": ["Service Outage", "Feature Request"]},
    {"complaint": "The graphics card is making weird noises",
     "golden_answer": ["Hardware Malfunction"]},
    {"complaint": "My keyboard just totally stopped working out of nowhere",
     "golden_answer": ["Hardware Malfunction"]},
    {"complaint": "Whenever I open your app, my phone gets really slow",
     "golden_answer": ["Software Bug"]},
    {"complaint": "Can you make the interface more user-friendly? I always get lost in the menus",
     "golden_answer": ["Feature Request", "User Error"]},
    {"complaint": "The cloud storage isn't syncing and I can't access my files from other devices",
     "golden_answer": ["Software Bug", "Service Outage"]},
]

Обратите внимание: часть эталонов спорная. «Сделайте интерфейс удобнее, я вечно теряюсь в меню» размечено как Feature Request + User Error — но кто-то разметил бы иначе. Это нормальная боль разметки: эталон — не абсолютная истина, а зафиксированное решение, с которым вы согласились. Главное, чтобы оно было согласованным по всему набору.

Квиз 1

Что такое эталонная метка (golden answer) в наборе данных для эвала?

Первый промпт

Начнём с простого промпта и померим, как он справляется. Функция принимает текст жалобы и возвращает готовую строку промпта:

def basic_prompt(complaint):
    return f"""
    Classify the following customer complaint into one or more of these categories:
    Software Bug, Hardware Malfunction, User Error, Feature Request, or Service Outage.
    Only respond with the matching category or categories and nothing else.

    Complaint: {complaint}

    Classification:
    """

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

Грейдер: сравниваем множества

Логика проверки чуть сложнее, чем в прошлом уроке с ногами. Там мы сравнивали строки один в один. Здесь ответов может быть несколько, и порядок в ответе модели не должен нас волновать — поэтому сравниваем set с set.

from anthropic import Anthropic
from dotenv import load_dotenv

load_dotenv()
client = Anthropic()

def get_model_response(prompt, model_name):
    response = client.messages.create(
        model=model_name,
        max_tokens=200,
        messages=[{"role": "user", "content": prompt}]
    )
    return response.content[0].text

def calculate_accuracy(eval_data, model_responses):
    correct_predictions = 0
    total_predictions = len(eval_data)

    for item, response in zip(eval_data, model_responses):
        golden_set = set(category.lower() for category in item["golden_answer"])
        prediction_set = set(category.strip().lower() for category in response.split(","))

        if golden_set == prediction_set:
            correct_predictions += 1

    return correct_predictions / total_predictions

def evaluate_prompt(prompt_func, eval_data, model_name):
    print(f"Оцениваем на модели: {model_name}")
    model_responses = [
        get_model_response(prompt_func(item["complaint"]), model_name)
        for item in eval_data
    ]
    accuracy = calculate_accuracy(eval_data, model_responses)

    print(f"Точность: {accuracy:.2%}")

    for item, response in zip(eval_data, model_responses):
        print(f"\nЖалоба: {item['complaint']}")
        print(f"Эталон: {item['golden_answer']}")
        print(f"Ответ модели: {response}")
    return accuracy

Что здесь происходит по шагам:

  1. evaluate_prompt прогоняет каждый вход через функцию-промпт и отправляет результат в модель через get_model_response, собирая ответы.
  2. Дальше он зовёт calculate_accuracy, чтобы сравнить ответы модели с эталонами.
  3. calculate_accuracy режет ответ модели по запятым, приводит всё к нижнему регистру и складывает в set — а потом сверяет с множеством эталонных категорий.
  4. Функция возвращает точность (accuracy) — долю примеров, где ответ полностью совпал с эталоном.
  5. evaluate_prompt печатает итог и каждый ответ отдельно.
Ключевое отличие от прошлого урока: мы не сравниваем строки точным совпадением. Мы сравниваем множества категорий — так «Software Bug, Service Outage» и «Service Outage, Software Bug» считаются одинаковыми ответами.

Запускаем:

evaluate_prompt(basic_prompt, eval_data, model_name="claude-haiku-4-5")

Базовый промпт даёт около 85% точности: три примера из двадцати модель разложила не так, как мы разметили.

Квиз 2

Эталон примера — ["Software Bug", "Service Outage"], модель ответила «Service Outage, Software Bug». Как это засчитает наш грейдер?

Улучшенный промпт

85% — есть куда расти. Правим промпт и перезапускаем тот же эвал. В новой версии два изменения: развёрнутые описания категорий и девять примеров «вход → ответ».

def improved_prompt(complaint):
    return f"""
    You are an AI assistant specializing in customer support issue classification.
    Your task is to analyze customer complaints and categorize them into one or more
    of the following categories:

    1. Software Bug: Issues related to software not functioning as intended.
    2. Hardware Malfunction: Problems with physical devices or components.
    3. User Error: Difficulties arising from user misunderstanding or misuse.
    4. Feature Request: Suggestions for new functionalities or improvements.
    5. Service Outage: System-wide issues affecting service availability.

    Important Guidelines:
    - A complaint may fall into multiple categories. If so, list all that apply
      but try to prioritize picking a single category when possible.

    Examples:
    1. Complaint: "The app crashes when I try to save my progress."
    Classification: Software Bug

    2. Complaint: "My keyboard isn't working after I spilled coffee on it."
    Classification: Hardware Malfunction

    3. Complaint: "I can't find the login button on your website."
    Classification: User Error

    4. Complaint: "It would be great if your app had a dark mode."
    Classification: Feature Request

    5. Complaint: "None of your services are loading for me or my colleagues."
    Classification: Service Outage

    6. Complaint: "The app breaks every time I try to change my profile picture"
    Classification: Software Bug

    7. Complaint: "The app is acting buggy on my phone and it seems like your website
       is down, so I'm completely stuck!"
    Classification: Software Bug, Service Outage

    8. Complaint: "Your software makes my computer super laggy and awful, I hate it!"
    Classification: Software Bug

    9. Complaint: "Your dumb app always breaks when I try to do anything with images."
    Classification: Software Bug

    Now, please classify the following customer complaint:

    <complaint>{complaint}</complaint>

    Only respond with the appropriate categories and nothing else.
    Classification:
    """

Запускаем тот же эвал с новым промптом:

evaluate_prompt(improved_prompt, eval_data, model_name="claude-haiku-4-5")

На нашем наборе — 100% точности. Мы не меняли ни модель, ни грейдер, ни данные: изменился только промпт, и цифра выросла. Это и есть петля «промпт → эвал → правка промпта → эвал», ради которой всё затевалось.

Держите в голове: это очень простой эвал на очень маленьком наборе. Урок показывает процесс проверки кодом, а не образец продакшен-эвала. 100% на двадцати примерах — не гарантия, что промпт хорош на реальном потоке жалоб.

Почему мало смотреть на итоговый процент

Заметьте, что evaluate_prompt печатает не только точность, но и каждый пример: жалобу, эталон и ответ модели. Это не для красоты.

Итоговый процент говорит сколько ошибок, но молчит о том, какие. А это разные истории:

  • Ошибки размазаны ровно. Промпт в целом слабоват — помогут более точные формулировки и примеры.
  • Все ошибки в одной категории. Например, модель регулярно путает User Error с Software Bug. Значит, дело не в промпте вообще, а в описании конкретно этих двух категорий — правьте их границу.
  • Ошибки только на примерах с двумя категориями. Модель склонна назвать одну и остановиться. Лечится инструкцией и примерами с двумя ответами (в улучшенном промпте пример №7 — ровно про это).
  • Ошибка не в модели, а в эталоне. Бывает чаще, чем кажется: вы смотрите на «ошибку» и понимаете, что модель права, а разметка сомнительная. Тогда правится набор данных, а не промпт.

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

Квиз 3

Точность 80%, и все четыре ошибки — путаница между User Error и Software Bug. Что разумнее сделать первым делом?

Упражнения

Упражнение 4.1 — Сломать грейдер

Наш calculate_accuracy режет ответ модели по запятым. Придумайте два формата ответа, при которых модель по сути права, а грейдер засчитает ошибку. Что бы вы поправили: промпт или грейдер?

Решение упражненияСначала попробуйте сами — потом сверьтесь

Примеры «правильных, но незачтённых» ответов:

  • Classification: Software Bug — модель повторила заголовок. После split(",") получится строка "classification: software bug", с эталоном она не совпадёт.
  • Software Bug and Service Outage — разделитель не запятая. Всё склеится в одну строку.
  • Список маркерами или с кавычками: 'Software Bug' — кавычки останутся внутри строки.
  • Вежливое вступление «Sure! Here's the classification: …» — та же беда.

Что чинить: сначала промпт. Строка «Only respond with the appropriate categories and nothing else» и девять примеров нужного формата — это и есть починка. Грейдер тоже можно сделать терпимее (например, искать вхождение каждой категории в текст ответа), но осторожно: слишком добрый грейдер начнёт засчитывать мусор и завысит точность. Правило простое — формат ответа задаёт промпт, а грейдер этот формат проверяет, а не спасает.

Упражнение 4.2 — Разбор ошибок по классам

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

Решение упражненияСначала попробуйте сами — потом сверьтесь
from collections import defaultdict

def errors_by_class(eval_data, model_responses):
    total = defaultdict(int)
    wrong = defaultdict(int)

    for item, response in zip(eval_data, model_responses):
        golden_set = set(c.lower() for c in item["golden_answer"])
        prediction_set = set(c.strip().lower() for c in response.split(","))

        for category in item["golden_answer"]:
            total[category] += 1
            if golden_set != prediction_set:
                wrong[category] += 1

    for category in sorted(total):
        print(f"{category}: ошибок {wrong[category]} из {total[category]}")

    # отдельно — примеры с двумя эталонными категориями
    multi = [i for i in eval_data if len(i["golden_answer"]) > 1]
    print(f"\nПримеров с двумя категориями: {len(multi)}")

На базовом промпте ошибки почти наверняка соберутся там, где эталон содержит две категории: без явной инструкции и примеров модель называет одну и останавливается. Это и объясняет, почему улучшенный промпт с примером №7 («Software Bug, Service Outage») закрывает разрыв.

Полезная деталь: считать «сколько примеров каждого класса модель разложила неверно» — не то же самое, что вводить формальные метрики качества классификации. Мы просто группируем ошибки, чтобы понять, куда смотреть.

Что запомнить

  • Классификация — лучший кандидат на проверку кодом: у каждого входа есть эталонная метка, записанная человеком заранее.
  • Точность — доля примеров, где ответ модели совпал с эталоном.
  • Когда ответов может быть несколько, сравнивайте множества (set), а не строки: порядок категорий не должен считаться ошибкой.
  • Формат ответа задаёт промпт («только категории и ничего больше»), грейдер этот формат проверяет. Не чините промптовую проблему добрым грейдером.
  • Печатайте не только итоговый процент, но и каждый разбор: одинаковые 85% могут означать совсем разные болезни.
  • Иногда «ошибка модели» — это ошибка вашей разметки. Проверяйте эталоны тоже.
  • Двадцать примеров — учебный масштаб. Для боевого эвала целитесь примерно в сотню.

Метод работает, но писать всю логику эвала руками утомительно, а сравнивать прогоны между собой неудобно. В следующем уроке возьмём инструмент, который делает это по конфигу — с таблицами, графиками и запуском сразу на нескольких моделях.

Дочитали и сделали упражнения? Зафиксируйте прогресс — отметка сохранится в вашем браузере.

Перевод и адаптация урока «Code-Graded Classification Evals» курса Prompt Evaluations © Anthropic, лицензия CC BY-NC 4.0. Перевод: Дарья Воронкина (@aishipuchka). Материал изменён: переведён на русский, примеры и названия моделей актуализированы, добавлены квизы и упражнения. Используется некоммерчески.