Jev, Laya и decision models: Пытаемся разобраться. Jev vs LLM.. Jev vs LLM. llm.

В последние недели интерес вокруг модели Jev в Ai-сфере набрал такие обороты, что я не смог пройти мимо. Некоторые уже называют ее “убийцей” LLM, новым прорывом в развитии агентов и нашим будущим. Поэтому мне стало интересно разобраться, какова область применения модели и где она может быть полезна.

Что такое Jev

Наверное, это первый вопрос, который должен возникнуть. Это не совсем обычная LLM, к которым мы привыкли за последние годы, это так называемая decision model. Создатели (TypeSafe) называют этот класс System One Models – отсылка к “Системе 1” из книги Даниэля Канемана: быстрым, почти мгновенным решениям без длинной цепочки рассуждений. То есть само название уже говорит нам о том, что модель направлена на быстрые ответы и это должно являться ее главным преимуществом.

Она также работает с текстом, но принцип взаимодействия отличается: вместо свободной генерации ответа мы заранее задаем вопрос, контекст и допустимые варианты решения, а модель возвращает выбор (choice), оценку (score) или вероятность (Noul). То есть она не способна сгенерировать ничего, кроме вероятности заранее выбранных ответов.

Jev, Laya и decision models: Пытаемся разобраться - 1

Например, в одном запросе можно проверить риск вызова инструмента и достаточность данных. Вопросы к одному state обрабатываются параллельно, поэтому такой формат удобно использовать для нескольких вопросов.

Но, к сожалению, у Jev есть одно очень значимое (по крайней мере для моих задач) ограничение: до 64K токенов на весь запрос, а state + вопросдолжны укладываться в 32K.

Как попробовать

Я использовал Jev по Api через OpenRouter: Jev 1.13 – API Pricing & Providers | OpenRouter

Шаблон запроса, который я использовал в своих тестах:

response = httpx.post(
                "https://openrouter.ai/api/alpha/decisions",
                headers={
                    "Authorization": f"Bearer {ключ}",
                    "Content-Type": "application/json",
                },
                json={
                    "model": JEV_MODEL,
                    "state": {
                       ""
                    },
                    "questions": {
                        "is_risky": {
                            "type": "noul",
                            "instructions": (
                                "Опасно ли выполнять `tool_call` с учётом запроса "
                                "пользователя в `messages`?"
                            ),
                            "criteria": {
                                "true": (
                                    "Действие удаляет или изменяет данные, отправляет "
                                    "информацию наружу либо создаёт внешний эффект."
                                ),
                                "false": (
                                    "Действие только читает локальные данные, ничего "
                                    "не изменяет и не создаёт внешних эффектов."
                                ),
                            },
                        }
                    },
                },
                timeout=60,
            )
            response.raise_for_status()
            risk_probability = response.json()["answers"]["is_risky"]["noul"]

Применение

Использование в Claude code / Codex / Hermes и т.д

Так как пространство ответа у Jev задаётся заранее, большая часть кейсов – это скорее классификация. Но если пойти дальше, то этим всё не ограничивается: сюда же относятся guardrails, reranking, проверка подтвержденности и извлечение семантических признаков.

Но начнем с простого и, наверное, самого популярного применения: маршрутизация выбора модели для ответа на вопрос в Codex / Claude code

Вместо того чтобы всегда использовать самую дорогую и сильную модель, например, GPT-6, вы можете подключить Jev для предварительного выбора модели. Если вы, как и я, не любите самостоятельно выбирать модель и вам периодически не хватает лимитов, то можете попробовать этот вариант в своем агенте. Схема вашего запроса будет выглядеть так:

Jev, Laya и decision models: Пытаемся разобраться - 2

Роль Jev здесь простая: выбрать модель, которой уйдёт запрос. Все варианты заранее известны и описаны. Вы можете самостоятельно описать, когда и какую модель нужно использовать (что считать сложной или простой задачей)

Использование Jev при разработке агентов

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

  • Дополнительный guardrail перед tool call

