Составная оценка (composite scoring) на Jev: рейтинги и скоринг из нескольких критериев
Попросить модель «оценить кандидата от 1 до 10» — ненадёжный способ получить рейтинг. Паттерн composite scoring разбивает оценку на независимые критерии, а веса оставляет в вашем коде. Разбираем, как это устроено, на примере отбора резюме.
Почему одна общая оценка не работает
Вопрос «Насколько хорош кандидат?» прячет за одним числом несколько суждений: опыт в языке, лидерство, архитектуру. Если в описании уровня смешаны разные качества — «пунктуальный, умный и опытный», — модель не может разместить вход, который силён в одном и слаб в другом. По наблюдению TypeSafe, уверенность в таких случаях падает, а score теряет смысл.
Composite scoring решает это в три шага:
- Разбить суждение на независимые измерения.
- Оценить каждое отдельным Score-вопросом.
- Сложить оценки с весами в коде.
Все вопросы уходят одним запросом и считаются параллельно — подробнее в статье о паттерне 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 годится для сравнения с порогом и ранжирования, но не для интерполяции точной величины.