ДокументацияАрхитектурные паттерныМаршрутизация намерений

Маршрутизация намерений

Classify incoming requests and route each to the optimal handler: deterministic logic, a specialist LLM, or a human.

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

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;
break;
} 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}


;
}

Далеко не каждый запрос пользователя требует одинакового обработчика. На некоторые запросы можно ответить простым поиском в базе данных. Для других требуется LLM с узкоспециализированным контекстом. А для третьих необходим человек. TypeSafe может выступать быстрым и экономичным классификатором на входе всей системы, определяя, какой именно обработчик должен быть задействован.

Пример: маршрутизация обращений клиентов

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

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
    message["сообщение клиента"]

    subgraph req["TypeSafe оценивает вопросы<br/>параллельно"]
        direction TB
        intent["<b>Choice:</b> намерение (intent)"]
        complexity["<b>Score:</b> сложность (complexity)"]
        %% Invisible links stack the questions; they are answered in parallel.
        intent ~~~ complexity
    end

    message -- "один запрос<br/>сообщение + 2 вопроса" --> req
    req -- "один ответ<br/>2 ответа с<br/>уверенностью" --> confidence{"<b>уверенность намерения<br/>≥ 0.5?</b><br/>ваш код"}
    confidence -- "нет" --> human["оператор-человек"]
    confidence -- "да" --> route{"<b>какое намерение?</b><br/>"}
    route -- "order_status" --> order["поиск статуса заказа<br/>детерминированный код"]
    route -- "product_question" --> product["LLM-специалист по продукту"]
    route -- "return_exchange" --> returns["LLM-специалист по возвратам"]
    route -- "complaint" --> escalate{"<b>сложность > 1<br/>или уверенность < 0.5?</b><br/>"}
    escalate -- "да" --> human
    escalate -- "нет" --> complaint["LLM разрешения жалоб"]

Шаг 1: классификация намерения и сложности

<TypesafeExample
title="questions"
display="questions"
example={{
questions: {
intent: {
type: 'choice',
instructions: 'The primary intent of this customer message',
criteria: {
order_status: 'Asking about an existing order',
product_question: 'Asking about a product before buying',
return_exchange: 'Wants to return or exchange something',
complaint: 'Unhappy with experience, wants resolution',
},
},
complexity: {
type: 'score',
instructions: 'How complex is this request to resolve',
criteria: [
'Simple lookup or standard procedure',
'Requires some judgment or multi-step process',
'Unusual situation, edge case, or escalation needed',
],
},
},
}}
/>

Шаг 2: маршрутизация к оптимальному обработчику

PYTHON TITLE="ROUTING.PY" THEME={NULL} api.wedstack.ru/v1
def route_ticket(ticket_id, response):
    intent = response.answers["intent"]
    complexity = response.answers["complexity"]

    if intent.confidence < 0.5:
        # Если уверенности недостаточно для классификации, перенаправляем человеку
        return route_to_human_agent(ticket_id)

    if intent.choice == "order_status":
        handle_order_status(ticket_id)

    elif intent.choice == "product_question":
        handle_with_llm(ticket_id, PRODUCT_SPECIALIST)

    elif intent.choice == "return_exchange":
        handle_with_llm(ticket_id, RETURNS_SPECIALIST)

    elif intent.choice == "complaint":
        low_confidence = complexity.confidence < 0.5
        # Более высокий балл complexity.score склоняется к необходимости эскалации
        if complexity.score > 1 or low_confidence:
            # Слишком сложно для безопасной автоматизации или мы не уверены в сложности — передаем человеку.
            route_to_human_agent(ticket_id)
        else:
            handle_with_llm(ticket_id, COMPLAINT_RESOLUTION)

Одно намерение направляется в детерминированный код без использования LLM вообще. Два других передаются специализированным LLM, загруженным соответствующим контекстом. Еще одно намерение использует оценку сложности, чтобы выбрать между LLM и оператором-человеком. TypeSafe выполняет всю классификацию за один быстрый вызов; дорогие ресурсы задействуются только для тех запросов, которым они действительно необходимы.

Обратите внимание на дополнительную проверку уверенности для оценки сложности. Как отмечается в разделе Уверенность (Confidence), всегда важно учитывать смысл низкой уверенности в контексте конкретной системы и последствий принимаемого решения.