Продвинутые вопросы в Jev: что умеют примитивы кроме базового

Обновлено

Базовый вопрос к Jev — строка с формулировкой и список вариантов. Но instructions и criteria принимают и JSON, а вариант Choice может нести целое поддерево категорий. Разбираем приёмы, которые выручают, когда простых формулировок уже не хватает.

Где разрешена структура

По документации TypeSafe, JSON принимают четыре места в вопросах:

Поле Тип вопроса Что принимает
instructions Choice, Score, Noul строка, объект, массив или null
значения criteria — описания вариантов Choice строка, объект, массив или null
элементы criteria — описания уровней Score строка, объект, массив или null
criteria.true и criteria.false Noul строка, объект, массив или null

Структура помогает в двух случаях. Первый — вопрос из нескольких частей: подписанные ключи делают его понятнее сплошного текста. Второй — вопросу нужны данные, которые уже лежат в JSON: схема, таксономия, строка из базы. Передайте их как есть или нужными полями, а не склеивайте в строковый шаблон.

Имена полей внутри объектов — question, focus, what, not_for — не часть API и не зарезервированы. Вы выбираете их сами, как ключи вариантов. Модель видит имена вместе со значениями, поэтому делайте их короткими и говорящими, а у соседних вариантов используйте одинаковые поля: так модели проще сравнивать варианты. Короткий однозначный вопрос оставляйте строкой.

Вопрос плюс данные

Типичный случай — проверка извлечённых данных. Описание поля лежит в объекте field, и на него по имени ссылаются вопросы разных типов:

{
  "model": "jev-latest",
  "state": {
    "source_text": "Счёт № 4471 от 3 марта 2026 года выставлен ООО «Пример Логистик» на 12 840 USD, оплата в течение 30 дней."
  },
  "questions": {
    "invoice_number_ok": {
      "type": "noul",
      "instructions": {
        "field": { "name": "invoice_number", "type": "string", "description": "Номер, напечатанный в счёте" },
        "extracted_value": "4471",
        "question": "Совпадает ли `extracted_value` с полем `field` так, как оно указано в `source_text`?"
      }
    },
    "customer_name": {
      "type": "choice",
      "instructions": {
        "field": { "name": "customer_name", "description": "Организация, которой выставлен счёт" },
        "question": "Какой вариант — значение поля `field` в `source_text`?"
      },
      "criteria": { "ООО «Пример Логистик»": null, "Пример Логистик": null, "ООО «Пример»": null }
    },
    "amount_due": {
      "type": "score",
      "instructions": {
        "field": { "name": "amount_due", "unit": "USD", "description": "Сумма к оплате" },
        "question": "Насколько велико значение поля `field` в `source_text`?"
      },
      "criteria": ["До 1000", "От 1000 до 10 000", "От 10 000 до 100 000", "Больше 100 000"]
    }
  }
}

В коде такие вопросы удобно строить циклом по полям записи и отправлять одним запросом — TypeSafe так же поступает в кукбуке о каскадном извлечении данных. Массив тоже подойдёт, если инструкция — это список того, что нужно проверить или сравнить: "compare": ["ticket.sender.display_name", "ticket.sender.email"].

Второй частый случай — сравнение с записями из базы. В документации резюме сверяется с тремя возможными дубликатами: формулировка вопроса одна и та же, меняется только запись в поле potential_duplicate. Запись с другим написанием имени, но тем же городом и работодателем получила 0,74, полный тёзка из другого города с другим работодателем — 0,09. Данные из кода могут меняться, а вопрос остаётся прежним и ссылается на них по имени.

Контрастные описания вариантов

Если два варианта Choice похожи, короткие строки-описания не помогают: модель колеблется между ними. Опишите каждый вариант объектом — что он покрывает, что к нему не относится, примеры:

"return_topic": {
  "type": "choice",
  "instructions": {
    "question": "О какой теме возврата спрашивает клиент?",
    "focus": "Определи, какую информацию хочет получить клиент."
  },
  "criteria": {
    "return_policy": {
      "what": "Можно ли вернуть товар и как это сделать",
      "not_for": "Ход уже отправленного возврата",
      "examples": ["Можно вернуть обувь, если я её один раз надел?", "Сколько дней есть на возврат?"]
    },
    "return_status": {
      "what": "Ход уже отправленного возврата",
      "not_for": "Можно ли вернуть товар и как это сделать",
      "examples": ["Мой возврат уже дошёл?", "Когда вернут деньги?"]
    }
  }
}

С такими описаниями сообщение «Отправил обувь неделю назад, когда вернут деньги?» в примере TypeSafe (в оригинале — на английском) получило return_status с уверенностью 1,0. Поле not_for работает как граница: каждое описание прямо говорит, что относится к соседу.

Обход таксономии

API принимает до 255 вариантов в одном Choice, а в одном из кукбуков TypeSafe уточняет, что надёжно Choice работает примерно до 240. Для глубоких каталогов удобнее идти по дереву: один Choice на уровень и спуск к выбранной ветке в коде.

Главный приём — передавать значением варианта поддерево. Так модель видит, что лежит внутри ветки, прежде чем её выбрать:

