Как проектировать систему на Jev: пошаговый подход

Обновлено

Jev — не агент и не чат-бот, а примитив, который встраивается в обычный код. Поэтому и проектировать систему на ней нужно иначе, чем на LLM. Разбираем подход TypeSafe по шагам и собираем всё в рабочую схему разбора обращений.

Главный принцип: код управляет, модель решает

TypeSafe сравнивает три архитектуры:

Архитектура Кто управляет процессом Особенность
Обычное ПО Код — дерево решений из надёжных примитивов Надёжно, но само не разбирает неструктурированные данные
LLM-агент Модель сама выбирает следующий шаг Каждый цикл — ещё один шанс сбиться
ПО с ИИ-примитивами Код, а модель — только там, где нужен здравый смысл Каждое ИИ-решение узкое и ограничено схемой

Агентная схема, по оценке TypeSafe, хорошо работает, когда за процессом следит человек. Jev рассчитана на третий вариант. Она не пишет код и не выбирает собственное следующее действие, а отвечает на узкие типизированные вопросы. Управление, правила и побочные эффекты остаются в коде.

Название класса моделей подсказывает, какие вопросы им задавать. System One отсылает к «быстрому мышлению» из книги Даниэля Канемана «Думай медленно… решай быстро»: модель рассчитана на быстрые сфокусированные суждения, а не на долгие рассуждения.

Ниже — семь шагов из руководства TypeSafe с нашими комментариями.

Шаг 1. Всё, что можно, делайте кодом

Детерминированная логика надёжна и дёшева. Если счёт просрочен больше чем на 30 дней, это if, а не вопрос к модели. То же относится к арифметике, счёту и сравнению дат: по данным TypeSafe, jev-1.13 в них ошибается. Модель извлекает, код считает.

Перед каждым новым вопросом спросите себя, нельзя ли получить ответ регулярным выражением, парсером или запросом к базе.

Шаг 2. Соберите state только из нужного

Кладите в state только то, что относится к текущим вопросам. Лишние детали отвлекают модель — TypeSafe называет это «гниением контекста» (context rot). И не полагайтесь на знания в весах модели, если актуальные данные есть в вашей базе: правила возврата лучше передать в state, чем надеяться, что модель их «знает».

Структурируйте state как JSON с понятными именами полей и указывайте в вопросах путь к нужному значению в обратных кавычках:

{
  "model": "jev-latest",
  "state": {
    "ticket": { "message": "С меня дважды списали 4900 ₽ за заказ A-104." },
    "order": {
      "id": "A-104",
      "charges": [
        { "amount_rub": 4900, "status": "captured" },
        { "amount_rub": 4900, "status": "captured" }
      ]
    },
    "refund_policy": "Двойные списания возвращаются полностью."
  },
  "questions": {
    "duplicate_charge": {
      "type": "noul",
      "instructions": "Указывают ли `ticket.message` и `order.charges` на двойное списание?"
    },
    "policy_supports_refund": {
      "type": "noul",
      "instructions": "Подтверждает ли `refund_policy` возврат, о котором просит `ticket.message`?"
    }
  }
}

Подробнее о формате — в статье о state в Jev.

Шаг 3. Разбейте вопросы до атомарных

TypeSafe называет этот шаг, вероятно, самым важным. Широкий вопрос прячет несколько суждений за одним ответом, а атомарные открывают их для проверки, настройки и комбинирования.

Пример из документации — подозрительное письмо. Плохо: один вопрос «Это спам?». Хорошо: шесть узких Noul.

Вопрос Что проверяет
requests_credentials просит ли письмо пароль или другие данные для входа
offers_unexpected_reward обещает ли неожиданный приз или выплату
creates_time_pressure торопит ли получателя
sender_identity_mismatch противоречит ли имя отправителя его домену
link_domain_mismatch противоречит ли домен ссылки названной организации
disguises_link_destination маскирует ли текст ссылки её реальный адрес

Так же TypeSafe разбирает проверку вызовов инструментов у агента: вместо «вызовы корректны?» — девять отдельных вопросов про выбор инструмента, соответствие аргументов схеме, совпадение даты, единиц измерения и координат.

Шаг 4. Добавьте структуру в вопросы

Кроме строк, instructions и criteria принимают объекты. Это удобно, когда к вопросу нужны данные из кода: запись из базы кладётся в отдельное поле, а вопрос ссылается на неё по имени. Похожие варианты Choice разводят описаниями с полями «что входит», «что не входит» и «примеры». Короткий однозначный вопрос оставляйте строкой. Все приёмы разобраны в статье о продвинутых вопросах.

Шаг 5. Задавайте много вопросов за раз

Независимые вопросы к одному state отправляйте одним запросом. Они считаются параллельно, поэтому декомпозиция не добавляет лишних обращений к API. По словам TypeSafe, так вы получаете максимум «интеллекта на доллар» — подробности в статье о паттерне fan-out.

Шаг 6. Собирайте ответы в коде

