Составная оценка (composite scoring) на Jev: рейтинги и скоринг из нескольких критериев

Обновлено

Попросить модель «оценить кандидата от 1 до 10» — ненадёжный способ получить рейтинг. Паттерн composite scoring разбивает оценку на независимые критерии, а веса оставляет в вашем коде. Разбираем, как это устроено, на примере отбора резюме.

Почему одна общая оценка не работает

Вопрос «Насколько хорош кандидат?» прячет за одним числом несколько суждений: опыт в языке, лидерство, архитектуру. Если в описании уровня смешаны разные качества — «пунктуальный, умный и опытный», — модель не может разместить вход, который силён в одном и слаб в другом. По наблюдению TypeSafe, уверенность в таких случаях падает, а score теряет смысл.

Composite scoring решает это в три шага:

  1. Разбить суждение на независимые измерения.
  2. Оценить каждое отдельным Score-вопросом.
  3. Сложить оценки с весами в коде.

Все вопросы уходят одним запросом и считаются параллельно — подробнее в статье о паттерне fan-out. Веса живут в коде: когда приоритеты меняются, вы правите коэффициент, а не переписываете промпт. И главное — видно, из чего сложилось итоговое число.

Как читать ответ Score

Чтобы складывать оценки, нужно понимать, что возвращает Score:

  • уровни нумеруются с нуля в порядке criteria: у пятиуровневой шкалы — от 0 до 4;
  • score — среднее номеров уровней, взвешенное по вероятностям, поэтому он может лечь между уровнями. Если 0,57 вероятности пришлось на уровень 1 и 0,43 на уровень 2, score равен 1,43;
  • одинаковый score получается из разных распределений: 1,0 — это и «всё на уровне 1», и «пополам на уровнях 0 и 2». Поэтому рядом со score смотрите probabilities и confidence.

Шкалы разной длины напрямую не сравнить: максимум трёхуровневой — 2, четырёхуровневой — 3. Перед сложением нормализуйте: score / (len(criteria) - 1) переводит любую шкалу в диапазон от 0 до 1.

Пример: отбор резюме на две роли

Возьмём пример TypeSafe: инженерные резюме оцениваются по четырём критериям, а из одних и тех же ответов строятся рейтинги на две роли — сильного инженера-исполнителя (senior IC) и руководителя разработки.

from typesafe_sdk import Score, TypeSafeClient

QUESTIONS = {
    "python_depth": Score(
        instructions="Насколько глубокий опыт Python у кандидата по резюме?",
        criteria=[
            "Python не упоминается",
            "Упоминается без подробностей",
            "Использовал в проектах, есть конкретика",
            "Основной язык, несколько проектов",
            "Глубокая экспертиза: архитектура, производительность, библиотеки",
        ],
    ),
    "team_leadership": Score(
        instructions="Какой у кандидата опыт управления инженерными командами?",
        criteria=[
            "Опыта управления нет",
            "Неформальное наставничество или роль техлида",
            "Руководил небольшой командой или проектом",
            "Руководил командой с прямыми подчинёнными",
            "Руководил несколькими командами или инженерной службой",
        ],
    ),
    "system_design": Score(
        instructions="Какой у кандидата опыт проектирования крупных или распределённых систем?",
        criteria=[
            "Архитектурной работы нет",
            "Участвовал в обсуждениях архитектуры",
            "Проектировал компоненты большой системы",
            "Отвечал за архитектуру значимой системы",
            "Проектировал масштабные системы в нескольких областях",
        ],
    ),
    "generalist": Score(
        instructions="Насколько резюме подтверждает, что кандидат осваивает незнакомые инструменты, роли или области?",
        criteria=[
            "Одна область или роль",
            "Некоторое разнообразие внутри узкой области",
            "Работал в нескольких областях или стеках",
            "Регулярно менял области и роли",
            "Не раз быстро осваивал новое и давал результат",
        ],
    ),
}

WEIGHTS = {
    "senior_ic": {"python_depth": 0.40, "team_leadership": 0.10, "system_design": 0.40, "generalist": 0.10},
    "eng_manager": {"python_depth": 0.15, "team_leadership": 0.40, "system_design": 0.20, "generalist": 0.25},
}

def rate(resume: str) -> dict:
    with TypeSafeClient() as client:
        r = client.system_one(state={"resume": resume}, questions=QUESTIONS)
    dims = {q: r.scores[q].score / (len(QUESTIONS[q].criteria) - 1) for q in QUESTIONS}
    return {role: sum(w * dims[q] for q, w in weights.items()) for role, weights in WEIGHTS.items()}

Веса каждой роли в сумме дают 1, поэтому итог тоже лежит от 0 до 1. Вот как это выглядит на двух кандидатах (числа условные, для иллюстрации):

