Примитивы (Вопросы)
The three TypeSafe question types (Choice, Score, Noul), the typed answers they return, how to choose between them, and how to ask several at once.
Три типа вопросов TypeSafe (Choice, Score, Noul), типизированные ответы, которые они возвращают, как выбирать между ними и как задавать несколько вопросов одновременно.
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.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 — это компактные, типизированные строительные блоки, которые вы компонуете в коде. Они работают парами: вопрос (question) определяет одно конкретное суждение, которое модель System One должна вынести относительно состояния (state), а ответ — это возвращаемое типизированное значение. Вы объединяете полученные ответы в своем коде для принятия решений. Существует три типа вопросов, каждый из которых возвращает ответ определенной структуры.
| Тип | На что отвечает | Возвращаемые данные |
|---|---|---|
| Choice | Какой из этих вариантов? | choice, probabilities, confidence |
| Score | Какой уровень по шкале? | score, legend, probabilities, confidence |
| Noul | Верно ли это утверждение? | noul (от 0 до 1) |
Вы можете задать один вопрос или отправить сразу несколько вопросов в одном запросе. Каждый вопрос в запросе видит одно и то же состояние, оценивается независимо и возвращает типизированный ответ под выбранным вами идентификатором (ID).
Задавайте по одной быстрой оценке на вопрос
Модели System One созданы для быстрых, сфокусированных суждений. Формулируйте вопрос так, чтобы компетентный специалист мог дать на него ответ за секунду при наличии нужного контекста. «Передает ли это сообщение ощущение срочности?» — отличный вопрос. «Проанализируй это сообщение и определи оптимальный план действий» — плохой вопрос. Для такого вывода требуется медленное рассуждение, и это явный признак того, что задачу необходимо разбить на небольшие вопросы и скомпоновать ответы в коде.
Если требуемое суждение зависит от нескольких независимых факторов, задайте отдельный вопрос по каждому фактору и объедините ответы с помощью собственной логики. Вместо «оцени этот питч стартапа» спросите об объеме рынка, технической реализуемости и дифференциации, а затем взвесьте их в коде в соответствии с их важностью. При смене приоритетов вам достаточно изменить весовые коэффициенты в коде, а не переписывать весь промпт. В разделе Задавайте несколько вопросов одновременно показано, как это реализовать.
Определение вопроса
У каждого вопроса есть ID, type и instructions. Вопросы Choice и Score также принимают criteria, которые определяют варианты для Choice или уровни шкалы для Score. Вопросы Noul принимают criteria как опциональное уточнение того, что означают «да» и «нет».
- ID. Ключ, который вы задаете сами, например
refund_requested. Он идентифицирует ответ в возвращаемой структуре. type. Одно из значений:choice,scoreилиnoul.instructions. Вопрос, который вы задаете относительно состояния. Здесь находится логика оценки. Формулируйте его как четкий конкретный вопрос или утверждение для оценки моделью. Для большинства вопросов достаточно обычной строки. Это также может быть объект или массив, где сам вопрос находится в одном поле, а данные, на которые он опирается, — в других; см. Используйте структуру в вопросах.criteria. Пространство возможных ответов: словарь вариантов для Choice, упорядоченный список уровней для Score и опциональное описание утвердительного и отрицательного ответов для Noul. Структура критериев подробно описана на странице каждого примитива.
Этот вопрос проверяет, запросил ли клиент возврат средств:
from typesafe_sdk import Noul
questions = {
"refund_requested": Noul(
instructions="Does the customer request a refund?",
),
}
Выбор типа вопроса
Выбирайте тип, соответствующий требуемой структуре ответа.
Choice подходит, когда ответ представляет собой один из известного набора неупорядоченных вариантов: маршрутизация тикета в отдел, классификация типа документа, определение языка программирования. Укажите полный список вариантов и добавьте вариант
other(другое) илиnone of the above(ни один из перечисленных), если список может охватывать не все возможные входные данные.Score подходит, когда ответ располагается на непрерывной шкале или спектре, и вы можете описать значение каждой градации: критичность бага, степень раздражения клиента, уровень владения навыком. Вы сами определяете уровни, а модель возвращает положение на этой шкале.
Noul идеально подходит для четких вопросов «да/нет», где полезным сигналом является сама вероятность: содержит ли сообщение персональные данные, запрашивает ли клиент возврат, упоминаются ли в резюме распределенные системы.
Если вам требуется оценить уровень навыка, используйте Score с заданными градациями, такими как «нет опыта», «базовое знакомство», «ежедневное использование» и «глубокая экспертиза». Если нужно бинарное решение «да/нет», четко сформулируйте условие, например: «Указано ли в резюме, что кандидат использовал Python в коммерческой разработке?»
Если кажется, что подходят оба типа, отдайте предпочтение тому, чей ответ ваш код может использовать наиболее прямо. Выбор Choice между refund, rebook и information напрямую проецируется на три ветки выполнения в коде. Оценка Score степени раздражения клиента проецируется на числовой порог. Noul проецируется на простое условие if.
Что возвращает модель
Ответы также являются примитивами. Каждый тип вопроса возвращает типизированное значение, которое ваш код может сравнивать, сопоставлять с пороговыми значениями, сортировать, передавать в дальнейшую логику или помещать в состояние последующего запроса (см. Когда один вопрос зависит от другого).
| Тип | Поля ответа | Как интерпретировать |
|---|---|---|
| Choice | choice, probabilities, confidence |
choice — выбранный вариант. probabilities — распределение вероятностей по всем вариантам. confidence отражает степень выраженности пика в этом распределении. |
| Score | score, legend, probabilities, confidence |
score — числовое значение на шкале уровней (может находиться между двумя уровнями). legend воспроизводит описания уровней по номерам. probabilities — распределение по уровням. |
| Noul | noul |
Вероятность истинности утверждения («да»). Значение близкое к 1 — уверенное «да», близкое к 0 — уверенное «нет», около 0.5 — неопределенность. У Noul нет отдельного поля confidence. |
Два фундаментальных свойства делают эти ответы компонуемыми:
- Каждый ответ строго ограничен предоставленными вариантами. Модель возвращает распределение вероятностей по вашим вариантам или уровням и никогда не выходит за их пределы. Вашему коду никогда не придется вычленять значение из сгенерированного текста.
- Каждый ответ независим. Ответ на один вопрос не становится скрытым контекстом для другого. Вы можете добавлять или удалять вопросы, не меняя результатов остальных.
В разделе Уверенность (Confidence) объясняется, как confidence вычисляется из probabilities и как использовать этот показатель для разграничения автоматических действий и эскалации человеку.
Обращение к конкретным полям
Оцениваемый контент — состояние (state) — часто представляет собой JSON-объект, состоящий из нескольких частей: переписки, записей базы данных, регламентов. Когда вопрос касается конкретной части, укажите ее в instructions с помощью пути через точку и индексы, заключенного в обратные кавычки. Модель точно поймет, какую часть состояния необходимо оценить.
Взгляните на диалог со службой поддержки из раздела State:
{
"ticket": {
"subject": "Duplicate charge",
"messages": [
{"from": "customer", "text": "I was charged twice for order A-104. Please refund the duplicate."},
{"from": "support", "text": "We are checking the charges."}
]
},
"order": {
"id": "A-104",
"charges": [
{"amount_usd": 49, "status": "captured"},
{"amount_usd": 49, "status": "captured"}
]
},
"refund_policy": "Duplicate charges are eligible for a refund."
}
Эти два вопроса обращаются к сообщению клиента, регламенту и списаниям по путям:
questions = {
"refund_requested": {
"type": "noul",
"instructions": "Does `ticket.messages[0].text` request a refund?",
},
"policy_supports_refund": {
"type": "noul",
"instructions": (
"Does `refund_policy` support the refund requested "
"in `ticket.messages[0].text`, given `order.charges`?"
),
},
}
Явные пути однозначно определяют, какие именно части структурированного состояния должны учитываться при вынесении суждения. Структурирование входных данных описано в разделе Состояние (State).
Задавайте несколько вопросов одновременно
Отправляйте все вопросы, относящиеся к одному состоянию, в едином запросе. Вы можете свободно комбинировать различные типы вопросов. Модели System One оценивают все вопросы в запросе параллельно. Добавление новых вопросов практически не меняет время ответа и увеличивает стоимость лишь на токены самих вопросов, которые крайне дешевы. Задать вопрос, который может и не понадобиться, практически ничего не стоит.
Этот запрос одновременно классифицирует сообщение клиента, проверяет его срочность и оценивает степень раздражения:
<TypesafeExample
example={{
state:
"Our API integration started returning 500 errors on every request about 20 minutes ago, and we can't process any customer orders until this is fixed.",
questions: {
department: {
type: 'choice',
instructions: 'Which team should handle this',
criteria: {
billing: 'Payment or subscription issues',
technical: 'Bugs or integration problems',
sales: 'Pricing or account questions',
},
},
is_urgent: {
type: 'noul',
instructions: 'The message conveys urgency or time-sensitivity',
},
frustration: {
type: 'score',
instructions: 'How frustrated the customer appears',
criteria: [
'Calm, just stating facts',
'Frustrated but civil',
'Very angry, strong language',
],
},
},
}}
/>
Наши клиентские SDK предоставляют типизированные вопросы и ответы. В Python передайте словарь questions с объектами Choice, Noul и Score в вызов client.system_one(...). Этот запрос передает тикет и правила возврата один раз и получает типизированный ответ на каждый вопрос:
from typesafe_sdk import Choice, Noul, Score, TypeSafeClient
state = {
"ticket_message": "My flight was cancelled. Can I get a refund?",
"refund_policy": "Cancelled flights are eligible for a full refund.",
}
with TypeSafeClient() as client:
response = client.system_one(
state=state,
questions={
"refund_requested": Noul(
instructions="Does `ticket_message` request a refund?",
),
"request_type": Choice(
instructions="What is the main request in `ticket_message`?",
criteria={
"refund": "The customer wants money returned.",
"rebooking": "The customer wants a replacement flight.",
"information": "The customer is asking for information only.",
},
),
"frustration": Score(
instructions="How frustrated does the customer appear in `ticket_message`?",
criteria=[
"Calm and neutral.",
"Concerned but civil.",
"Very angry or using strong language.",
],
),
},
)
print(response.answers["refund_requested"].noul)
print(response.answers["request_type"].choice)
print(response.answers["frustration"].score)
Варианты установки и примеры для вашего языка программирования см. в разделе клиентские SDK.
Задавайте спекулятивные вопросы
Задавайте любые вопросы, которые могут потребоваться вашему коду, включая те, ответы на которые понадобятся лишь при определенных условиях, и позвольте коду самому решать, какие ответы использовать. Если обращение оказалось не отчетом о баге, просто проигнорируйте ответ о критичности ошибки. Мы называем это паттерном Спекулятивное ветвление (Speculative fan-out). В кукбуке по параллельным вопросам показано, как объединение 13 вопросов в один вызов обходится в 11.5 раз дешевле и выполняется в 9.6 раз быстрее, чем 13 отдельных вызовов, возвращая абсолютно идентичные ответы.
Разделение сложного суждения на несколько вопросов
Суждение, зависящее от нескольких факторов, лучше всего разделить на отдельные вопросы по каждому фактору. Объедините ответы в коде, присвоив каждому вес в соответствии с его относительной значимостью. Веса определяете вы сами. Если совокупный результат расходится с решением вашей команды, просто скорректируйте веса в коде и выполните проверку снова. Добавление вопросов практически не влияет на задержку, так как они выполняются параллельно внутри одного запроса. Разделение стоит лишь нескольких дополнительных токенов вопроса.
Например, приоритет тикета может складываться из трех вопросов типа Score: насколько критичен баг, насколько раздражен клиент и насколько подробно отчет описывает проблему для инженера. На странице Score этот запрос и код нормализации и взвешивания ответов подробно разобраны в разделе Разделение сложного суждения на несколько оценок Score. Этот подход называется паттерном Композитная оценка (Composite scoring).
Когда один вопрос зависит от другого
Вопросы в одном запросе независимы: один ответ не становится контекстом для другого вопроса. Если последующее суждение зависит от предыдущего ответа, выполните второй запрос в коде. Реальная зависимость возникает только тогда, когда ваш код физически не может сформировать второй запрос без получения первого ответа: ответ требуется для подгрузки дополнительных данных в состояние, определения структуры состояния или выбора вариантов следующего вопроса. В остальных случаях задавайте вопросы вместе и объединяйте их ответы в коде.
Разделение на два запроса — это исключение, а не правило. Если вопросы второго запроса можно было сформулировать относительно исходного состояния, задайте их в первом запросе и позвольте коду проигнорировать ненужные. В трех наших кукбуках второй запрос делается по обоснованной причине: в предложении навыков (Skill suggestion) в первом запросе ранжируются 182 навыка, а затем для топ-3 запрашивается полный текст и проводится повторная оценка; в восстановлении структуры (Structure recovery) сначала проверяется, разрывает ли перенос строки предложение, строки объединяются в блоки, а затем эти блоки классифицируются; в иерархической классификации (Hierarchical classification) каждый ответ Choice определяет, какие варианты будут предложены в следующем запросе.
Рекомендации по декомпозиции рабочего процесса на сфокусированные суждения см. в руководстве Как создавать решения с помощью TypeSafe.
Следующие шаги
Чтобы увидеть, как эти элементы объединяются в системные архитектуры, перейдите в раздел Паттерны.