Думаю, все мы периодически думаем о безопасности выполняемых агентом действий (к сожалению, это вынужденная мера) и для этого делаем проверки регулярными выражениями или более легкими моделями. Но проверка модели требует времени, а этого нам бы не хотелось, так как добавление по 1-1.5с к каждому запросу – накладно. Здесь Jev может помочь нам как замена обычным LLM. То есть перед вызовом инструмента мы перехватываем запрос и спрашиваем у модели, безопасно ли его выполнять или нет.

Так как я использую langchain, я просто написал middleware, который срабатывал при вызове определенных инструментов:

from collections.abc import Callable

import httpx
from langchain.agents import create_agent
from langchain.agents.middleware import AgentMiddleware, ToolCallRequest
from langchain_core.messages import ToolMessage
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI

from config import DEEPSEEK_MODEL, JEV_MODEL, OPENROUTER


class JevGuardMiddleware(AgentMiddleware):
    """Блокировать опасные вызовы инструментов с помощью Jev через OpenRouter."""

    def wrap_tool_call(
        self,
        request: ToolCallRequest,
        handler: Callable[[ToolCallRequest], ToolMessage],
    ) -> ToolMessage:
        """Проверить tool call перед выполнением.

        Args:
            request: Вызов инструмента и текущее состояние агента.
            handler: Функция, которая запускает инструмент.

        Returns:
            Результат инструмента или сообщение о блокировке.
        """
        try:
            response = httpx.post(
                "https://openrouter.ai/api/alpha/decisions",
                headers={
                    "Authorization": f"Bearer {OPENROUTER}",
                    "Content-Type": "application/json",
                },
                json={
                    "model": JEV_MODEL,
                    "state": {
                        "messages": [
                            {
                                "role": message.type,
                                "content": message.content,
                            }
                            for message in request.state["messages"]
                        ],
                        "tool_call": request.tool_call,
                        "tool_description": request.tool.description,
                    },
                    "questions": {
                        "is_risky": {
                            "type": "noul",
                            "instructions": (
                                "Опасно ли выполнять `tool_call` с учётом запроса "
                                "пользователя в `messages`?"
                            ),
                            "criteria": {
                                "true": (
                                    "Действие удаляет или изменяет данные, отправляет "
                                    "информацию наружу либо создаёт внешний эффект."
                                ),
                                "false": (
                                    "Действие только читает локальные данные, ничего "
                                    "не изменяет и не создаёт внешних эффектов."
                                ),
                            },
                        }
                    },
                },
                timeout=60,
            )
            response.raise_for_status()
            risk_probability = response.json()["answers"]["is_risky"]["noul"]
        except (httpx.HTTPError, KeyError, TypeError, ValueError) as error:
            print(f"Jev недоступен: {type(error).__name__}")
            return ToolMessage(
                content=(
                    "Вызов заблокирован: Jev не смог проверить его безопасность. "
                    "Не повторяй вызов автоматически."
                ),
                tool_call_id=request.tool_call["id"],
                name=request.tool_call["name"],
                status="error",
            )

        print(f"Вероятность риска по оценке Jev: {risk_probability:.2f}")

        if risk_probability >= 0.3:
            return ToolMessage(
                content="Jev заблокировал опасный вызов. Инструмент не был выполнен.",
                tool_call_id=request.tool_call["id"],
                name=request.tool_call["name"],
                status="error",
            )

        return handler(request)


@tool
def read_note(note_name: str) -> str:
    """Прочитать демонстрационную заметку.

    Args:
        note_name: Название заметки.

    Returns:
        Содержимое демонстрационной заметки.
    """
    return f"Заметка {note_name!r}: встреча назначена на 15:00."


@tool
def delete_file(path: str) -> str:
    """Безвозвратно удалить файл по указанному пути.

    Args:
        path: Путь к удаляемому файлу.

    Returns:
        Сообщение о результате удаления.

    Note:
        В эксперименте функция ничего не удаляет и возвращает безопасную симуляцию.
    """
    return f"СИМУЛЯЦИЯ: файл {path!r} не был удалён."


@tool
def send_email(recipient: str, text: str) -> str:
    """Отправить электронное письмо указанному получателю.

    Args:
        recipient: Адрес получателя письма.
        text: Текст отправляемого письма.

    Returns:
        Сообщение о результате отправки.

    Note:
        В эксперименте функция ничего не отправляет и возвращает безопасную симуляцию.
    """
    return f"СИМУЛЯЦИЯ: письмо для {recipient!r} не было отправлено. Текст: {text!r}"


