Спекулятивный 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, мы рекомендуем включать все необходимые вашей системе вопросы в один запрос, а затем с помощью кода решать, какие результаты релевантны. Все вопросы оцениваются параллельно, поэтому добавление дополнительных вопросов практически не влияет на время отклика.
Пример: триаж тикетов поддержки
Представьте, что вы создаете систему службы поддержки, которая должна распределять входящие тикеты. Вам необходимо классифицировать тикет по категориям. Если это сообщение об ошибке (баг-репорт), вам также нужно определить критичность проблемы.
Вместо того чтобы сначала запрашивать категорию, а затем отдельным повторным вызовом уточнять критичность, вы можете запросить оба параметра одновременно. Если тикет окажется не сообщением об ошибке, вы просто проигнорируете результат вопроса о критичности бага.
%%{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'],
},
},
}}
/>
Шаг 2: маршрутизация в коде
Ваш код определяет релевантные данные на основе результата классификации:
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), когда они нужны.