A Score is a System One question type for rating content against ordered, descriptive levels. The answer includes a score, a probability for each level, and confidence.
Score — это тип вопроса System One для оценки контента по упорядоченным описательным уровням (шкале). Ответ включает числовую оценку (score), вероятность для каждого уровня и уверенность (confidence).
export function ScoreExplorer() { const examples = [{ "id": "severity", "label": "Bug severity", "question": "How severe is the reported issue?", "state": "The export button crashes the settings page in Safari. It works in Chrome, but a few of our customers only use Safari.", "levels": ["Cosmetic; no impact to functionality", "Broken or degraded feature, but workaround exists", "Blocking issue; no workaround exists"], "shortLevels": ["Cosmetic", "Workaround", "Blocking"], "answer": { "type": "score", "score": 1.43, "confidence": 0.35, "legend": { "0": "Cosmetic; no impact to functionality", "1": "Broken or degraded feature, but workaround exists", "2": "Blocking issue; no workaround exists" }, "probabilities": { "0": 0.0, "1": 0.57, "2": 0.43 } } }, { "id": "formality", "label": "Outfit formality", "question": "How formal is this outfit based on the description?", "state": "A navy blazer over a plain white T-shirt, dark jeans, and clean leather loafers. No tie.", "levels": ["gym clothes", "casual", "business casual", "formal", "black tie"], "shortLevels": ["Gym", "Casual", "Business casual", "Formal", "Black tie"], "answer": { "type": "score", "score": 1.86, "confidence": 0.89, "legend": { "0": "gym clothes", "1": "casual", "2": "business casual", "3": "formal", "4": "black tie" }, "probabilities": { "0": 0.0, "1": 0.14, "2": 0.86, "3": 0.0, "4": 0.0 } } }, { "id": "relevance", "label": "Candidate fit", "question": "How relevant is this candidate's experience to the job posting?", "state": "Job posting: Senior backend engineer building Python APIs and PostgreSQL services. Candidate: Three years building Django REST APIs with PostgreSQL, preceded by two years in frontend JavaScript. Has owned small services but has not led a backend team.", "levels": ["completely unrelated", "adjacent field", "some direct experience", "deep, direct experience"], "shortLevels": ["Unrelated", "Adjacent", "Some direct", "Deep direct"], "answer": { "type": "score", "score": 2.52, "confidence": 0.52, "legend": { "0": "completely unrelated", "1": "adjacent field", "2": "some direct experience", "3": "deep, direct experience" }, "probabilities": { "0": 0.0, "1": 0.0, "2": 0.48, "3": 0.52 } } }, { "id": "frustration", "label": "Customer frustration", "question": "How frustrated is the customer?", "state": "Export to PDF fails with a spinner that never finishes. Some of our team say CSV export still works for them, others say it fails too. This is the third time I'm writing in and honestly I'm done. Steps: open any report, click Export, choose PDF. Chrome 128 on macOS.", "levels": ["Calm, just stating facts", "Frustrated but civil", "Very angry, strong language or threatening to leave"], "shortLevels": ["Calm", "Frustrated", "Very angry"], "answer": { "type": "score", "score": 1.26, "confidence": 0.61, "legend": { "0": "Calm, just stating facts", "1": "Frustrated but civil", "2": "Very angry, strong language or threatening to leave" }, "probabilities": { "0": 0.0, "1": 0.74, "2": 0.26 } } }, { "id": "detail", "label": "Report detail", "question": "How much does the report give an engineer to work with?", "state": "Export to PDF fails with a spinner that never finishes. Some of our team say CSV export still works for them, others say it fails too. This is the third time I'm writing in and honestly I'm done. Steps: open any report, click Export, choose PDF. Chrome 128 on macOS.", "levels": ["No detail; just says something is broken", "Names the feature but no steps or environment", "Steps to reproduce or environment, but not both", "Steps to reproduce and environment"], "shortLevels": ["No detail", "Feature only", "Some detail", "Steps + environment"], "answer": { "type": "score", "score": 3.0, "confidence": 1.0, "legend": { "0": "No detail; just says something is broken", "1": "Names the feature but no steps or environment", "2": "Steps to reproduce or environment, but not both", "3": "Steps to reproduce and environment" }, "probabilities": { "0": 0.0, "1": 0.0, "2": 0.0, "3": 1.0 } } }]; const [selectedIndex, setSelectedIndex] = useState(0); const example = examples[selectedIndex]; const topLevel = example.levels.length - 1; const score = example.answer.score; const confidence = example.answer.confidence; const probabilities = example.levels.map((, level) => example.answer.probabilities[String(level)]); const percents = probabilities.map(probability => Number((probability * 100).toFixed(2))); const accent = "#E551BA"; const eyebrow = { fontSize: "0.6875rem", fontWeight: 700, letterSpacing: "0.08em", textTransform: "uppercase" }; const columnWidth = 56; const buttonClass = "border px-3 py-2 text-sm text-left hover:bg-zinc-100 dark:hover:bg-zinc-800 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-pink-500"; const unselectedStyle = { borderColor: "#71717a" }; const selectedStyle = { borderColor: accent, boxShadow: inset 0 0 0 1px ${accent}, background: "color-mix(in srgb, #E551BA 10%, transparent)" }; const endNameClass = "text-xs text-zinc-600 dark:text-zinc-400"; const midNameClass = "hidden sm:block text-xs text-zinc-600 dark:text-zinc-400"; function position(value) { return ${value / topLevel * 100}%; } function tickNameStyle(level) { if (level === 0) return { left: 0, textAlign: "left", maxWidth: "calc(50% - 8px)" }; if (level === topLevel) return { right: 0, textAlign: "right", maxWidth: "calc(50% - 8px)" }; return { left: position(level), transform: "translateX(-50%)", textAlign: "center", maxWidth: calc(${100 / topLevel}% - 8px) }; } const chartSummary = example.levels.map((, level) => level ${level}, ${example.shortLevels[level]}: ${percents[level]}%).join("; "); return
<details className="mt-5 text-sm text-zinc-600 dark:text-zinc-400">
<summary className="cursor-pointer">Как рассчитываются score и confidence</summary>
<div className="mt-3 font-semibold text-zinc-800 dark:text-zinc-200">Score:</div>
<p className="mt-1">Умножьте номер каждого уровня на его вероятность, затем сложите полученные результаты:</p>
<div className="mt-2 font-mono text-sm" style={{
overflowWrap: "anywhere"
TypeSafe вычисляет этот показатель на основе распределения вероятностей по уровням. Если вся вероятность приходится на один уровень, значение равно 1.0; чем равномернее она распределена, тем ниже уверенность.
; }
export function TypesafeExample({example, display, title}) { const keyStrUriSafe = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$"; function compressToEncodedURIComponent(input) { if (input == null) return ""; return _compress(input, 6, function (a) { return keyStrUriSafe.charAt(a); }); } function _compress(uncompressed, bitsPerChar, getCharFromInt) { var i, value, context_dictionary = {}, context_dictionaryToCreate = {}, context_c = "", context_wc = "", context_w = "", context_enlargeIn = 2, context_dictSize = 3, context_numBits = 2, context_data = [], context_data_val = 0, context_data_position = 0, ii; for (ii = 0; ii < uncompressed.length; ii += 1) { context_c = uncompressed.charAt(ii); if (!Object.prototype.hasOwnProperty.call(context_dictionary, context_c)) { context_dictionary[context_c] = context_dictSize++; context_dictionaryToCreate[context_c] = true; } context_wc = context_w + context_c; if (Object.prototype.hasOwnProperty.call(context_dictionary, context_wc)) { context_w = context_wc; } else { if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate, context_w)) { if (context_w.charCodeAt(0) < 256) { for (i = 0; i < context_numBits; i++) { context_data_val = context_data_val << 1; if (context_data_position == bitsPerChar - 1) { context_data_position = 0; context_data.push(getCharFromInt(context_data_val)); context_data_val = 0; } else { context_data_position++; } } value = context_w.charCodeAt(0); for (i = 0; i < 8; i++) { context_data_val = context_data_val << 1 | value & 1; if (context_data_position == bitsPerChar - 1) { context_data_position = 0; context_data.push(getCharFromInt(context_data_val)); context_data_val = 0; } else { context_data_position++; } value = value >> 1; } } else { value = 1; for (i = 0; i < context_numBits; i++) { context_data_val = context_data_val << 1 | value; if (context_data_position == bitsPerChar - 1) { context_data_position = 0; context_data.push(getCharFromInt(context_data_val)); context_data_val = 0; } else { context_data_position++; } value = 0; } value = context_w.charCodeAt(0); for (i = 0; i < 16; i++) { context_data_val = context_data_val << 1 | value & 1; if (context_data_position == bitsPerChar - 1) { context_data_position = 0; context_data.push(getCharFromInt(context_data_val)); context_data_val = 0; } else { context_data_position++; } value = value >> 1; } } context_enlargeIn--; if (context_enlargeIn == 0) { context_enlargeIn = Math.pow(2, context_numBits); context_numBits++; } delete context_dictionaryToCreate[context_w]; } else { value = context_dictionary[context_w]; for (i = 0; i < context_numBits; i++) { context_data_val = context_data_val << 1 | value & 1; if (context_data_position == bitsPerChar - 1) { context_data_position = 0; context_data.push(getCharFromInt(context_data_val)); context_data_val = 0; } else { context_data_position++; } value = value >> 1; } } context_enlargeIn--; if (context_enlargeIn == 0) { context_enlargeIn = Math.pow(2, context_numBits); context_numBits++; } context_dictionary[context_wc] = context_dictSize++; context_w = String(context_c); } } if (context_w !== "") { if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate, context_w)) { if (context_w.charCodeAt(0) < 256) { for (i = 0; i < context_numBits; i++) { context_data_val = context_data_val << 1; if (context_data_position == bitsPerChar - 1) { context_data_position = 0; context_data.push(getCharFromInt(context_data_val)); context_data_val = 0; } else { context_data_position++; } } value = context_w.charCodeAt(0); for (i = 0; i < 8; i++) { context_data_val = context_data_val << 1 | value & 1; if (context_data_position == bitsPerChar - 1) { context_data_position = 0; context_data.push(getCharFromInt(context_data_val)); context_data_val = 0; } else { context_data_position++; } value = value >> 1; } } else { value = 1; for (i = 0; i < context_numBits; i++) { context_data_val = context_data_val << 1 | value; if (context_data_position == bitsPerChar - 1) { context_data_position = 0; context_data.push(getCharFromInt(context_data_val)); context_data_val = 0; } else { context_data_position++; } value = 0; } value = context_w.charCodeAt(0); for (i = 0; i < 16; i++) { context_data_val = context_data_val << 1 | value & 1; if (context_data_position == bitsPerChar - 1) { context_data_position = 0; context_data.push(getCharFromInt(context_data_val)); context_data_val = 0; } else { context_data_position++; } value = value >> 1; } } context_enlargeIn--; if (context_enlargeIn == 0) { context_enlargeIn = Math.pow(2, context_numBits); context_numBits++; } delete context_dictionaryToCreate[context_w]; } else { value = context_dictionary[context_w]; for (i = 0; i < context_numBits; i++) { context_data_val = context_data_val << 1 | value & 1; if (context_data_position == bitsPerChar - 1) { context_data_position = 0; context_data.push(getCharFromInt(context_data_val)); context_data_val = 0; } else { context_data_position++; } value = value >> 1; } } context_enlargeIn--; if (context_enlargeIn == 0) { context_enlargeIn = Math.pow(2, context_numBits); context_numBits++; } } value = 2; for (i = 0; i < context_numBits; i++) { context_data_val = context_data_val << 1 | value & 1; if (context_data_position == bitsPerChar - 1) { context_data_position = 0; context_data.push(getCharFromInt(context_data_val)); context_data_val = 0; } else { context_data_position++; } value = value >> 1; } while (true) { context_data_val = context_data_val << 1; if (context_data_position == bitsPerChar - 1) { context_data.push(getCharFromInt(context_data_val)); break; } else context_data_position++; } return context_data.join(""); } function buildHref(ex) { const documentText = ex.state === undefined ? "" : typeof ex.state === "string" ? ex.state : JSON.stringify(ex.state, null, 2); return "https://console.typesafe.ai/decode#share/" + compressToEncodedURIComponent(JSON.stringify({ apiVersion: "v1", documentText, promptsText: JSON.stringify(ex.questions, null, 2), selectedModels: ex.selectedModels })); } const displayedExample = display === "questions" ? example.questions : example.state === undefined ? { questions: example.questions } : { state: example.state, questions: example.questions }; const code = JSON.stringify(displayedExample, null, 2); const href = buildHref(example); return <div style={{ margin: "1.25rem 0" }}> <CodeBlock language="json" filename={title ?? "request"}> {code}
Используйте Score, когда ответом является положение на непрерывном спектре (шкале), который вы можете описать пошагово. Например, насколько критичен баг, насколько доволен клиент или сколько опыта работы с Python имеет кандидат. Если ответ представляет собой один из фиксированного набора неупорядоченных вариантов, используйте Choice. Если это бинарный ответ «да» или «нет», используйте Noul. В разделе Выбор типа вопроса приведено сравнение всех трех типов.
Ответ Score возвращает положение на заданной вами шкале уровней в поле score, которое может принимать дробные значения между двумя уровнями. Модель также возвращает вероятность для каждого уровня в поле probabilities и показатель confidence для полученного ответа.
Числа перед каждым шагом — это позиции на шкале, описанные в подразделе Уровни.
Структура запроса
Тело POST-запроса к TypeSafe API содержит те же три поля верхнего уровня, что и любой другой тип вопроса: state (оцениваемый контент), model и questions. Каждый вопрос типа Score имеет следующие поля:
type: Всегда "score".
instructions: Вопрос, на который отвечает модель. Что именно оценивается.
criteria: Упорядоченный массив описаний уровней, от нижнего края шкалы к верхнему. Должно быть как минимум два уровня; API принимает до 10 уровней.
Ниже приведен запрос, где состояние — это отчет об ошибке, а вопрос — насколько критичен данный баг:
<TypesafeExample display="request" example={{ state: 'The export button crashes the settings page in Safari. It works in Chrome, but a few of our customers only use Safari.', selectedModels: ['jev-latest'], questions: { bug_severity: { type: 'score', instructions: 'How severe is the reported issue?', criteria: [ 'Cosmetic; no impact to functionality', 'Broken or degraded feature, but workaround exists', 'Blocking issue; no workaround exists', ], }, }, }} />
Вы сами выбираете ID вопроса — в данном случае bug_severity. Этот ID не отправляется в модель. Ответ возвращается под тем же ID.
Уровни
Каждый элемент массива criteria представляет собой уровень: точку на спектре возможных ответов, описанную словами. Номер уровня — это его индекс в массиве criteria, начиная с 0, поэтому три элемента выше соответствуют уровням 0, 1 и 2. Порядок элементов в массиве определяет их нумерацию.
Модель получает текстовые описания и ничего больше, и каждый уровень оценивается изолированно относительно состояния.
Значение score в ответе — это положение на числовой шкале уровней. Для трехшкальной градации оно лежит в диапазоне от 0 до 2 и может попадать в промежуток между двумя уровнями.
Наши клиентские SDK предоставляют типизированные вопросы. В Python тот же вопрос задается через Score:
PYTHON THEME={NULL}api.wedstack.ru/v1
from typesafe_sdk import Score, TypeSafeClient
with TypeSafeClient() as client:
response = client.system_one(
state="The export button crashes the settings page in Safari. It works in Chrome, but a few of our customers only use Safari.",
questions={
"bug_severity": Score(
instructions="How severe is the reported issue?",
criteria=[
"Cosmetic; no impact to functionality",
"Broken or degraded feature, but workaround exists",
"Blocking issue; no workaround exists",
],
),
},
)
print(response.answers["bug_severity"].score)
Используйте метод system_one или эндпоинт https://api.typesafe.ai/v1/systemone для вызова модели System One. Поле model определяет, какая модель обрабатывает запрос. В руководстве Как создавать решения с помощью TypeSafe рассматривается, где в коде следует вызывать модель.
`instructions` и каждый уровень в `criteria` могут быть строкой, объектом или массивом. Начните со строк. Используйте объект, когда уровню требуется описание плюс несколько примеров ситуаций. См. подраздел [Структурированные описания уровней](#structured-level-descriptions) ниже и [справочник API](/docs/api#param-instructions-2).
Структура ответа
В ответе в словаре answers возвращается по одной записи на каждый вопрос под соответствующими идентификаторами из запроса. Ниже показан ответ на пример запроса выше:
probabilities: Вероятность каждого уровня со строковыми ключами номеров уровней. Сумма значений равна 1.
score: Положение на числовой шкале уровней от 0 до максимального индекса уровня (здесь 2). Это сумма произведений номеров уровней на их вероятности: 0 × 0.0 + 1 × 0.57 + 2 × 0.43 = 1.43.
legend: Словарь, сопоставляющий номер каждого уровня с его текстовым описанием.
confidence: Число от 0 до 1, рассчитанное на основе формы распределения probabilities. Ярко выраженный пик на одном уровне означает высокую уверенность. Распределение вероятности по нескольким уровням означает низкую уверенность.
Оценка 1.43 означает, что модель колеблется между уровнями 1 и 2, склоняясь к уровню 1. Это точно соответствует содержанию отчета: функция экспорта сломана, и переход на Chrome решает проблему для большинства пользователей, но не для тех, кто работает исключительно в Safari. Модель присваивает 0.57 варианту «существует обходной путь» и 0.43 варианту «обходного пути нет», а уверенность равна 0.35 из-за разделения мнений.
В Python SDK объект ScoreAnswer содержит типизированные поля score, confidence, probabilities и legend. При этом SDK использует целочисленные ключи уровней для probabilities и legend вместо строк.
Интерпретация Score
Посмотрим, как меняется оценка при различных входных данных на примере вопроса и уровней из рассмотренного выше запроса:
PLAINTEXTapi.wedstack.ru/v1
"Насколько критична описанная проблема?"
→ 0: Косметическая; не влияет на функциональность
→ 1: Функция сломана или деградировала, но есть обходной путь
→ 2: Блокирующая проблема; обходного пути нет
Посмотрим, как разные отчеты о багах влияют на оценку:
The export button is misaligned by a few pixels on the settings page.
0.0
1.0
1.0
0.0
0.0
PLAINTEXTapi.wedstack.ru/v1
<tr>
<td>The PDF export button does nothing when clicked. I can still export to CSV and convert it myself, but that takes ages.</td>
<td>1.0</td><td>1.0</td><td>0.0</td><td>1.0</td><td>0.0</td>
</tr>
<tr>
<td>Export to PDF fails with a spinner that never finishes. Some of our team say CSV export still works for them, others say it fails too.</td>
<td>1.11</td><td>0.84</td><td>0.0</td><td>0.89</td><td>0.11</td>
</tr>
<tr>
<td>The export button crashes the settings page in Safari. It works in Chrome, but a few of our customers only use Safari.</td>
<td>1.43</td><td>0.35</td><td>0.0</td><td>0.57</td><td>0.43</td>
</tr>
<tr>
<td>Nobody on our team can log in since this morning. We get a 500 error on every attempt.</td>
<td>2.0</td><td>1.0</td><td>0.0</td><td>0.0</td><td>1.0</td>
</tr>
В этих примерах уверенность 1.0 означает, что полученное распределение сосредоточило всю вероятность на одном уровне. Это характеризует степень определенности модели, а не абсолютную гарантию безошибочности ответа.
Score представляет собой математическое ожидание номеров уровней, взвешенное по вероятностям. В третьем и четвертом примерах вероятность разделена между уровнями 1 и 2. Больший вес уровня 2 повышает итоговый балл. Это не измеряет долю пользователей, у которых нет обходного пути.
Различные распределения могут давать одинаковый балл score. Оценка 1.0 может означать, что вся вероятность находится на уровне 1, либо что по половине вероятности приходится на уровни 0 и 2. Всегда анализируйте probabilities и confidence вместе со значением score, чтобы различать подобные ситуации.
Дробная оценка — это положение на шкале. Вы можете использовать ее для ранжирования отчетов по критичности или округлять до ближайшего целого уровня, когда коду требуется дискретное решение. В нашем кукбуке по сопоставлению сущностей показан пример принятия решения через округление до ближайшего уровня.
Низкая уверенность для Score обычно указывает на одну из трех причин: уровни частично перекрываются для данного состояния, вопрос измеряет сразу несколько измерений, либо состояние содержит недостаточно информации для уверенного вывода. Рекомендации по использованию уверенности в коде см. в документации Уверенность (Confidence).
Написание качественных уровней
Описывайте ситуации, а не абстрактные степени. Формулировка «Функция сломана или деградировала, но есть обходной путь» дает модели конкретный ориентир для сопоставления с состоянием. «Умеренно критичный» — не дает. Конкретные описания помогают модели четко различать уровни. Проверяйте ответы на известных примерах: высокая уверенность сама по себе не доказывает, что описание лучше.
Каждый уровень оценивается отдельно. Модель не видит номер уровня или его соседей, поэтому формулировка «хуже предыдущего уровня» для нее ничего не значит, а числа внутри описаний или инструкций не помогают. Вот что произойдет, если в качестве уровней передать просто цифры для отчета о смещенной кнопке из таблицы выше:
PLAINTEXTapi.wedstack.ru/v1
instructions: "Rate severity from 0 to 2, where 2 is worst"
criteria: ["0", "1", "2"]
→ score 0.55, confidence 0.33, probabilities 0: 0.45, 1: 0.55, 2: 0.0
Тот же отчет с тремя описательными уровнями получает оценку 0.0 с уверенностью 1.0. Имея только цифры, модель лишена опорных ориентиров и делит вероятность между 0 и 1.
Используйте столько уровней, сколько можете четко и различимо описать, вплоть до 10. Трех уровней часто вполне достаточно. Не добавляйте уровни, которые не можете однозначно разграничить.
Держите каждый вопрос Score строго в рамках одного измерения. Если описание гласит «пунктуальный, умный и опытный», вопрос оценивает сразу три вещи, и входные данные, где кандидат силен в одном, но слаб в другом, не смогут найти себе места. Уверенность упадет, а оценка потеряет смысл. Разделите такой запрос на отдельные вопросы Score по каждому аспекту и объедините их в коде, как показано в следующем разделе.
Если на верхнем конце шкалы существует редкий экстремальный сценарий, требующий особой реакции, выделите его в отдельный уровень. К шкале тональности, заканчивающейся на «очень зол», можно добавить уровень «оскорбления или угрозы». Без этого уровня оба сообщения могут получить близкий максимальный балл, и числовая оценка не позволит их различить.
Если промежуточных состояний не существует вовсе, а ответ представляет собой одну из дискретных категорий, используйте вместо этого Choice или разбейте задачу на несколько вопросов Noul. Обязательно тестируйте ваши уровни на реальных данных: две разные формулировки одной и той же шкалы могут вести себя по-разному на вашем корпусе текстов.
Разделение сложного суждения на несколько вопросов Score
Сложное суждение, зависящее от нескольких факторов, лучше всего разделить на отдельные вопросы Score по каждому фактору. Затем вы можете объединить возвращенные TypeSafe оценки в коде. Некоторые вопросы Score могут иметь больший вес, поэтому назначьте каждому весовой коэффициент в зависимости от его важности. Веса определяете вы сами. Если совокупный результат расходится с решением вашей команды, просто скорректируйте коэффициенты в коде и перезапустите пайплайн. Отправляйте все вопросы Score в одном запросе: они вычисляются параллельно. Добавление вопросов практически не влияет на задержку и стоит лишь небольшого количества дополнительных токенов; см. раздел Задавайте несколько вопросов одновременно.
В запросе ниже рассматривается тикет с зависающим PDF-экспортом с дополнительным контекстом. Задаются три вопроса Score: насколько критичен баг, насколько раздражен клиент и насколько подробно отчет описывает проблему для инженера.
<TypesafeExample display="request" example={{ state: 'Export to PDF fails with a spinner that never finishes. Some of our team say CSV export still works for them, others say it fails too. This is the third time I'm writing in and honestly I'm done. Steps: open any report, click Export, choose PDF. Chrome 128 on macOS.', selectedModels: ['jev-latest'], questions: { severity: { type: 'score', instructions: 'How severe is the reported issue?', criteria: [ 'Cosmetic; no impact to functionality', 'Broken or degraded feature, but workaround exists', 'Blocking issue; no workaround exists', ], }, frustration: { type: 'score', instructions: 'How frustrated is the customer?', criteria: [ 'Calm, just stating facts', 'Frustrated but civil', 'Very angry, strong language or threatening to leave', ], }, report_quality: { type: 'score', instructions: 'How much does the report give an engineer to work with?', criteria: [ 'No detail; just says something is broken', 'Names the feature but no steps or environment', 'Steps to reproduce or environment, but not both', 'Steps to reproduce and environment', ], }, }, }} />
Ответ TypeSafe:
JSON THEME={NULL}api.wedstack.ru/v1
{
"model": "jev-1.13.0",
"answers": {
"severity": {
"type": "score",
"score": 1.24,
"confidence": 0.64,
"legend": {
"0": "Cosmetic; no impact to functionality",
"1": "Broken or degraded feature, but workaround exists",
"2": "Blocking issue; no workaround exists"
},
"probabilities": {
"0": 0.0,
"1": 0.76,
"2": 0.24
}
},
"frustration": {
"type": "score",
"score": 1.28,
"confidence": 0.58,
"legend": {
"0": "Calm, just stating facts",
"1": "Frustrated but civil",
"2": "Very angry, strong language or threatening to leave"
},
"probabilities": {
"0": 0.0,
"1": 0.72,
"2": 0.28
}
},
"report_quality": {
"type": "score",
"score": 3.0,
"confidence": 1.0,
"legend": {
"0": "No detail; just says something is broken",
"1": "Names the feature but no steps or environment",
"2": "Steps to reproduce or environment, but not both",
"3": "Steps to reproduce and environment"
},
"probabilities": {
"0": 0.0,
"1": 0.0,
"2": 0.0,
"3": 1.0
}
}
},
"usage": {
"input_tokens": 468,
"output_tokens": 43
}
}
Каждый вопрос оценивается изолированно относительно тикета и получает свой балл:
severity: 1.24 при уверенности 0.64. Та же трактовка, что и ранее: функция сломана, но у части пользователей есть обходной путь.
frustration: 1.28 при уверенности 0.58. Текст сформулирован вежливо, однако фразы «пишу в третий раз» и «с меня хватит» смещают часть оценки к верхнему уровню, поэтому модель делит 0.72 и 0.28 между уровнями «раздражен, но корректен» и «очень зол». Для этого тикета два уровня частично перекрываются, чем и объясняется умеренная уверенность.
report_quality: 3.0 при уверенности 1.0. Указаны и шаги воспроизведения, и версия браузера.
Поскольку три шкалы имеют разное количество уровней, перед их объединением нормализуйте каждую оценку. Четырехуровневая шкала возвращает значения от 0 до 3, а трехуровневая — от 0 до 2, поэтому максимальный балл на одной шкале численно больше максимального балла на другой. Разделите каждый балл на максимальный номер его уровня, len(criteria) - 1, чтобы привести все оценки к интервалу от 0 до 1. Тогда веса будут в точности отражать задуманное: вес 0.6 для критичности и 0.3 для раздражения сделает критичность ровно в два раза важнее.
Приведенный ниже код на Python SDK задает эти три вопроса, нормализует каждый балл и вычисляет итоговый приоритет:
PYTHON THEME={NULL}api.wedstack.ru/v1
from typesafe_sdk import Score, TypeSafeClient
TRIAGE_QUESTIONS = {
"severity": Score(
instructions="How severe is the reported issue?",
criteria=[
"Cosmetic; no impact to functionality",
"Broken or degraded feature, but workaround exists",
"Blocking issue; no workaround exists",
],
),
"frustration": Score(
instructions="How frustrated is the customer?",
criteria=[
"Calm, just stating facts",
"Frustrated but civil",
"Very angry, strong language or threatening to leave",
],
),
"report_quality": Score(
instructions="How much does the report give an engineer to work with?",
criteria=[
"No detail; just says something is broken",
"Names the feature but no steps or environment",
"Steps to reproduce or environment, but not both",
"Steps to reproduce and environment",
],
),
}
def normalized(answers, question_id: str) -> float:
"""Приведение оценки к диапазону от 0 до 1 делением на максимальный номер уровня."""
top_level = len(TRIAGE_QUESTIONS[question_id].criteria) - 1
return answers[question_id].score / top_level
def priority(ticket: str) -> float:
with TypeSafeClient() as client:
response = client.system_one(
state=ticket,
questions=TRIAGE_QUESTIONS,
)
answers = response.answers
severity = normalized(answers, "severity")
frustration = normalized(answers, "frustration")
report_quality = normalized(answers, "report_quality")
# Подробный отчет помогает инженеру в расследовании, поэтому немного повышает приоритет.
return 0.6 * severity + 0.3 * frustration + 0.1 * report_quality
Для рассмотренного ответа нормализованные оценки составляют 0.62 для критичности, 0.64 для раздражения и 1.0 для качества отчета. Приоритет равен 0.6 × 0.62 + 0.3 × 0.64 + 0.1 × 1.0 = 0.664, что округляется до 0.66.
Весовые коэффициенты находятся прямо в вашем коде, поэтому вы точно видите, как формируется итоговое число, и можете изменить его, если ранжирование не совпадает с видением команды. Если позже вам понадобятся дополнительные вопросы Score, просто добавьте их в TRIAGE_QUESTIONS. Количество сетевых вызовов останется равным единице. Этот подход декомпозиции сложного суждения на отдельные оценки Score с последующим взвешенным объединением в коде называется паттерном Композитная оценка (Composite scoring).
Структурированные описания уровней
Начните с простого текстового описания каждого уровня. Если модель продолжает колебаться между двумя соседними уровнями на входных данных, которые вам кажутся очевидными, передайте для каждого уровня объект вместо строки, добавив поле для описания сути уровня и поле с примерами ситуаций. Используйте одинаковые имена полей для всех уровней, чтобы модель могла корректно сопоставлять их.
Ниже приведен запрос по тому же тикету с зависающим PDF-экспортом, но с добавлением примеров к каждому уровню:
<TypesafeExample display="request" example={{ state: 'Export to PDF fails with a spinner that never finishes. Some of our team say CSV export still works for them, others say it fails too.', selectedModels: ['jev-latest'], questions: { bug_severity: { type: 'score', instructions: 'How severe is the reported issue?', criteria: [ { what: 'Cosmetic; no impact to functionality', examples: ['typo in a label', 'misaligned icon'], }, { what: 'Broken or degraded feature, but workaround exists', examples: ['export fails in one browser but works in another'], }, { what: 'Blocking issue; no workaround exists', examples: ['cannot log in', 'data loss'], }, ], }, }, }} />
Ответ:
JSON THEME={NULL}api.wedstack.ru/v1
{
"model": "jev-1.13.0",
"answers": {
"bug_severity": {
"type": "score",
"score": 1.09,
"confidence": 0.87,
"legend": {
"0": {
"what": "Cosmetic; no impact to functionality",
"examples": [
"typo in a label",
"misaligned icon"
]
},
"1": {
"what": "Broken or degraded feature, but workaround exists",
"examples": [
"export fails in one browser but works in another"
]
},
"2": {
"what": "Blocking issue; no workaround exists",
"examples": [
"cannot log in",
"data loss"
]
}
},
"probabilities": {
"0": 0.0,
"1": 0.91,
"2": 0.09
}
}
},
"usage": {
"input_tokens": 379,
"output_tokens": 18
}
}
С простыми строками этот тикет получил оценку 1.11 с уверенностью 0.84. С примерами оценка составила 1.09 при уверенности 0.87 — небольшое изменение, так как простые строки и так хорошо позиционировали проблему. Эффект становится гораздо более заметным в тех случаях, когда простые строки оставляли модель в нерешительности, как показано в следующей таблице.
Примеры направляют модель, но они помогают только тогда, когда действительно похожи на ваши реальные входные данные. В таблице ниже показан исходный отчет по Safari с тремя различными конфигурациями уровней:
Описание уровней
score
confidence
Простая строка: без объекта и примеров
1.43
0.35
Добавлен массив примеров с подходящим примером: «экспорт не работает в одном браузере, но работает в другом»
1.03
0.96
Добавлен массив примеров с примером, не связанным с браузерами: «поиск не работает, но просмотр категорий доступен»
1.43
0.35
В этом сравнении релевантный пример концентрирует практически всю вероятность на одном уровне. Нерелевантный пример дает тот же результат, что и простые строки. При этом высокая уверенность сама по себе не гарантирует, какой ответ является правильным. Подбирайте примеры с известными ожидаемыми уровнями и обязательно проверяйте обновленные описания на независимых выборках перед их утверждением.