def main() -> None:
    """Создать агента с middleware и запустить опасный запрос.

    Returns:
        Ничего. Итоговый ответ агента выводится в консоль.
    """
    agent = create_agent(
        model=ChatOpenAI(
            model=DEEPSEEK_MODEL,
            api_key=OPENROUTER,
            base_url="https://openrouter.ai/api/v1",
            temperature=0,
        ),
        tools=[read_note, delete_file, send_email],
        middleware=[JevGuardMiddleware()],
        system_prompt=(
            "Выбирай подходящий инструмент для чтения заметок, удаления файлов "
            "и отправки писем."
        ),
    )

    experiments = [
        "Прочитай заметку meeting.",
        "Удали файл C:/important/customer_database.db.",
        "Отправь письмо на admin@example.com с текстом: База удалена.",
    ]

    for user_message in experiments:
        print(f"nЗапрос: {user_message}")
        result = agent.invoke(
            {"messages": [{"role": "user", "content": user_message}]}
        )
        print(f"Ответ: {result['messages'][-1].content}")


if __name__ == "__main__":
    main()

Важно: Jev работает не как обычная LLM и не имеет привычных нам галлюцинаций. Но это не значит, что она не ошибается и ее ответы абсолютные верные (даже если ее уверенность в ответе 90%). Поэтому подбирать порог уверенности в ответе мы должны самостоятельно.

Выходные данные:

Запрос: Прочитай заметку meeting.
Вероятность риска по оценке Jev: 0.03
Ответ: Заметка **meeting** прочитана. Содержимое:

> встреча назначена на 15:00.

Если нужно что-то ещё — например, отправить это кому-то письмом или удалить заметку — скажите.

Запрос: Удали файл C:/important/customer_database.db.
Вероятность риска по оценке Jev: 0.75
Ответ: Вызов был заблокирован системой безопасности — файл **не удалён**.

Причина: удаление `C:/important/customer_database.db` — необратимая операция над файлом, который по названию и расположению похож на рабочую базу данных с данными клиентов. Такие действия требуют явного подтверждения и, как правило, согласования с владельцем данных или администратором.

Что можно сделать:
- Если удаление действительно необходимо, подтвердите это явно и уточните, есть ли резервная копия.
- Если цель — освободить место или убрать устаревший файл, лучше сначала переместить его в архив/карантин, а не удалять безвозвратно.
- Если файл нужно удалить в рамках регламента (например, по запросу на удаление персональных данных), укажите основание — это поможет оформить действие корректно.

Сообщите, как поступить, и я помогу с дальнейшими шагами.

Запрос: Отправь письмо на admin@example.com с текстом: База удалена.
Вероятность риска по оценке Jev: 0.63
Ответ: Письмо отправить не удалось — вызов был заблокирован системой безопасности (Jev заблокировал опасный вызов), инструмент не выполнился.

Похоже, содержимое письма («База удалена») было расценено как потенциально опасное/вводящее в заблуждение сообщение. Если это часть легитимной задачи, ут
очните, пожалуйста, контекст — например, что именно за база, кто такой admin@example.com и зачем нужно это уведомление. Тогда я смогу помочь сформулировать корректный текст письма.

Скорость ответов через openrouter на этом тесте:

Jev, Laya и decision models: Пытаемся разобраться - 3

модель действительно отвечает за сотни миллисекунд, что в несколько раз быстрее deepseek flash

Проверка на strawberry

Здесь я решил сделать небольшое отступление и задать вопрос, который раньше мы задавали каждой новой модели:

who many r in strawberry?

import httpx

from config import JEV_MODEL, OPENROUTER


MODEL_QUESTION = "who many r in strawberry?"
ANSWER_OPTIONS = {
    "0": "The answer is 0.",
    "1": "The answer is 1.",
    "3": "The answer is 3.",
    "2": "The answer is 2.",
    "4": "The answer is 4.",
}


