CNN TürkBenzine kademeli ÖTV artışıPunch10 major highlights of Tinubu’s 66th Independence Day addressThe Jerusalem PostShin Bet heads to Dubai to coordinate return flights after flydubai hijacking causes suspensionRTP DesportoTiago Luís Pereira, bronze no triplo nos Mundiais `indoor`, é a mais recente saída do SportingESPNJJ Gabriel and the 'nightmare' battle for Premier League academy talentInquirerWATCH: Sara Duterte impeachment trial | Oct. 1, 2026Bollywood HungamaBigg Boss 20: Rhiti Tiwari evicted in surprise mid-week exit after captaincy task? Here’s what we know!Egypt IndependentUS troops complete Iraq exit, as Iran and its allies celebrateNHK 社会高齢者の医療費窓口負担 見直しに向けて議論開始 厚労省20 MinutenGeldsorgen nach Long Covid: Cora Schumacher will auswandernWirtualna PolskaCiepły początek października. Nawet 25 stopniRTL BoulevardNederland haalt 'alles uit de kast' om Bolle Jos uit Sierra Leone te krijgen
The Daily Newsstand · Free, Always
Thursday, October 1, 2026

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

Translate

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

Что такое Jev

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

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

Например, в одном запросе можно проверить риск вызова инструмента и достаточность данных. Вопросы к одному 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 здесь простая: выбрать модель, которой уйдёт запрос. Все варианты заранее известны и описаны. Вы можете самостоятельно описать, когда и какую модель нужно использовать (что считать сложной или простой задачей)

Использование 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 на этом тесте:

модель действительно отвечает за сотни миллисекунд, что в несколько раз быстрее 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 каждому фрагменту

Если 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:

Чтобы попробовать 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.

Nimble

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

https://github.com/bespokelabsai/nimble

Kev

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

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

Winnow-12B

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

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

Метрики

На просторах интернета я нашел таблицу с сравнением разных аналогов 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 из за их скорости.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.