Продвинутые вопросы в 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.