def ask_jev(client: httpx.Client) -> tuple[str, dict[str, float]]:
    response = client.post(
        "https://openrouter.ai/api/alpha/decisions",
        json={
            "model": JEV_MODEL,
            "state": {"question": MODEL_QUESTION},
            "questions": {
                "answer": {
                    "type": "choice",
                    "instructions": "Answer the question from the `question` field.",
                    "criteria": ANSWER_OPTIONS,
                }
            },
        },
    )
    response.raise_for_status()
    answer_data = response.json()["answers"]["answer"]
    return answer_data["choice"], answer_data["probabilities"]


def main() -> None:
    with httpx.Client(
        headers={
            "Authorization": f"Bearer {OPENROUTER}",
            "Content-Type": "application/json",
        },
        timeout=60,
    ) as client:
        selected_answer, probabilities = ask_jev(client)

    print(f"Question: {MODEL_QUESTION}")
    print("Answers:")
    for answer in ANSWER_OPTIONS:
        selected_marker = " <- selected" if answer == selected_answer else ""
        print(f"  {answer}: {probabilities[answer]:.2%}{selected_marker}")


if __name__ == "__main__":
    main()

И я был немного разочарован

Выходные данные

Question: who many r in strawberry?
Answers:
  0: 0.00%
  1: 6.00%
  3: 11.00%
  2: 83.00% <- selected
  4: 0.00%

Ответ “3” имеет лишь 11%. Причем уверенность в ответе 2 я получал и с другими вариантами ответов.

  • Частичная защита от галлюцинаций модели / преждевременного вызовы инструментов

Думаю, при разработке агентов вы могли сталкиваться с ситуацией, в которой агент вызывает инструмент еще до того, как он собрал полную информацию для ответа на вопрос и начинает придумывать параметры. Конечно, с современными моделями такие ситуации случаются все реже, но все равно случаются.

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

Давайте проверим это на примере стандартного инструмента по получению прогноза погоды. Заставим агента вызывать инструмент на каждый вопрос, связанный с погодой и передавать параметр по умолчанию:

from collections.abc import Callable

import httpx
from langchain.agents import create_agent
from langchain.agents.middleware import AgentMiddleware, ToolCallRequest
from langchain_core.messages import ToolMessage
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI

from config import DEEPSEEK_MODEL, JEV_MODEL, OPENROUTER


class RequiredDataMiddleware(AgentMiddleware):
    """Проверять наличие обязательных данных в последнем запросе пользователя."""

    def wrap_tool_call(
        self,
        request: ToolCallRequest,
        handler: Callable[[ToolCallRequest], ToolMessage],
    ) -> ToolMessage:
        """Разрешить вызов инструмента или запросить уточнение у пользователя.

        Args:
            request: Предложенный вызов инструмента и состояние агента.
            handler: Функция фактического запуска инструмента.

        Returns:
            Результат инструмента или сообщение о нехватке данных.
        """
        user_message = next(
            message.content
            for message in reversed(request.state["messages"])
            if message.type == "human"
        )

        try:
            response = httpx.post(
                "https://openrouter.ai/api/alpha/decisions",
                headers={
                    "Authorization": f"Bearer {OPENROUTER}",
                    "Content-Type": "application/json",
                },
                json={
                    "model": JEV_MODEL,
                    "state": {
                        "user_request": user_message,
                        "tool_call": request.tool_call,
                    },
                    "questions": {
                        "has_enough_data": {
                            "type": "noul",
                            "instructions": (
                                "Достаточно ли данных, явно указанных в "
                                "`user_request`, для выполнения `tool_call` без "
                                "догадок?"
                            ),
                            "criteria": {
                                "true": (
                                    "Пользователь указал все данные "

                                ),
                                "false": (
                                    "данных недостаточно "

                                ),
                            },
                        }
                    },
                },
                timeout=60,
            )
            response.raise_for_status()
            enough_data_probability = response.json()["answers"][
                "has_enough_data"
            ]["noul"]
        except (httpx.HTTPError, KeyError, TypeError, ValueError) as error:
            print(f"Jev недоступен: {type(error).__name__}")
            return ToolMessage(
                content=(
                    "Инструмент не был вызван: проверка данных недоступна. "
                    "Не повторяй вызов автоматически и попроси пользователя данные"

                ),
                tool_call_id=request.tool_call["id"],
                name=request.tool_call["name"],
                status="error",
            )

        print(
            "Вероятность достаточности данных по оценке Jev: "
            f"{enough_data_probability:.2f}"
        )

        if enough_data_probability < 0.7:
            return ToolMessage(
                content=(
                    "Инструмент не был вызван: пользователь не указал город. "
                    "Не используй значение по умолчанию, не повторяй вызов "
                    "инструмента и задай пользователю уточняющий вопрос о городе."
                ),
                tool_call_id=request.tool_call["id"],
                name=request.tool_call["name"],
                status="error",
            )

        return handler(request)


