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

—

от автора

Привет, Хабр!

В последние недели интерес вокруг модели 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 Callableimport httpxfrom langchain.agents import create_agentfrom langchain.agents.middleware import AgentMiddleware, ToolCallRequestfrom langchain_core.messages import ToolMessagefrom langchain_core.tools import toolfrom langchain_openai import ChatOpenAIfrom config import DEEPSEEK_MODEL, JEV_MODEL, OPENROUTERclass 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)@tooldef read_note(note_name: str) -> str:    """Прочитать демонстрационную заметку.    Args:        note_name: Название заметки.    Returns:        Содержимое демонстрационной заметки.    """    return f"Заметка {note_name!r}: встреча назначена на 15:00."@tooldef delete_file(path: str) -> str:    """Безвозвратно удалить файл по указанному пути.    Args:        path: Путь к удаляемому файлу.    Returns:        Сообщение о результате удаления.    Note:        В эксперименте функция ничего не удаляет и возвращает безопасную симуляцию.    """    return f"СИМУЛЯЦИЯ: файл {path!r} не был удалён."@tooldef 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 httpxfrom config import JEV_MODEL, OPENROUTERMODEL_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 Callableimport httpxfrom langchain.agents import create_agentfrom langchain.agents.middleware import AgentMiddleware, ToolCallRequestfrom langchain_core.messages import ToolMessagefrom langchain_core.tools import toolfrom langchain_openai import ChatOpenAIfrom config import DEEPSEEK_MODEL, JEV_MODEL, OPENROUTERclass 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)@tooldef 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_secondsdef 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 Routerrouter = 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 из за их скорости.

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

@ViacheslavVoo

ссылка на оригинал статьи https://habr.com/ru/articles/1088710/