ДокументацияАрхитектурные паттерныСпекулятивный Fan-out

Спекулятивный Fan-out

Send many questions in a single call, including speculative ones, and let your code decide what's relevant.

Отправляйте множество вопросов в одном вызове, включая гипотетические, оставляя вашему коду решение о том, какие данные релевантны.

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}


;
}

Поскольку TypeSafe поддерживает отправку множества вопросов в одном вызове API, мы рекомендуем включать все необходимые вашей системе вопросы в один запрос, а затем с помощью кода решать, какие результаты релевантны. Все вопросы оцениваются параллельно, поэтому добавление дополнительных вопросов практически не влияет на время отклика.

Пример: триаж тикетов поддержки

Представьте, что вы создаете систему службы поддержки, которая должна распределять входящие тикеты. Вам необходимо классифицировать тикет по категориям. Если это сообщение об ошибке (баг-репорт), вам также нужно определить критичность проблемы.

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

MERMAID ACTIONS={TRUE} THEME={NULL} api.wedstack.ru/v1
%%{init: {"fontFamily": "Inter, sans-serif", "flowchart": {"rankSpacing": 35, "wrappingWidth": 300, "subGraphTitleMargin": {"top": 8, "bottom": 60}}}}%%
flowchart LR
    t["тикет в службу поддержки"]

    subgraph req["Модель TypeSafe AI<br/>оценивает каждый вопрос<br/>относительно тикета параллельно"]
        direction TB
        c["<b>Choice:</b> категория"]
        b["<b>Score:</b> критичность бага"]
        r["<b>Noul:</b> есть шаги воспроизведения?"]
        f["<b>Noul:</b> запрошен возврат средств?"]
        s["<b>Score:</b> уровень раздражения"]
        %% invisible links: without an edge these share a rank and sit side by side
        c ~~~ b ~~~ r ~~~ f ~~~ s
    end

    t -- "один запрос<br/>тикет + 5 вопросов" --> req
    req -- "один ответ: 5 ответов<br/>решения + вероятности" --> route{"<b>фильтрация, объединение и маршрутизация</b><br/>в вашем коде"}
    route -- "bug_report" --> eng["чтение критичности и шагов<br/>эскалация или бэклог"]
    route -- "billing" --> bill["запрошен возврат<br/>отправка в биллинг"]
    route -- "feature_request" --> feat["логирование запроса<br/>отправка разработчикам"]

Шаг 1: спекулятивный веерный опрос

<TypesafeExample
title="questions"
display="questions"
example={{
state:
"Hi, I placed an order (#98423) last Thursday and was charged twice. I also can't log in after the site update, and adding Apple Pay would be really helpful. This is getting frustrating.",
questions: {
category: {
type: 'choice',
instructions: 'Determine the broad category of this support ticket',
criteria: {
bug_report:
'The user is reporting something that is broken or producing errors',
billing: 'Charges, invoices, refunds, subscriptions',
feature_request: 'The user is requesting new functionality',
account: 'Login, permissions, profile, security',
},
},
bug_severity: {
type: 'score',
instructions: 'How severe is the reported issue',
criteria: [
'Cosmetic; no impact to functionality',
'Broken or degraded feature; workaround exists',
'Blocking issue; no workaround exists',
],
},
has_reproducible_steps: {
type: 'noul',
instructions:
'The user describes specific steps to reproduce the issue',
},
refund_requested: {
type: 'noul',
instructions: 'The user is explicitly asking for a refund or credit',
},
frustration: {
type: 'score',
instructions: 'How frustrated the user appears',
criteria: ['Calm, matter-of-fact', 'Frustrated but civil', 'Very angry'],
},
},
}}
/>

**Спекулятивные вопросы:** `bug_severity` и `has_reproducible_steps` имеют значение только в том случае, если тикет является баг-репортом. `refund_requested` имеет значение только для биллинга. Мы включаем все эти вопросы сразу, поскольку дополнительные вопросы практически не влияют на время отклика. Если тикет окажется запросом новой функции, оценка критичности бага будет просто нерелевантна, и соответствующая ветка вашего кода проигнорирует её.

Шаг 2: маршрутизация в коде

Ваш код определяет релевантные данные на основе результата классификации:

PYTHON TITLE="TRIAGE.PY" THEME={NULL} api.wedstack.ru/v1
category = response.answers["category"]
bug_severity = response.answers["bug_severity"]
bug_repro = response.answers["has_reproducible_steps"]
refund = response.answers["refund_requested"]
frustration = response.answers["frustration"]

if category.choice == "bug_report":
    if bug_severity.score > 1.5 and bug_repro.noul > 0.6:
        escalate_to_engineering(ticket_id, severity="high")
    else:
        add_to_bug_backlog(ticket_id)

elif category.choice == "billing":
    if refund.noul > 0.7:
        route_to_billing_with_flag(ticket_id, refund_likely=True)
    else:
        route_to_billing(ticket_id)

elif category.choice == "feature_request":
    log_feature_request(ticket_id)

# Уровень раздражения полезен независимо от категории
if frustration.score > 1.5:
    flag_for_priority_response(ticket_id)

Всё, что необходимо для полного дерева решений, получается из одного вызова. Спекулятивные вопросы игнорируются, если они нерелевантны, и экономят дополнительный сетевой запрос (round trip), когда они нужны.