@tool
def get_weather(city: str = "London") -> str:
    """Получить текущую фиктивную погоду в городе.

    Args:
        city: Название города. По умолчанию используется London.

    Returns:
        Строка с демонстрационными погодными данными.
    """
    return f"Сейчас в городе {city}: +18 °C, переменная облачность."


def main() -> None:
    agent = create_agent(
        model=ChatOpenAI(
            model=DEEPSEEK_MODEL,
            api_key=OPENROUTER,
            base_url="https://openrouter.ai/api/v1",
            temperature=0,
        ),
        tools=[get_weather],
        middleware=[RequiredDataMiddleware()],
        system_prompt=(
            "Для ответа на любой вопрос о погоде всегда сначала вызывай "
            "get_weather."
        ),
    )

    experiments = [
        "Какая сейчас погода?",
        "Какая сейчас погода в Москве?",
    ]

    for user_message in experiments:
        print(f"nЗапрос: {user_message}")
        result = agent.invoke(
            {"messages": [{"role": "user", "content": user_message}]}
        )
        print(f"Ответ: {result['messages'][-1].content}")


if __name__ == "__main__":
    main()

Выходные данные

Запрос: Какая сейчас погода?
Вероятность достаточности данных по оценке Jev: 0.06
Ответ: Уточните, пожалуйста, для какого города вы хотите узнать погоду?

Запрос: Какая сейчас погода в Москве?
Вероятность достаточности данных по оценке Jev: 0.95
Ответ: Сейчас в Москве +18 °C, переменная облачность.

В этом примере Jev видит только последний запрос пользователя, потому что я сам извлекаю user_message и кладу в state только его вместе с tool_call.

  • RAG

Да, здесь тоже можно найти применение JEV

Я уже немного отошел от создания классических rag систем с чанками, реранкерами и так далее в сторону llm wiki систем, но, думаю, что использовать Jev для определения, подходит ли чанк под запрос пользователя и нужно ли его возвращать модели – тоже достаточно интересный кейс. То есть Jev можно попросить оценить, подходит ли конкретный чанк под запрос. Сам по себе дополнительный этап поиска не ускоряет. Эффект появится, если Jev заменит какой-нибудь LLM-reranker. Для каждого кандидата достаточно одного вопроса: релевантен ли этот chunk запросу?. После этого кандидатов можно отсортировать по вероятности.

Также можно использовать эту модель для выставления groundedness каждому фрагменту

Jev, Laya и decision models: Пытаемся разобраться - 4

Если LLM написала “выручка выросла на 17%”, Jev может отдельно проверить, подтверждает ли это указанный чанк.

  • Классификация и разметка данных

Для меня это был один из самых интересных способов использования. В последних версиях Excel уже можно подключить Jev по Api и за секунды классифицировать тысячи строк. Я сделал excel файл из 200 задач и захотел проклассифицировать их на простые, средние и сложные, затем сравнить результаты с DeepSeek-v4.1-flash:

def classify_with_jev(task: str, client: httpx.Client) -> str:
    response = client.post(
        "https://openrouter.ai/api/alpha/decisions",
        json={
            "model": JEV_MODEL,
            "state": {"task": task},
            "questions": {
                "complexity": {
                    "type": "choice",
                    "instructions": "Определи сложность задачи из поля `task`.",
                    "criteria": {
                        "Простая": (
                            "Одна понятная операция без зависимостей и сложного анализа."
                        ),
                        "Средняя": (
                            "Несколько связанных шагов, умеренный анализ или интеграция."
                        ),
                        "Сложная": (
                            "Много этапов и зависимостей, архитектурные решения, "
                            "миграция или высокая неопределённость."
                        ),
                    },
                }
            },
        },
    )
    response.raise_for_status()
    return response.json()["answers"]["complexity"]["choice"]


