Документация

Noul

Вопрос Noul просит модель TypeSafe оценить вопрос с ответом «да» или «нет» и вернуть вероятность того, что ответ — «да».

Используйте Noul, когда ответ — «да» или «нет». Например, просит ли это сообщение возврат денег, упоминает ли это резюме распределённые системы, содержит ли этот комментарий персональные данные. Если ответ — один из нескольких вариантов, используйте Choice. Если ответ — позиция на шкале, используйте Score. Как выбрать тип вопроса сравнивает все три.

Ответ Noul — одно число, представляющее вероятность того, что ответ «да», где 0 означает «нет», а 1 означает «да».

Структура запроса

Тело POST-запроса к API TypeSafe имеет те же три поля верхнего уровня, что и любой другой тип вопроса: state — содержимое для оценки; model; и questions. Каждый вопрос Noul имеет следующие поля:

  • type: Всегда "noul".
  • instructions: Вопрос «да/нет», на который отвечает модель, или утверждение, которое она оценивает.
  • criteria: Необязательно. Объект с описаниями true и false того, что означают «да» и «нет».

Ниже запрос, где состояние — сообщение в поддержку, а два вопроса — хочет ли клиент человека и обращался ли он в поддержку раньше:

request
{
  "state": "I have asked three times now. Can I please just talk to a real person?",
  "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"
      }
    }
  }
}

Id вопросов выбираете вы, здесь это is_human_escalation и is_repeat_contact. Id не отправляются модели. Каждый ответ возвращается под тем же id. Первый вопрос опирается только на 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 и endpoint https://api.typesafe.ai/v1/systemone названы в честь System One, AI-модели TypeSafe. Как создавать с TypeSafe рассказывает, где её использовать в вашем коде.

Если вы используете агента программирования, сначала установите навык агента TypeSafe, чтобы он знал формы запроса и ответа.

Структура ответа

В ответе на каждый вопрос приходится одна запись в answers, под id из запроса:

