ДокументацияАрхитектурные паттерныСоставной скоринг

Составной скоринг

Break a complex judgment into atomic scores, combine with weights you control in code.

Разделяйте сложное комплексное суждение на атомарные оценки и объединяйте их с весами, контролируемыми в коде.

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) {
if (uncompressed == null) return "";
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}


;
}

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

Пример: скрининг резюме

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

MERMAID ACTIONS={TRUE} THEME={NULL} api.wedstack.ru/v1
%%{init: {"fontFamily": "Inter, sans-serif", "flowchart": {"rankSpacing": 35, "wrappingWidth": 300, "subGraphTitleMargin": {"top": 12, "bottom": 36}}}}%%
flowchart LR
    resume["резюме кандидата"]

    subgraph req["TypeSafe оценивает вопросы<br/>параллельно"]
        direction TB
        py["<b>Score:</b> глубина владения Python"]
        lead["<b>Score:</b> лидерство в команде"]
        arch["<b>Score:</b> проектирование систем"]
        general["<b>Score:</b> гибкость / кругозор (generalist)"]
        %% Invisible links stack the questions; they are answered in parallel.
        py ~~~ lead ~~~ arch ~~~ general
    end

    resume -- "один запрос<br/>резюме + 4 вопроса" --> req
    req -- "один ответ<br/>4 оценки (score)" --> normalize["<b>нормализация оценок к 0–1</b><br/>деление каждой на 4 в вашем коде"]
    normalize --> ic["<b>веса для Senior IC</b><br/>40% Python + 10% лидерство<br/>40% архитектура + 10% кругозор"]
    normalize --> em["<b>веса для Engineering Manager</b><br/>15% Python + 40% лидерство<br/>20% архитектура + 25% кругозор"]
    ic --> rank["ранжирование кандидатов<br/>для каждой роли"]
    em --> rank

Шаг 1: независимая оценка каждого измерения

<TypesafeExample
title="questions"
display="questions"
example={{
questions: {
python_depth: {
type: 'score',
instructions:
'How much depth of python experience does this candidate have, based on the supplied resume?',
criteria: [
'No Python experience mentioned',
'Mentioned but no detail',
'Used in projects, some specifics',
'Primary language, multiple projects',
'Deep expertise: architecture, performance, libraries',
],
},
team_leadership: {
type: 'score',
instructions:
'How much experience does this candidate have managing or leading engineering teams?',
criteria: [
'No management experience mentioned',
'Informal mentorship or tech lead role',
'Led a small team or project',
'Managed a team with direct reports',
'Managed multiple teams or an engineering org',
],
},
system_design: {
type: 'score',
instructions:
'How much experience does this candidate have designing large-scale or distributed systems?',
criteria: [
'No architecture work mentioned',
'Contributed to design discussions',
'Designed components of a larger system',
'Owned architecture of a significant system',
'Designed systems at scale across multiple domains',
],
},
generalist: {
type: 'score',
instructions:
'How much evidence is there that this candidate picks up unfamiliar tools, roles, or domains outside their core specialty?',
criteria: [
'Only one domain or role mentioned',
'Some variety but within a narrow field',
'Worked across a few different areas or tech stacks',
'Regularly moved between domains, wore many hats',
'Track record of ramping up in unfamiliar areas and delivering',
],
},
},
}}
/>

Шаг 2: объединение с учетом весов

Каждое измерение нормализуется в диапазон 0–1 и взвешивается. Весовые коэффициенты позволяют легко настраивать относительную важность каждого фактора, не теряя при этом точности и нюансов индивидуальных оценок.

PYTHON TITLE="SCORING.PY" THEME={NULL} api.wedstack.ru/v1
py      = response.answers["python_depth"].score / 4
lead    = response.answers["team_leadership"].score / 4
arch    = response.answers["system_design"].score / 4
general = response.answers["generalist"].score / 4

# Старший индивидуальный специалист (Senior IC)
ic_score = (0.40 * py) + (0.10 * lead) + (0.40 * arch) + (0.10 * general)

# Технический менеджер (Engineering Manager)
em_score = (0.15 * py) + (0.40 * lead) + (0.20 * arch) + (0.25 * general)

Это позволяет ранжировать кандидатов на основе итогового композитного балла. Но что ещё важнее, вы получаете полную прозрачность того, как именно рассчитывается финальный балл. Если кандидаты с наивысшим рейтингом не соответствуют вашим ожиданиям, вы можете просто скорректировать веса в коде для достижения идеального баланса.