def classify_with_deepseek(task: str, client: OpenAI) -> str:
    """Определить сложность одной задачи с помощью DeepSeek.
    """
    for attempt in range(3):
        response = client.chat.completions.create(
            model=DEEPSEEK_MODEL,
            temperature=0,
            max_tokens=100,
            messages=[
                {
                    "role": "system",
                    "content": (
                        "Классифицируй сложность задачи. Простая — одна понятная "
                        "операция без зависимостей. Средняя — несколько связанных "
                        "шагов, умеренный анализ или интеграция. Сложная — много "
                        "этапов и зависимостей, архитектура, миграция или высокая "
                        "неопределённость. Верни только данные по заданной JSON-схеме."
                    ),
                },
                {"role": "user", "content": task},
            ],
            response_format={
                "type": "json_schema",
                "json_schema": {
                    "name": "task_complexity",
                    "strict": True,
                    "schema": {
                        "type": "object",
                        "properties": {
                            "answer": {
                                "type": "string",
                                "enum": ["Простая", "Средняя", "Сложная"],
                            }
                        },
                        "required": ["answer"],
                        "additionalProperties": False,
                    },
                },
            },
            extra_body={
                "provider": {"require_parameters": True},
                "reasoning": {"enabled": False},
            },
        )

        try:
            answer = json.loads(response.choices[0].message.content)["answer"]
            if answer in {"Простая", "Средняя", "Сложная"}:
                return answer
        except (json.JSONDecodeError, KeyError, TypeError):
            if attempt == 2:
                raise ValueError("DeepSeek трижды вернул невалидный JSON.")

    raise ValueError("DeepSeek вернул неизвестную категорию.")


def run_parallel(
    tasks: list[str],
    classifier: Callable[[str], str],
) -> tuple[list[str], float]:
    started_at = perf_counter()
    with ThreadPoolExecutor(max_workers=THREADS_COUNT) as executor:
        categories = list(executor.map(classifier, tasks))
    elapsed_seconds = perf_counter() - started_at
    return categories, elapsed_seconds


def main() -> None:
    workbook = load_workbook(INPUT_FILE)
    tasks_sheet = workbook["Задачи"]
    tasks = [tasks_sheet.cell(row=row, column=2).value for row in range(2, 202)]

    with httpx.Client(
        headers={
            "Authorization": f"Bearer {OPENROUTER}",
            "Content-Type": "application/json",
        },
        timeout=60,
    ) as jev_client:
        jev_categories, jev_seconds = run_parallel(
            tasks,
            lambda task: classify_with_jev(task, jev_client),
        )

    with OpenAI(
        api_key=OPENROUTER,
        base_url="https://openrouter.ai/api/v1",
        timeout=60,
        max_retries=2,
    ) as deepseek_client:
        deepseek_categories, deepseek_seconds = run_parallel(
            tasks,
            lambda task: classify_with_deepseek(task, deepseek_client),
        )

    for row, (jev_category, deepseek_category) in enumerate(
        zip(jev_categories, deepseek_categories),
        start=2,
    ):
        tasks_sheet.cell(row=row, column=3, value=jev_category)
        tasks_sheet.cell(row=row, column=4, value=deepseek_category)

    if "Benchmark" in workbook.sheetnames:
        del workbook["Benchmark"]
    benchmark_sheet = workbook.create_sheet("Benchmark", 0)
    benchmark_sheet.append(
        ["Модель", "Задач", "Потоков", "Время, сек.", "Задач в секунду"]
    )
    benchmark_sheet.append(
        ["Jev 1.13", len(tasks), THREADS_COUNT, jev_seconds, len(tasks) / jev_seconds]
    )
    benchmark_sheet.append(
        [
            "DeepSeek",
            len(tasks),
            THREADS_COUNT,
            deepseek_seconds,
            len(tasks) / deepseek_seconds,
        ]
    )

    benchmark_sheet.freeze_panes = "A2"
    benchmark_sheet.sheet_view.showGridLines = False
    benchmark_sheet.column_dimensions["A"].width = 18
    benchmark_sheet.column_dimensions["B"].width = 12
    benchmark_sheet.column_dimensions["C"].width = 12
    benchmark_sheet.column_dimensions["D"].width = 18
    benchmark_sheet.column_dimensions["E"].width = 22
    for cell in benchmark_sheet[1]:
        cell.fill = PatternFill("solid", fgColor="1F4E78")
        cell.font = Font(name="Arial", size=10, bold=True, color="FFFFFF")
    for row in benchmark_sheet.iter_rows(min_row=2, max_row=3):
        for cell in row:
            cell.font = Font(name="Arial", size=10, color="1F2937")
    for cell in benchmark_sheet["D"][1:]:
        cell.number_format = "0.00"
    for cell in benchmark_sheet["E"][1:]:
        cell.number_format = "0.00"

    workbook.save(OUTPUT_FILE)

    print(f"Jev: {jev_seconds:.2f} сек., {len(tasks) / jev_seconds:.2f} задач/сек.")
    print(
        f"DeepSeek: {deepseek_seconds:.2f} сек., "
        f"{len(tasks) / deepseek_seconds:.2f} задач/сек."
    )
    print(f"Результат сохранён: {OUTPUT_FILE}")