Кандидат Python Лидерство Архитектура Универсальность Senior IC Руководитель
А 0,95 0,20 0,80 0,40 0,76 0,48
Б 0,50 0,90 0,60 0,75 0,61 0,74

Один запрос на кандидата — два разных рейтинга: А лидирует на позицию инженера, Б — на позицию руководителя. Если лучшие кандидаты не совпадают с ожиданиями команды, вы меняете веса и сразу видите, какое измерение сдвинуло итог.

Как писать шкалы для скоринга

Качество составной оценки упирается в качество каждой шкалы. Рекомендации TypeSafe:

  • Описывайте ситуации, а не степени. «Функция сломана, но есть обходной путь» даёт модели, с чем сравнить вход. «Умеренно серьёзно» — не даёт.
  • Не полагайтесь на номера и соседей. Модель оценивает каждый уровень отдельно и не видит ни его номера, ни соседних уровней, так что «хуже предыдущего» для неё ничего не значит. В документации есть показательный пример: со шкалой из голых цифр «0», «1», «2» косметическая ошибка получила 0,55 при уверенности 0,33, а с описательными уровнями — 0,0 при уверенности 1,0.
  • Одно измерение на вопрос. Если уровень описывает сразу несколько независимых качеств, разбейте его на отдельные вопросы.
  • Столько уровней, сколько можете различить. Максимум — 10, но и трёх часто достаточно. Уровни, которые нельзя описать по-разному, только размывают распределение.
  • Редкому крайнему случаю — свой уровень. Если «оскорбления и угрозы» нужно обрабатывать иначе, чем «очень зол», дайте им отдельный уровень, иначе оба окажутся у верхней границы.

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

Как собирать итоговую оценку

Взвешенная сумма — базовый вариант. В руководстве TypeSafe так же собирают и Noul-ответы. Пример — качество ответа ассистента: 0,4 × «отвечает на запрос» + 0,4 × «цитаты подтверждены» + 0,2 × (1 − «противоречит контексту»). Обратите внимание на последнее слагаемое: «плохой» сигнал переворачивают, чтобы все компоненты работали в одну сторону.

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

Уверенность — отдельно от балла. Составной балл не показывает, насколько модель уверена в каждой части. Проверяйте confidence главных измерений: в примере TypeSafe тикет получает высокий приоритет, только если раздражение клиента оценено не ниже 1,5 и уверенность в этой оценке не ниже 0,7. Неуверенные случаи по важному критерию лучше отдать человеку — как подобрать пороги, рассказано в статье об уверенности Jev.

Не вычисляйте точные величины. Score годится для сравнения с порогом и ранжирования. Восстанавливать по дробному score точное число — стаж в годах или сумму — TypeSafe не советует: численная калибровка уровней у jev-1.13 слабая.

Когда весов вручную мало. Вероятности Jev можно подать признаками в классическую ML-модель и обучить комбинацию на исторических исходах. Если разметки нет, TypeSafe предлагает получить её ансамблем дорогих рассуждающих моделей. Подробнее — в статье о Jev и машинном обучении.

Где применять составную оценку

Задача Измерения Что получается
Отбор резюме опыт в стеке, лидерство, архитектура, универсальность рейтинг под роль
Приоритет тикета серьёзность, раздражение клиента, полнота описания порядок очереди
Скоринг лидов соответствие отрасли, зрелость компании, намерение купить очередь для продаж
Модерация тяжесть нарушения и уверенность пропустить, предупредить, проверить, заблокировать

Для приоритета тикета в документации TypeSafe есть готовые веса: 0,6 на серьёзность, 0,3 на раздражение и 0,1 на полноту описания. Когда оценка касается людей — найма, кредита, страховки, — держите человека в контуре: в карте сценариев TypeSafe для рекрутинга прямо указано, что неуверенные случаи нужно передавать на проверку человеку.

Частые вопросы

Как нормализовать Score в Jev?

Разделите score на номер верхнего уровня, то есть на число уровней минус один. Уровни нумеруются с нуля, поэтому у пятиуровневой шкалы score лежит от 0 до 4 и после деления попадает в диапазон от 0 до 1.

Сколько запросов нужно для составной оценки?

Один. Все Score-вопросы отправляются вместе и считаются параллельно, поэтому новый критерий почти не замедляет ответ и стоит только токенов своей формулировки.

Можно ли по дробному score восстановить точное значение, например стаж в годах?

Нет. TypeSafe предупреждает, что уровни Score у jev-1.13 слабо откалиброваны численно. Дробный score годится для сравнения с порогом и ранжирования, но не для интерполяции точной величины.

Источники