Choice (Выбор альтернативы)
A Choice is a System One question type for selecting one option from a defined set. The answer includes the selected option, a probability for each option, and confidence.
Choice — это тип вопроса System One для выбора одного варианта из заданного набора. Ответ включает выбранный вариант, вероятность для каждого варианта и уверенность (confidence).
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_position = 0;
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}
}
Используйте Choice, когда ответом является один вариант из фиксированного набора. Например, какая команда должна обработать тикет, к какой категории относится товар или на каком языке написан фрагмент кода. Если ответ представляет собой положение на шкале, используйте Score. Если это бинарный ответ «да» или «нет», используйте Noul. В разделе Выбор типа вопроса приведено сравнение всех трех типов.
Ответ Choice возвращает выбранный вариант в поле choice. Модель также возвращает вероятность для каждого варианта в поле probabilities и показатель confidence для выбранного варианта.
Примеры вопросов:
"На каком языке программирования написан этот код"
→ варианты: python, javascript, typescript, go, rust, other
"Какой это тип встречи на основе названия и описания"
→ варианты: standup, planning, retrospective, one on one, brainstorm, none of the above
"К какой категории товаров относится этот предмет"
→ варианты: electronics, clothing, home garden, food and beverage
Структура запроса
Тело POST-запроса к TypeSafe API имеет определенную структуру. На верхнем уровне находятся три поля: state (оцениваемый контент), model и questions (словарь вопросов, где ключами являются выбранные вами идентификаторы, а значениями — объекты вопросов). Каждый вопрос типа Choice имеет следующие поля:
type: Всегда"choice".instructions: Вопрос, на который отвечает модель.criteria: Варианты ответов в виде словаря (map). Каждый ключ — имя варианта, а значение — его описание.
Ниже приведен запрос, где состояние — это обращение в службу поддержки интернет-магазина обуви, а вопрос заключается в том, какая команда должна его обработать:
<TypesafeExample
display="request"
example={{
state: 'My running shoes arrived in the wrong size. Can I swap them for a size 10?',
selectedModels: ['jev-latest'],
questions: {
department: {
type: 'choice',
instructions: 'Which team should handle this?',
criteria: {
returns: 'Exchanges, wrong or damaged items',
shipping: 'Delivery status, delays, lost packages',
billing: 'Charges, invoices, payment problems',
},
},
},
}}
/>
Вы сами выбираете ID вопроса — в данном случае department. Ответ возвращается под тем же ID. Модель никогда не видит сам ID вопроса. Имена вариантов и их описания отправляются в модель, поэтому формулируйте описания так, чтобы они четко разграничивали варианты между собой.
Наши клиентские SDK предоставляют типизированные вопросы. В Python тот же вопрос задается через Choice:
from typesafe_sdk import Choice, TypeSafeClient
with TypeSafeClient() as client:
response = client.system_one(
state="My running shoes arrived in the wrong size. Can I swap them for a size 10?",
questions={
"department": Choice(
instructions="Which team should handle this?",
criteria={
"returns": "Exchanges, wrong or damaged items",
"shipping": "Delivery status, delays, lost packages",
"billing": "Charges, invoices, payment problems",
},
),
},
)
print(response.answers["department"].choice)
Используйте метод system_one или эндпоинт https://api.typesafe.ai/v1/systemone для вызова модели System One. Поле model определяет, какая модель обрабатывает запрос. В разделе Как создавать решения с помощью TypeSafe рассматривается, в каких местах кода следует вызывать модель.
Используйте один из наших клиентских SDK или вызывайте HTTP API напрямую. Если интеграцию пишет агент разработки, сначала установите скилл TypeSafe для агентов, чтобы он знал форматы запросов и ответов.
Структура ответа
В ответе в словаре answers возвращается по одной записи на каждый вопрос под соответствующими идентификаторами из запроса. Ниже показан ответ на пример запроса выше:
{
"model": "jev-1.13.0",
"answers": {
"department": {
"type": "choice",
"choice": "returns",
"confidence": 1.0,
"probabilities": {
"shipping": 0.0,
"returns": 1.0,
"billing": 0.0
}
}
},
"usage": {
"input_tokens": 328,
"output_tokens": 34
}
}
Помимо type, каждый ответ Choice содержит три значения:
choice: Вариант с наибольшей вероятностью.probabilities: Полное распределение вероятностей по всем вариантам. Сумма всех значений равна 1.confidence: Число от 0 до 1, рассчитанное на основе формы распределенияprobabilities. Пологое распределение, в котором вероятность распределена по нескольким вариантам, означает низкую уверенность. Ярко выраженный пик на одном варианте означает высокую уверенность.
Этот тикет простой, поэтому вся вероятность сосредоточена на returns, а уверенность равна 1.0. Если бы в тикете упоминались и неверный размер, и отсутствующий возврат денег, вероятность разделилась бы между returns и billing, и уверенность снизилась бы.
Хорошая практика: задавайте более одного вопроса за вызов
Задавайте все вопросы типа Choice, которые могут потребоваться вашему коду, в одном запросе, а не делайте отдельный запрос на каждый вопрос. Вопросы оцениваются параллельно. Добавление вопросов практически не меняет время отклика, а код может проигнорировать ненужные ответы. Дополнительные вопросы расходуют токены. В разделе Задавайте несколько вопросов одновременно это описано подробно; следующий раздел демонстрирует пять вопросов Choice в одном вызове.
Та же логика применима и к вариантам внутри одного вопроса Choice. Вопрос Choice принимает до 255 вариантов, и добавление вариантов стоит лишь нескольких токенов каждый, поэтому передавайте модели полный список команд, категорий или продуктов, а не урезанный список. Добавьте вариант other или none of the above, если список может покрывать не все возможные входные данные, чтобы модель могла явно указать, что ни один вариант не подходит.
Для классификации документов по глубокой иерархии или большой таксономии объединяйте вопросы Choice по уровням. В кукбуке по иерархической классификации показано, как запускать лучевой поиск (beam search) по вероятностям Choice, сохраняя K лучших путей-кандидатов на каждом уровне вместо выбора одной жадной ветки.
Более сложный пример
Базовый пример выше маршрутизирует тикет в команду. Более крупной системе поддержки также могут потребоваться причина возврата, проблема с доставкой, желаемое решение клиента и его тон общения.
Запрос ниже задает пять вопросов Choice относительно тикета, который является более неоднозначным: он затрагивает три команды и не говорит прямо, чего хочет клиент.
<TypesafeExample
display="request"
example={{
state: 'Shoes arrived two weeks late and in the wrong size. Also I see two charges of $120 on my card. What are you going to do about this?',
selectedModels: ['jev-latest'],
questions: {
department: {
type: 'choice',
instructions: 'Which team should handle this?',
criteria: {
returns: 'Exchanges, wrong or damaged items',
shipping: 'Delivery status, delays, lost packages',
billing: 'Charges, invoices, payment problems',
},
},
return_reason: {
type: 'choice',
instructions: 'If the customer wants to return something, why?',
criteria: {
wrong_size: "The item doesn't fit",
wrong_item: 'A different product was delivered',
damaged: 'The item arrived broken or faulty',
changed_mind: 'The item is fine, the customer no longer wants it',
other: 'A return reason that fits none of the above',
},
},
shipping_issue: {
type: 'choice',
instructions: 'If this is a shipping problem, which kind is it?',
criteria: {
not_delivered: 'The package never arrived',
delayed: 'The package is late but still on its way',
wrong_address: 'The package went to the wrong place',
damaged_in_transit: 'The package arrived damaged',
other: 'A shipping problem that fits none of the above',
},
},
requested_resolution: {
type: 'choice',
instructions: 'What does the customer want to happen?',
criteria: {
exchange: 'Swap the item for a different one',
refund: 'Money back',
replacement: 'The same item sent again',
information: 'Just an answer, no action needed',
},
},
tone: {
type: 'choice',
instructions: "What is the customer's tone?",
criteria: {
calm: null,
frustrated: null,
angry: null,
},
},
},
}}
/>
Два из этих вопросов Choice являются спекулятивными: return_reason важен только если department — это returns, а shipping_issue важен только если это shipping. Вопрос tone использует значения null в качестве описаний, поскольку имена вариантов понятны сами по себе.
Ответ TypeSafe:
{
"model": "jev-1.13.0",
"answers": {
"department": {
"type": "choice",
"choice": "returns",
"confidence": 0.42,
"probabilities": {
"shipping": 0.04,
"billing": 0.35,
"returns": 0.61
}
},
"return_reason": {
"type": "choice",
"choice": "wrong_size",
"confidence": 1.0,
"probabilities": {
"other": 0.0,
"wrong_size": 1.0,
"changed_mind": 0.0,
"damaged": 0.0,
"wrong_item": 0.0
}
},
"shipping_issue": {
"type": "choice",
"choice": "delayed",
"confidence": 0.67,
"probabilities": {
"wrong_address": 0.0,
"other": 0.26,
"not_delivered": 0.0,
"damaged_in_transit": 0.0,
"delayed": 0.74
}
},
"requested_resolution": {
"type": "choice",
"choice": "refund",
"confidence": 0.2,
"probabilities": {
"replacement": 0.34,
"refund": 0.4,
"information": 0.02,
"exchange": 0.24
}
},
"tone": {
"type": "choice",
"choice": "frustrated",
"confidence": 0.76,
"probabilities": {
"frustrated": 0.84,
"angry": 0.16,
"calm": 0.0
}
}
},
"usage": {
"input_tokens": 589,
"output_tokens": 212
}
}
Каждый вопрос оценивается изолированно относительно исходного тикета:
- Ответ
department—returnsс вероятностью 0.61, ноbillingимеет 0.35 из-за двойного списания. Тикет относится сразу к двум отделам, и невысокая уверенность 0.42 отражает это. - Ответ
return_reason—wrong_sizeс уверенностью 1.0, что вполне ожидаемо, так как это прямо сказано в тикете. - Ответ
shipping_issueразделен междуdelayedиother. Это спекулятивный вопрос, и так какdepartmentне вернулshipping, код может просто проигнорировать этот ответ, как показано в примере кода ниже. - Ответ
requested_resolutionсклоняется кrefundс вероятностью 0.40, аreplacementиexchangeделят большую часть оставшейся доли; уверенность равна 0.20. Двойное списание намекает на возврат денег, неверный размер намекает на обмен, но клиент прямо не написал, чего именно хочет. - Ответ
tone—frustratedс вероятностью 0.84 и уверенностью 0.76.
Приведенный ниже код считывает нужные ответы, игнорирует остальные и воспринимает низкую уверенность как повод уточнить информацию у клиента, а не действовать вслепую:
from typesafe_sdk import Choice, TypeSafeClient
TRIAGE_QUESTIONS = {
"department": Choice(
instructions="Which team should handle this?",
criteria={
"returns": "Exchanges, wrong or damaged items",
"shipping": "Delivery status, delays, lost packages",
"billing": "Charges, invoices, payment problems",
},
),
"return_reason": Choice(
instructions="If the customer wants to return something, why?",
criteria={
"wrong_size": "The item doesn't fit",
"wrong_item": "A different product was delivered",
"damaged": "The item arrived broken or faulty",
"changed_mind": "The item is fine, the customer no longer wants it",
"other": "A return reason that fits none of the above",
},
),
"shipping_issue": Choice(
instructions="If this is a shipping problem, which kind is it?",
criteria={
"not_delivered": "The package never arrived",
"delayed": "The package is late but still on its way",
"wrong_address": "The package went to the wrong place",
"damaged_in_transit": "The package arrived damaged",
"other": "A shipping problem that fits none of the above",
},
),
"requested_resolution": Choice(
instructions="What does the customer want to happen?",
criteria={
"exchange": "Swap the item for a different one",
"refund": "Money back",
"replacement": "The same item sent again",
"information": "Just an answer, no action needed",
},
),
"tone": Choice(
instructions="What is the customer's tone?",
criteria={"calm": None, "frustrated": None, "angry": None},
),
}
def triage(ticket: str) -> None:
with TypeSafeClient() as client:
response = client.system_one(
state=ticket,
questions=TRIAGE_QUESTIONS,
)
answers = response.answers
department = answers["department"]
if department.confidence < 0.3:
# Неясно, в какую команду направить. Передаем человеку для ручной сортировки.
send_to_manual_triage(ticket)
return
if department.choice == "returns":
# Ответ return_reason используется только здесь
assign(ticket, team="returns", issue=answers["return_reason"].choice)
elif department.choice == "shipping":
# Ответ shipping_issue используется только здесь
assign(ticket, team="shipping", issue=answers["shipping_issue"].choice)
else:
assign(ticket, team="billing")
# Вторая команда с заметной долей вероятности получает копию
for team, probability in department.probabilities.items():
if team != department.choice and probability > 0.25:
notify(ticket, team=team)
resolution = answers["requested_resolution"]
if resolution.confidence < 0.5:
# Клиент не указал, чего хочет. Спрашиваем, а не угадываем.
ask_customer_what_they_want(ticket)
elif resolution.choice == "refund":
flag_for_refund_approval(ticket)
if answers["tone"].choice == "angry":
flag_for_senior_agent(ticket)
Для рассматриваемого тикета этот код назначает тикет команде возвратов с проблемой wrong_size, отправляет копию в финансовый отдел (billing), так как доля 0.35 превышает порог 0.25, и запрашивает у клиента желаемое решение, поскольку уверенность 0.20 ниже порога 0.5. Ответ shipping_issue код вообще не использует.
Один запрос, пять ответов, а вся логика маршрутизации состоит из обычных операторов if. Если позже вам понадобится определить язык клиента или продукт, о котором идет речь, просто добавьте еще один вопрос Choice в TRIAGE_QUESTIONS; количество сетевых запросов останется равным единице.
В демо умного домашнего ассистента каждый запрос пользователя оценивается по длинному списку вопросов Choice в одном вызове: категория запроса, комната, устройство и действие. Большинство из этих вопросов не относятся к конкретной команде, и код их игнорирует.
Структурированные инструкции и критерии
Начните с краткого однострочного описания каждого варианта. Если два варианта похожи и модель путает их, опишите каждый объект вместо простой строки. Задайте поля для того, что вариант охватывает, что относится к соседнему варианту, и несколько примеров.
Два варианта ответа ниже, return_policy и return_status, легко спутать. В тикете о любом из них могут упоминаться возвраты и выплаты, поэтому в каждом варианте явно указано, для чего он не предназначен (not_for).
<TypesafeExample
display="request"
example={{
state: 'I sent the shoes back a week ago. When do I get my money?',
selectedModels: ['jev-latest'],
questions: {
return_topic: {
type: 'choice',
instructions: {
question: 'Which returns topic is the customer asking about?',
focus: 'Classify the information the customer wants.',
},
criteria: {
return_policy: {
what: 'Whether and how an item can be returned',
not_for: 'Progress of a return already sent',
examples: [
"Can I return shoes I've worn once?",
'How long do I have to return an order?',
],
},
return_status: {
what: 'Progress of a return already sent',
not_for: 'Whether and how an item can be returned',
examples: [
'Has my return arrived yet?',
'When will my refund be paid?',
],
},
},
},
},
}}
/>
Модель возвращает return_status с уверенностью 1.0:
{
"model": "jev-1.13.0",
"answers": {
"return_topic": {
"type": "choice",
"choice": "return_status",
"confidence": 1.0,
"probabilities": {
"return_policy": 0.0,
"return_status": 1.0
}
}
},
"usage": {
"input_tokens": 407,
"output_tokens": 32
}
}
Имена полей question, focus, what, not_for и examples не являются зафиксированными в API или зарезервированными. Вы выбираете их сами точно так же, как выбираете имена вариантов. Модель видит имена вместе со значениями, поэтому используйте лаконичные понятные названия, точно описывающие содержание.