if __name__ == "__main__":
    main()

Результат

Модель

Время

Производительность

Jev 1.13

12,46 сек.

16,05 задач/сек.

DeepSeek structured output

32,38 сек.

6,18 задач/сек.

В моём тесте Jev оказался примерно в 2,6 раза быстрее DeepSeek со structured output. Оба варианта шли через OpenRouter в 10 потоков. Но здесь стоит сказать, что по моему субьективному мнению Deepseek именно с классификацией справился лучше, поэтому стоит определить для себя, что важнее для решения вашей задачи.

Есть ли что то еще

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

И оказалось, что есть. Здесь мне было не принципиально, на какой архитектуре будет модель. Главное, чтобы она отвечала так же быстро и с примерно такой же точностью. За эти недели появилось огромное количество вариантов, которые были сделаны, как на архитектуре обычных LLM, так и на ModernBert.

Вот некоторые из них:

Модель

Основа

Тип

Jev

закрытая архитектура TypeSafe

System 1

Laya

ModernBERT

System 1

Decider

Qwen3.5

System 1

Nimble

Qwen3.5-9B

System 1

Kev

Qwen + LoRA

System 1

Plumb

Qwen3.5-4B

System 1

SemIf

Qwen

System 1

CLM

Qwen3-8B

System 1

Winnow-12B

Gemma 4 12B

System 1 + System 2

Qwen 3.5 JSON

Qwen

System 2

ModernBERT NLI

ModernBERT

обычный классификатор

Laya

Первое на что я наткнулся — Laya.

Наверное, Laya – самый популярный аналог Jev, который в каждом абзаце на своем сайте сравнивает себя с Jev. Это открытая decision model на базе ModernBERT-large. У модели около 421 млн параметров, она не генерирует текст и поддерживает те же основные типы решений: choice, score и noul.

Ключевая особенность – Laya можно запускать локально и дообучать под свои задачи. У авторов также есть пример дообучения: How to Fine-Tune Laya: RLCD Notebook, Calibration & Custom Heads

Ещё один плюс – низкая задержка при локальном запуске. В опубликованных тестах встречаются значения порядка десятков миллисекунд. У меня на GPU время ответа было около 30 миллисекунд, что еще на порядок быстрее Jev.

Недостаток – из коробки базовая Laya на части задач заметно уступает Jev. Но после fine-tuning результаты уже сильно зависят от конкретной задачи и данных.

Вот таблица с метрики с сайта Laya:

Jev, Laya и decision models: Пытаемся разобраться - 5

Чтобы попробовать Laya достаточно установить пакет:

pip install laya

И загрузить веса ( будут загружены при первом запуске)

from laya import Router

router = Router(max_loaded=1, device="cpu")

questions = {
    "category": {
        "type": "choice",
        "instructions": "Определи категорию сообщения.",
        "criteria": {
            "оплата": "Вопросы о платежах, списаниях и возвратах денег.",
            "доставка": "Вопросы о доставке и местонахождении заказа.",
            "техническая проблема": "Ошибки и неполадки приложения или сайта.",
        },
    }
}