"department": {
  "type": "choice",
  "instructions": "К какому отделу магазина относится товар?",
  "criteria": {
    "Спорт": {
      "Велоспорт": ["Велофляги и флягодержатели", "Велофонари", "Шлемы"],
      "Туризм": ["Палатки", "Спальники", "Питьевые системы"]
    },
    "Дом и кухня": { "Посуда для напитков": ["Бутылки для воды", "Термокружки"] },
    "Детские товары": ["Поильники", "Подогреватели бутылочек"]
  }
}

Для товара «Пластиковая бутылка 950 мл с откидной трубочкой, подходит к большинству флягодержателей» модель видит обе правдоподобные ветки — велофляги и бутылки для воды — и может взвесить упоминание флягодержателей. Распределение probabilities покажет, насколько близок выбор и стоит ли исследовать обе ветки.

Дальше код задаёт следующий Choice с потомками выбранного узла и повторяет, пока не дойдёт до листа. Если поддерево слишком большое, TypeSafe советует урезать значение до прямых потомков и нескольких примеров листьев.

Жадный спуск не исправляет раннюю ошибку. В кукбуке Hierarchical classification TypeSafe держит сразу K путей (beam search) и сравнивает их по среднему геометрическому вероятностей на рёбрах пути. Пути идут параллельными вопросами, поэтому дополнительный перебор почти не добавляет задержки. На четырёх размеченных примерах beam search с K = 3 нашёл верный лист во всех четырёх случаях, жадный спуск — в двух. Выборка крошечная, но идея понятна.

Шкалы с сигналами

Уровни Score тоже могут быть объектами — например, с кратким описанием и признаками:

"pr_scope": {
  "type": "score",
  "instructions": {
    "question": "Насколько описание pull request сосредоточено на одном изменении?",
    "note": "Оценивай число независимых изменений, а не размер каждого."
  },
  "criteria": [
    { "summary": "Одно изменение, ясно описано", "signals": ["Одна правка или функция", "Нет «заодно поправил»"] },
    { "summary": "Основное изменение и небольшая связанная правка", "signals": ["Мелкая правка поддерживает основную"] },
    { "summary": "Несколько независимых изменений", "signals": ["Две и больше несвязанных правок"] }
  ]
}

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

Описание уровней score confidence
Только строки 1,43 0,35
С примером, похожим на реальный вход 1,03 0,96
С примером на другую тему 1,43 0,35

Примеры помогают, только если похожи на ваши реальные данные. Высокая уверенность сама по себе не доказывает, что ответ верный: проверяйте новые описания на размеченных входах. Как из нескольких таких шкал собрать рейтинг — в статье о составной оценке.

Критерии Noul

У Noul criteria необязательны. Когда граница между «да» и «нет» тонкая, опишите обе стороны:

"requests_credentials": {
  "type": "noul",
  "instructions": {
    "question": "Просит ли `message` раскрыть секретные данные для входа?",
    "focus": "Ищи просьбу прислать сами данные, а не сменить или сбросить их."
  },
  "criteria": {
    "true": { "what": "Просит прислать пароль, PIN или одноразовый код", "examples": ["Пришлите код из СМС"] },
    "false": { "what": "Секретные данные не запрашиваются", "examples": ["Смените пароль в настройках"] }
  }
}

Два правила из документации. Формулируйте так, чтобы высокое значение означало «да»: вопрос «Письмо не содержит персональных данных?» переворачивает смысл, и код потом прочитает его наоборот. И не допускайте противоречий: Noul, у которого true описывает «нет», работает хуже. Проверьте вопрос с критериями и без — и оставьте вариант, который лучше работает на ваших данных.

Когда нужен второй запрос

Вопросы одного запроса независимы: ответ одного не виден другому. Если следующее суждение зависит от предыдущего ответа, сделайте второй запрос — но только когда без ответа нельзя собрать сам запрос: подгрузить данные, поменять состав state или варианты. Так устроен спуск по таксономии и кукбук о подборе навыков для агента: первый запрос ранжирует 182 навыка, второй перечитывает три лучших с полными описаниями. В остальных случаях задавайте вопросы вместе — это паттерн fan-out.

Частые ошибки

  • Шкала из цифр. Уровни «0», «1», «2» ничего не говорят модели: она не видит номеров и оценивает каждый уровень отдельно.
  • Два условия в одном Noul. «Клиент зол и просит возврат?» — это два вопроса.
  • Надежда на ключ вопроса. Ключ вроде is_spam модель не видит, полная формулировка должна быть в instructions.
  • Перенос порогов между типами. Noul и Choice с вариантами «да» и «нет» отвечают на разные вопросы, и их числа нельзя сравнивать напрямую.

Основы типов вопросов — в статье о примитивах Jev, а собрать запрос и сразу получить код можно в конструкторе запроса.

Частые вопросы

Можно ли передать в instructions Jev JSON вместо строки?

Да. instructions у всех трёх типов вопросов принимает строку, объект, массив или null. Удобно класть сам вопрос в одно поле, а данные, на которые он ссылается, — в соседние.

Зарезервированы ли в Jev имена полей вроде what, not_for, examples?

Нет. Имена полей внутри instructions и criteria выбираете вы. Модель видит их вместе со значениями, поэтому используйте короткие понятные имена и одинаковые поля у всех вариантов.

Как классифицировать по дереву категорий, если вариантов больше 255?

Задавайте один Choice на уровень дерева и спускайтесь по нему в коде. Значением варианта можно передать поддерево, чтобы модель видела, что лежит внутри ветки. Для надёжности держите несколько путей одновременно — это beam search.

Источники