{
  "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. Фраза «I have asked three times now» совпадает с описанием true у is_repeat_contact, поэтому он равен 0.93.

Чтение Noul

Число — это и ответ, и уверенность в одном. Значение около 1 — уверенное «да». Значение около 0 — уверенное «нет». Значение около 0.5 означает, что модель даёт «да» и «нет» похожую вероятность.

Таблица ниже показывает записанные ответы jev-1.13.0 на вопрос is_human_escalation для разных сообщений клиентов:

Состояние noul
Спасибо, это помогло! 0.02
Как сбросить пароль? 0.07
Мне нужно решить это сегодня, чего бы это ни стоило. 0.26
Вы бот? 0.40
Есть ли способ поговорить с кем-нибудь о моём счёте? 0.84
Я уже спрашивал три раза. Можно мне, пожалуйста, поговорить с живым человеком? 0.99

Первые две и последние две строки однозначны. Фраза «I need this sorted today» срочная, но человека в ней не просят, и она получает 0.26. Фраза «Are you a bot?» намекает на желание человека, не прося о нём, и модель делит почти поровну — 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, когда действовать по «да» и по «нет» одинаково просто. Повышайте его, когда действовать по ложному «да» дорого, например поднимать дежурного или выдавать возврат. Понижайте его, когда пропустить истинное «да» дорого, например не отметить проблему безопасности. Значения посередине можно передать человеку, а не направлять по одной из ветвей кода. Это то же разветвление на три пути, которое страница Уверенность описывает для ответов Choice и Score.

Значение Noul лежит от 0 до 1, но это не шкала того, о чём вы спросили. Это вероятность того, что ответ «да». Если вопрос на самом деле о степени, значение не измеряет степень. Ниже вопрос «Силён ли кандидат в Python?» задан про четырёх кандидатов, рядом со Score с четырьмя уровнями: нет опыта, некоторое знакомство, регулярное использование на работе, глубокий опыт.

Кандидат Noul: «Силён ли кандидат в Python?» Score: «Сколько у кандидата опыта Python?»
Мой опыт — в Java и Go. Python я не использовал. 0.03 0.0 (Нет опыта)
Я изредка использовал Python для небольших скриптов параллельно с основной работой на Java. 0.14 1.0 (Некоторое знакомство)
На прошлой работе я два года использовал Python каждый день, в основном для конвейеров данных. 0.81 2.05 (Регулярное использование на работе)
Восемь лет я писал на Python ежедневно, в том числе поддерживал большую кодовую базу на Django. 0.92 2.89 (Глубокий опыт)

Noul судит одно утверждение — «силён», и значения показывают, насколько оно вероятно. Вы могли бы создать уровни в диапазоне от 0 до 1 в своём коде, например от 0.3 до 0.7 для «некоторого опыта», но модель их не увидит, поэтому в ответе ничего не оценивалось против них. Промежуточное значение может означать средний опыт или неясный случай, и расстояние между кандидатами — не то, что выбрали вы. Score судит каждое описание уровня отдельно, поэтому каждый кандидат попал на написанный вами уровень или рядом с ним, и возвращённые вероятности показывают, как модель разделила своё суждение между уровнями. Если вы не согласны, переформулируйте уровень и запустите снова. Как выбрать тип вопроса объясняет различие.

Написание вопроса Noul

Задавайте по одному вопросу «да/нет» на каждый Noul. Если в вопросе два условия, как «Клиент злится и просит возврат?», модель должна оценить оба сразу, и значение значит меньше. Задайте два Noul и объедините их в коде.

Формулируйте вопрос так, чтобы высокое значение означало «да». «Содержит ли сообщение персональные данные?» — ясно. «Свободно ли сообщение от персональных данных?» переворачивает смысл, и код, который прочитает его позже, поймёт всё наоборот.

Утверждение работает так же хорошо, как вопрос. Для «Клиент просит возврат» значение около 1 означает, что утверждение истинно. Попробуйте обе формулировки на своих данных и посмотрите, какая работает лучше.

Сделайте границу между «да» и «нет» однозначной. «Есть ли у этого кандидата хоть какой-то опыт Python?» работает хорошо, потому что «хоть какой-то» не оставляет середины. Когда граница тонкая, добавьте criteria с описаниями true и false, как это делает вопрос is_repeat_contact выше. Для большинства Noul инструкции достаточно, поэтому попробуйте свои вопросы с 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:
        # The model isn't sure either way. Let a person decide.
        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, поэтому код направляет его агенту с высоким приоритетом. Сообщение «How do I reset my password?» даёт 0.07 по обоим вопросам и направляется боту.

Пороги живут в вашем коде. Если рецензенты видят слишком много сообщений, сузьте разрыв между NO и YES. Если проскакивает слишком много неверных маршрутов, расширьте его. Если позже понадобится узнать, упоминает ли сообщение платёж или содержит ли оно персональные данные, добавьте ещё один Noul в SUPPORT_QUESTIONS. Число запросов останется прежним — один.

Структурированные инструкции

Инструкции могут быть объектом вместо строки: вопрос в одном поле, вспомогательные данные в остальных. Использование структуры в вопросах рассказывает, когда это помогает. Здесь это применяется к вопросу, построенному кодом: только что пришедшее резюме сравнивается с записями в базе кандидатов, которые могут быть тем же человеком. Каждая запись как есть попадает в поле potential_duplicate, поле question одинаково для всех записей, и все записи проверяются в одном запросе. Ключи вопросов, сгенерированные кодом, содержат ID каждой записи в базе:

request
{
  "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"
        }
      ]
    }
  },
  "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]:
    """One Noul per candidate record, all asking the same question."""
    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
    ]

Cookbook по каскаду извлечения структурированных данных использует структурированные инструкции для проверки извлечённой записи. Каждое поле получает один и тот же набор вопросов. Объект instructions каждого вопроса содержит текст вопроса в свойстве main_question. Есть также свойства field_spec и extracted_field, которые меняются для каждого поля.

Noul в cookbook

Посмотрите наши cookbook, чтобы увидеть приложения, использующие вопросы Noul:

  • Параллельные вопросы прогоняет контрольный список из 13 вопросов по нормативным требованиям над одной статьёй в одном запросе.
  • Самосогласованность: noul оценивает страховое требование по рубрике из 15 вопросов и измеряет, насколько стабильны значения между запусками.
  • Переранжирование использует саму вероятность, а не порог: один Noul на пару запрос-кандидат, затем кандидаты сортируются по значению.
  • Построчный поиск объединяет Choice, который находит совпадающую строку, с Noul, который проверяет, есть ли в документе ответ вообще.
  • Восстановление структуры задаёт по одному Noul на пару строк — разделил ли перенос строки предложение, — чтобы собрать абзацы из простого текста.