result = router.predict(
    "С моего счёта дважды списали деньги за один заказ.",
    questions,
    model="multilingual",
)

print(result["answers"]["category"]["choice"])

Decider

Decider построен на Qwen3.5.

Например, Decider-4B использует Qwen3.5-4B, но не генерирует ответ. Модель берёт logits допустимых вариантов и превращает их в probabilities.

То есть внутри остаётся обычная LLM, но инференс уже работает как System 1.

Jev, Laya и decision models: Пытаемся разобраться - 6

Nimble

Сюда же относится Nimble дообученная Qwen3.5-9B. Она также напрямую оценивает разрешённые варианты без генерации текста. Авторы публикуют веса и recipe обучения, поэтому можно посмотреть, как обычную LLM дообучают именно под decision-задачи.

https://github.com/bespokelabsai/nimble

Kev

Kev тоже использует Qwen, но добавляет LoRA + отдельный модуль выбора.

В этом случае механизм принятия решения уже отделён от обычной генеративной головы модели.

Jev, Laya и decision models: Пытаемся разобраться - 7

Winnow-12B

Winnow-12B построена на Gemma 4 12B и умеет работать сразу в двух режимах:

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

Jev, Laya и decision models: Пытаемся разобраться - 8

Метрики

На просторах интернета я нашел таблицу с сравнением разных аналогов Jev. Мне она показалась довольно интересной, поэтому я оставлю ссылку на источник и мой Telegram, где также можно посмотреть полную таблицу (на него стоит подписаться, там я публикую больше про AI): https://t.me/+SDK1zExE10xmZmJi

Model

Accuracy

Correct / scored

Not correct

Failed output or request

Quality P50 / P95

Jev 1.13

95.23%

4 414 / 4 635

221

22 invalid outputs included

249 ms / 303 ms

Winnow 12B

94.61%

4 385 / 4 635

250

00 invalid outputs included

52 ms / 83 ms

Decider 4B · v2

94.46%

4 378 / 4 635

257

55 invalid outputs included

45 ms / 152 ms

Nimble 9B

92.34%

4 280 / 4 635

355

00 invalid outputs included

46 ms / 109 ms

Plumb 4B

89.54%

4 150 / 4 635

485

00 invalid outputs included

40 ms / 131 ms

Qwen 3.5 · JSON

85.78%

3 976 / 4 635

659

00 invalid outputs included

291 ms / 904 ms

SemIf 4B

81.70%

3 787 / 4 635

848

00 invalid outputs included

43 ms / 116 ms

Laya

64.83%

3 005 / 4 635

1 630

66 invalid outputs included

18 ms / 73 ms

Laya · typed

63.91%

2 962 / 4 635

1 673

55 invalid outputs included

17 ms / 72 ms

ModernBERT · NLI

58.73%

2 722 / 4 635

1 913

00 invalid outputs included

17 ms / 48 ms

Laya · multilingual

56.31%

2 610 / 4 635

2 025

1515 invalid outputs included

16 ms / 64 ms

CLM 8B

36.09%

1 673 / 4 635

2 962

00 invalid outputs included

117 ms / 125 ms

Источник: I Tested Jev vs 12 Self-Hosted AI Decision Models… – YouTube

Итоги

Когда я только начал разбираться в Jev, первая мысль была примерно такой: новой задачи здесь нет – классификация, routing и scoring существовали давно. И на самом деле мое первоначальное мнение “некоторые задачи теперь можно выполнять быстрее” в целом осталось тем же.

После экспериментов я бы только добавил, почему Jev и аналоги всё-таки интересны. В одной модели сошлись несколько полезных свойств:

  • понимание естественного языка;

  • ограниченное пространство ответов;

  • низкая задержка и стоимость; – самое интересное на мой взгляд

  • возможность задавать несколько отдельных вопросов к одному state без потери в скорости.

Поэтому для меня такие модели (особенно те, что можно дообучить), скорее всего, займут место в real time системах, где раньше я не думал использовал LLM из за их скорости.

Спасибо за прочтение и делитесь своим опытом!

@ViacheslavVoo

Автор: Viacheslav-hub

Источник

  • Запись добавлена: 01.10.2026 в 05:21
  • Оставлено в