Независимые ответы объединяют правилами или взвешенной суммой. Если есть исторические исходы, вероятности Jev можно подать признаками в классическую ML-модель. Как объединять, не теряя отдельных суждений, — в статье о составной оценке.

Шаг 7. Маршрутизируйте по неуверенности

Код должен по-разному действовать при уверенных и неуверенных ответах, а неуверенные — отдавать человеку или более дорогой рассуждающей модели. Пороги TypeSafe советует проверять на своих данных, построив зависимость точности от уверенности. Как собрать из этого экономичную систему — в статье о каскаде Jev → LLM.

Как это выглядит целиком

Сокращённая версия примера TypeSafe — разбор входящего обращения:

from typesafe_sdk import Choice, Noul, TypeSafeClient

def triage_ticket(ticket, customer):
    if ticket["status"] == "closed":  # шаг 1: детерминированное решает код
        return "no_action"

    state = {  # шаг 2: только нужный контекст
        "ticket": {"message": ticket["message"], "sender": ticket["sender"]},
        "customer": {"plan": customer["plan"]},
    }
    questions = {  # шаги 3–5: атомарные вопросы одним запросом
        "topic": Choice(
            instructions="Какая команда должна обработать `ticket.message`?",
            criteria={
                "billing": "Списания, счета, возвраты",
                "orders": "Статус, доставка, отмена заказа",
                "account": "Вход, профиль, безопасность",
            },
        ),
        "requests_credentials": Noul(instructions="Просит ли `ticket.message` прислать пароль или код?"),
        "sender_mismatch": Noul(instructions="Противоречит ли `ticket.sender.display_name` домену в `ticket.sender.email`?"),
        "unexpected_reward": Noul(instructions="Сообщает ли `ticket.message` о неожиданном призе или выплате?"),
        "refund_requested": Noul(instructions="Клиент прямо просит вернуть деньги?"),
    }
    with TypeSafeClient() as client:
        r = client.system_one(state=state, questions=questions)

    spam_risk = (  # шаг 6: веса в коде
        0.45 * r.nouls["requests_credentials"].noul
        + 0.30 * r.nouls["sender_mismatch"].noul
        + 0.25 * r.nouls["unexpected_reward"].noul
    )
    topic = r.choices["topic"]

    if 0.4 < spam_risk < 0.6 or topic.confidence < 0.75:  # шаг 7
        return route_to_human_review(ticket)
    if spam_risk >= 0.6:
        return quarantine_as_spam(ticket)
    if topic.choice == "billing":
        return route_to_billing(ticket, refund=r.nouls["refund_requested"].noul >= 0.7)
    return route_to_team(topic.choice, ticket)

Веса признаков спама — 0,45, 0,30 и 0,25 — в сумме дают 1. Серая зона от 0,4 до 0,6 и неуверенная тема уходят человеку, остальное код решает сам. Вопрос о возврате спекулятивный: код читает его только в ветке оплаты.

Как тестировать

Типизированные ответы удобно проверять обычными тестами. TypeSafe заявляет, что System One даёт стабильные ответы при повторных вызовах, поэтому набор размеченных примеров работает как регрессионный тест:

  1. Соберите от нескольких десятков до пары сотен реальных входов с ожидаемым ответом на каждый вопрос.
  2. Прогоните их и сохраните ответы, вероятности и уверенность.
  3. После каждой правки формулировок, критериев или весов прогоняйте набор снова и сравнивайте с прошлым результатом.
  4. Отдельно разбирайте примеры с низкой уверенностью: они показывают, каким вопросам не хватает контекста или чётких критериев.

Быстро попробовать state и вопросы без кода можно в песочнице консоли TypeSafe — на неё ведут ссылки из примеров в документации. Собрать запрос и сразу получить код поможет наш конструктор запроса.

Чего избегать

  • спрашивать модель о том, что код вычисляет точно;
  • прятать несколько суждений в одном вопросе;
  • ставить задачи для медленного мышления: многоходовые рассуждения и цепочки косвенных ссылок;
  • класть в state больше, чем нужно вопросу;
  • ждать дообучения: по данным TypeSafe, Jev не дообучают под клиентов, и свою область модель узнаёт из state, инструкций и критериев;
  • подбирать пороги под алиас jev-latest: он переезжает на новые версии, поэтому TypeSafe советует фиксировать конкретную;
  • проверять систему только на английских примерах: основной язык обучения Jev — английский, и для русского потока точность и пороги нужно мерить отдельно.

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

Можно ли дообучить Jev на своих данных?

Нет. По данным TypeSafe, Jev не дообучают и не адаптируют через LoRA под клиентов — все аккаунты работают с одними весами. Под свою область модель настраивают через state, формулировки вопросов и критерии, а также через декомпозицию.

Чем система на Jev отличается от LLM-агента?

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

Что важнее всего при проектировании на Jev?

По словам TypeSafe, декомпозиция вопросов. Узкие атомарные вопросы открывают каждое суждение для проверки и настройки, а широкий вопрос прячет несколько суждений за одним ответом.

Источники