Noul (Вероятность Да/Нет)
A Noul question asks the TypeSafe model to evaluate a yes/no question and return the probability that the answer is yes.
Вопрос Noul запрашивает у модели TypeSafe оценку вопроса типа «да/нет» и возвращает вероятность того, что ответом является «да».
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}
}
Используйте Noul, когда ответом является «да» или «нет». Например, запрашивает ли это сообщение возврат средств, упоминаются ли в резюме распределенные системы, содержит ли комментарий персональные данные. Если ответ выбирается из нескольких вариантов, используйте Choice. Если это положение на шкале, используйте Score. В разделе Выбор типа вопроса приведено подробное сравнение всех трех типов.
Ответ Noul представляет собой одно число — вероятность того, что ответ утвердительный («да»), где 0 означает «нет», а 1 означает «да».
Структура запроса
Тело POST-запроса к TypeSafe API содержит те же три поля верхнего уровня, что и любой другой тип вопроса: state (оцениваемый контент), model и questions. Каждый вопрос типа Noul имеет следующие поля:
type: Всегда"noul".instructions: Вопрос типа «да/нет», на который отвечает модель, или утверждение для оценки его истинности.criteria: Опционально. Объект с описаниямиtrue(«да») иfalse(«нет») того, что именно означают утвердительный и отрицательный ответы.
Ниже приведен запрос, где состояние — это сообщение в службу поддержки, а два вопроса проверяют, хочет ли клиент связаться с человеком и обращался ли он в поддержку ранее:
<TypesafeExample
display="request"
example={{
state: 'I have asked three times now. Can I please just talk to a real person?',
selectedModels: ['jev-latest'],
questions: {
is_human_escalation: {
type: 'noul',
instructions: 'Is the customer asking for a human agent?',
},
is_repeat_contact: {
type: 'noul',
instructions: 'Has the customer contacted support about this before?',
criteria: {
true: 'Mentions a prior attempt, ticket, or that they have asked before',
false: 'No sign of any previous contact',
},
},
},
}}
/>
Вы сами выбираете идентификаторы вопросов — здесь is_human_escalation и is_repeat_contact. Идентификаторы не отправляются в модель. Каждый ответ возвращается под тем же идентификатором. Первый вопрос опирается исключительно на instructions. Второй вопрос добавляет criteria, чтобы уточнить, что считать за «да», а что — за «нет».
В Python SDK те же вопросы задаются через объекты Noul:
from typesafe_sdk import Noul, NoulCriteria, TypeSafeClient
with TypeSafeClient() as client:
response = client.system_one(
model="jev-latest",
state="I have asked three times now. Can I please just talk to a real person?",
questions={
"is_human_escalation": Noul(
instructions="Is the customer asking for a human agent?",
),
"is_repeat_contact": Noul(
instructions="Has the customer contacted support about this before?",
criteria=NoulCriteria(
true="Mentions a prior attempt, ticket, or that they have asked before",
false="No sign of any previous contact",
),
),
},
)
print(response.answers["is_human_escalation"].noul)
print(response.answers["is_repeat_contact"].noul)
Метод system_one и эндпоинт https://api.typesafe.ai/v1/systemone названы в честь System One, модели искусственного интеллекта TypeSafe. В руководстве Как создавать решения с помощью TypeSafe рассматривается, где использовать ее в коде.
Если вы работаете с агентом разработки, сначала установите скилл TypeSafe для агентов, чтобы он знал форматы запросов и ответов.
Структура ответа
В ответе в словаре answers возвращается по одной записи на каждый вопрос под идентификаторами из запроса:
{
"model": "jev-1.13.0",
"answers": {
"is_human_escalation": {
"type": "noul",
"noul": 0.99
},
"is_repeat_contact": {
"type": "noul",
"noul": 0.93
}
},
"usage": {
"input_tokens": 360,
"output_tokens": 39
}
}
Оба ответа здесь близки к 1. Клиент пишет «поговорить с живым человеком», поэтому is_human_escalation равен 0.99. Фраза «я спросил уже трижды» соответствует критерию true для is_repeat_contact, поэтому значение равно 0.93.
Интерпретация Noul
Возвращаемое число совмещает в себе и сам ответ, и степень определенности модели. Значение, близкое к 1, означает уверенное «да». Значение, близкое к 0, означает уверенное «нет». Значение около 0.5 указывает на то, что модель считает варианты «да» и «нет» равновероятными.
В таблице ниже показаны реальные ответы модели jev-1.13.0 на вопрос is_human_escalation для различных сообщений пользователей:
| Состояние | noul |
|---|---|
| Thanks, that fixed it! | 0.02 |
| How do I reset my password? | 0.07 |
| I need this sorted today, whatever it takes. | 0.26 |
| Are you a bot? | 0.40 |
| Is there any way to speak to someone about my invoice? | 0.84 |
| I have asked three times now. Can I please just talk to a real person? | 0.99 |
Первые два и последние два примера однозначны. Фраза «Мне нужно решить это сегодня» выражает срочность, но не содержит просьбы связать с человеком, получая 0.26. Вопрос «Вы бот?» намекает на желание поговорить с человеком, прямо об этом не заявляя, и модель дает вероятность 0.40. Оба эти случая требуют принятия решения на основе порога в вашем коде.
У Noul нет отдельного значения confidence, в отличие от Choice или Score. Распределение вероятностей для Noul содержит только два исхода («да» и «нет»), поэтому единственное значение noul полностью его описывает. В Choice или Score вероятность распределяется по нескольким вариантам или уровням, и confidence отражает степень выраженности пика в этом распределении.
Чаще всего ваш код сопоставляет noul с пороговым значением для получения булева флага:
wants_human = response.answers["is_human_escalation"].noul > 0.9
if wants_human:
route_to_agent(ticket)
else:
route_to_bot(ticket)
Выбор порогового значения зависит от цены возможной ошибки. Используйте 0.5, когда реакция на «да» и «нет» одинаково безопасна. Повышайте порог, когда ложноположительное срабатывание обходится дорого (например, вызов дежурного инженера или оформление возврата денег). Понижайте его, когда дорого обходится пропуск истинного «да» (например, пропуск инцидента безопасности). Значения в промежуточной зоне можно отправлять на ручную проверку человеку вместо автоматических веток кода. Это то же самое трехстороннее разделение, которое описано на странице Уверенность (Confidence) для Choice и Score.
Значение Noul лежит в диапазоне от 0 до 1, но оно не отражает степень выраженности качества. Это исключительно вероятность того, что ответ утвердительный («да»). Если вопрос на самом деле касается степени проявления свойства, значение не измеряет эту степень. Ниже вопрос «Силен ли кандидат в Python?» задан относительно четырех кандидатов параллельно с оценкой Score с четырьмя уровнями: без опыта, базовое знакомство, регулярное использование на работе, глубокая экспертиза.
| Кандидат | Noul: «Силен ли кандидат в Python?» | Score: «Сколько опыта работы с Python у кандидата?» |
|---|---|---|
| My experience is in Java and Go. I have not used Python. | 0.03 | 0.0 (Нет опыта) |
| I have used Python occasionally for small scripts alongside my main Java work. | 0.14 | 1.0 (Базовое знакомство) |
| I used Python every day for two years in my last job, mostly data pipelines. | 0.81 | 2.05 (Регулярное использование на работе) |
| I have written Python daily for eight years, including maintaining a large Django codebase. | 0.92 | 2.89 (Глубокая экспертиза) |
Вопрос Noul оценивает одно утверждение — «силен», и полученные числа показывают вероятность истинности этого факта. Вы могли бы искусственно разбить диапазон от 0 до 1 в своем коде (например, считать от 0.3 до 0.7 «средним опытом»), однако модель этого контекста не видит, и ничего в ответе не оценивалось относительно таких границ. Среднее значение может означать как средний опыт, так и неясность формулировок в резюме, а расстояние между кандидатами не является контролируемым параметром. Score же оценивает каждое описание уровня по отдельности, поэтому каждый кандидат позиционируется относительно сформулированных вами уровней, а вероятности показывают разделение мнений модели. Если вы не согласны с результатом, достаточно скорректировать описание уровня. Различие подробно описано в разделе Выбор типа вопроса.
Формулирование вопросов Noul
Задавайте по одному вопросу «да/нет» на каждый Noul. Если вопрос содержит два условия (например, «Раздражен ли клиент и просит ли он возврат денег?»), модели приходится оценивать оба фактора одновременно, и ценность полученного числа снижается. Задайте два вопроса Noul и объедините их в коде.
Формулируйте вопрос так, чтобы высокое значение означало «да». Вопрос «Содержит ли сообщение персональные данные?» понятен и однозначен. Вопрос «Свободно ли сообщение от персональных данных?» инвертирует смысл, и код, считывающий ответ, легко запутать.
Утверждение работает так же эффективно, как и прямой вопрос. Для утверждения «Клиент запрашивает возврат средств» значение близкое к 1 означает истинность утверждения. Протестируйте обе формулировки на своих данных, чтобы выбрать наиболее эффективную.
Делайте границу между «да» и «нет» однозначной. Вопрос «Имеет ли кандидат какой-либо опыт работы с Python?» работает надежно, так как слово «какой-либо» исключает двусмысленность. Когда граница тонка, добавьте criteria с описаниями true и false, как это сделано для is_repeat_contact выше. Для большинства вопросов Noul достаточно только instructions, поэтому протестируйте вопросы с criteria и без них и выберите вариант с лучшими результатами на ваших документах.
Хорошая практика: задавайте более одного вопроса за вызов
Для проверки чек-листа условий задавайте множество вопросов Noul в одном запросе: по одному вопросу на каждое условие, а логику их комбинации оставляйте коду. Вопросы оцениваются параллельно, поэтому добавление вопросов Noul практически не меняет время отклика. Подробнее см. в разделе Задавайте несколько вопросов одновременно.
Обработка нескольких ответов Noul в коде
Запрос из двух вопросов выше дает коду достаточный объем информации для маршрутизации сообщения. В примере ниже кейс передается человеку, если клиент просит об этом, а приоритет повышается, если клиент обращался ранее. Промежуточные значения по любому из вопросов направляются оператору на ручную проверку:
from typesafe_sdk import Noul, NoulCriteria, TypeSafeClient
SUPPORT_QUESTIONS = {
"is_human_escalation": Noul(
instructions="Is the customer asking for a human agent?",
),
"is_repeat_contact": Noul(
instructions="Has the customer contacted support about this before?",
criteria=NoulCriteria(
true="Mentions a prior attempt, ticket, or that they have asked before",
false="No sign of any previous contact",
),
),
}
YES = 0.8
NO = 0.2
def route(message: str) -> None:
with TypeSafeClient() as client:
response = client.system_one(
model="jev-latest",
state=message,
questions=SUPPORT_QUESTIONS,
)
answers = response.answers
wants_human = answers["is_human_escalation"].noul
repeat = answers["is_repeat_contact"].noul
if NO < wants_human < YES or NO < repeat < YES:
# Модель не уверена ни в одном из вариантов. Пусть решает человек.
send_to_review(message)
return
priority = "high" if repeat > YES else "normal"
if wants_human > YES:
route_to_agent(message, priority=priority)
else:
route_to_bot(message, priority=priority)
Для рассмотренного выше сообщения значение noul для is_human_escalation составляет 0.99, а для is_repeat_contact — 0.93, поэтому код направляет его оператору с высоким приоритетом. Сообщение «Как сбросить пароль?» получает 0.07 по обоим вопросам и направляется чат-боту.
Пороговые значения задаются прямо в вашем коде. Если операторы получают слишком много обращений, сузьте зазор между NO и YES. Если в систему просачиваются ошибочные маршруты, расширьте его. Если позже вам понадобится узнать, упоминается ли в сообщении оплата или содержит ли оно персональные данные, просто добавьте еще один Noul в SUPPORT_QUESTIONS. Количество сетевых запросов останется равным единице.
Структурированные инструкции
Инструкции могут быть объектом вместо строки: в одном поле находится сам вопрос, а в других — вспомогательные данные. В разделе Используйте структуру в вопросах описано, когда это целесообразно. Здесь этот подход используется для вопроса, генерируемого в коде: поступившее резюме сравнивается с записями в базе данных кандидатов, которые могут принадлежать тому же человеку. Каждая запись передается в поле potential_duplicate как есть, формулировка question одинакова для всех записей, и все они проверяются за один запрос. Сгенерированные кодом ключи вопросов содержат идентификаторы записей из базы данных:
<TypesafeExample
display="request"
example={{
state: {
resume: {
name: 'John Smith',
location: 'Oakland, CA',
summary: 'Backend engineer with eight years of Python and Go experience.',
experience: [
{ employer: 'Google', title: 'Senior Backend Engineer', years: '2021-2025' },
{ employer: 'Microsoft', title: 'Software Engineer', years: '2017-2021' },
],
},
},
selectedModels: ['jev-latest'],
questions: {
same_as_record_18: {
type: 'noul',
instructions: {
potential_duplicate: { name: 'Jon Smith', location: 'Oakland, CA', last_employer: 'Google' },
question: 'Is the resume for the same person as potential_duplicate?',
},
},
same_as_record_42: {
type: 'noul',
instructions: {
potential_duplicate: { name: 'John Smith', location: 'Austin, TX', last_employer: 'Lone Star Freight' },
question: 'Is the resume for the same person as potential_duplicate?',
},
},
same_as_record_77: {
type: 'noul',
instructions: {
potential_duplicate: { name: 'John Smithers', location: 'Oakland, CA', last_employer: 'Bay Health Clinic' },
question: 'Is the resume for the same person as potential_duplicate?',
},
},
},
}}
/>
Ответ:
{
"model": "jev-1.13.0",
"answers": {
"same_as_record_18": {
"type": "noul",
"noul": 0.74
},
"same_as_record_42": {
"type": "noul",
"noul": 0.09
},
"same_as_record_77": {
"type": "noul",
"noul": 0.08
}
},
"usage": {
"input_tokens": 535,
"output_tokens": 58
}
}
Каждый ответ представляет собой вероятность того, что резюме принадлежит человеку из соответствующей записи. Запись 18 содержит опечатку в имени, но совпадает по локации и работодателю, получая 0.74. В записи 42 то же имя, но другой город и работодатель — оценка 0.09. В записи 77 похожее имя в том же городе с другим работодателем — оценка 0.08. Примените пороговые фильтры в коде (как в разделе Обработка нескольких ответов Noul в коде) и отправьте промежуточные спорные значения оператору.
В Python SDK такие вопросы легко генерируются из списка кандидатов. Текст вопроса зафиксирован, а данные записей подставляются программно:
from typesafe_sdk import Noul, TypeSafeClient
SAME_PERSON = "Is the resume for the same person as `potential_duplicate`?"
def duplicate_questions(candidates: list[dict]) -> dict[str, Noul]:
"""По одному вопросу Noul на каждую запись кандидата, с одинаковым вопросом для всех."""
return {
f"same_as_record_{candidate['id']}": Noul(
instructions={
"potential_duplicate": {
"name": candidate["name"],
"location": candidate["location"],
"last_employer": candidate["last_employer"],
},
"question": SAME_PERSON,
},
)
for candidate in candidates
}
def find_duplicates(resume: dict, candidates: list[dict]) -> list[str]:
with TypeSafeClient() as client:
response = client.system_one(
model="jev-latest",
state={"resume": resume},
questions=duplicate_questions(candidates),
)
return [
question_id
for question_id, answer in response.answers.items()
if answer.noul > 0.7
]
В кукбуке по каскадному извлечению структурированных данных структурированные инструкции используются для верификации извлеченной записи. К каждому полю применяется один и тот же набор вопросов. В объекте instructions каждого вопроса основной текст находится в свойстве main_question, а свойства field_spec и extracted_field меняются для каждого конкретного поля.
Noul в кукбуках
Ознакомьтесь с нашими кукбуками, чтобы увидеть применение вопросов Noul в реальных приложениях:
- Параллельные вопросы — проверка регуляторного чек-листа из 13 вопросов к одной статье в рамках одного запроса.
- Самосогласованность: Noul — оценка страхового случая по шкале из 15 вопросов с измерением стабильности значений между прогонами.
- Переранжирование (Re-ranking) — прямое использование вероятности без порогов: один Noul на каждую пару запрос-кандидат с последующей сортировкой кандидатов по полученному значению.
- Построчный поиск — связка Choice, находящего подходящую строку, и Noul, проверяющего, содержит ли документ ответ в принципе.
- Восстановление структуры — один Noul на каждую пару строк для проверки того, разрывает ли перенос строки предложение, для восстановления абзацев из неструктурированного текста.