Jev в 25 строках на Python

· 2 мин чтения
jev llm inference classification python
Jev в 25 строках на Python

О модели Jev от компании Typesafe (System One Models) не говорит разве что ленивый: её называют новым рубежом больших языковых моделей и «новой парадигмой ИИ». Команда NobodyWho, разработчики open-source-движка для локального озвучивания игр, отнеслась к этому хайпу скептически и решила показать суть технологии наглядно. Их аргумент — рабочая реализация Jev в 25 строках Python на модели Qwen3-0.6B через llama-cpp-python.

Это, конечно, пародийный пост, но с серьёзным подтекстом: ядро «прорывной» технологии — обычная классификация через логиты следующего токена, то есть приём, знакомый любому, кто работал с LLM на низком уровне. Разберём этот код по шагам.

Что делает Jev на самом деле

Если отрезать маркетинг, Jev — это модель, которая получает промпт с набором вариантов и выдаёт вероятности для каждого из них. Не генерирует текст, а классифицирует: вход — письмо, выход — «легитимное / спам / фишинг» с распределением вероятностей.

Именно это и делает код ниже. Никакого вызова API, никакой синтетики для дообучения, никакого reinforcement learning для калибровки решений (RLCD) — только открытая модель с Hugging Face, локальный инференс и немного numpy.

Шаг 1: загружаем модель

# /// script
# requires-python = S0
# dependencies = [S1, S2, S3]
# ///

import numpy
from llama_cpp import Llama

# Really, you can use any GGUF model from https://huggingface.co/models?library=gguf

model = Llama.from_pretrained(
    repo_id="Qwen/Qwen3-0.6B-GGUF",
    filename="Qwen3-0.6B-Q8_0.gguf",
    n_ctx=512,
    logits_all=True,
    verbose=False,
)

Здесь три важных детали:

  • Скрипт оформлен по стандарту PEP 723 — метаданные зависимостей прямо в комментарии. Запускается одной командой через uv run.
  • Модель — крошечный Qwen3-0.6B в квантовании Q8_0. Подойдёт любая GGUF-модель, кстати.
  • logits_all=True — ключевой флаг. Он заставляет llama.cpp возвращать логиты для всех позиций, а не только сэмплировать следующий токен. Без этого трюк не сработает.

Шаг 2: промпт и варианты выбора

labels = ["A", "B", "C"]
choices = ["Legitimate", "Spam", "Phishing"]
email = "Payroll asks for your password on a non-company sign-in page."
options = "\n".join(
    f"{label}. {choice}" for label, choice in zip(labels, choices, strict=True)
)
prompt = f"""<|im_start|>system
Choose one option.<|im_end|>
<|im_start|>user
Email: {email}\n\n{options}<|im_end|>
<|im_start|>assistant
<think>\n\n</think>\n\n"""
model.eval(tokens=model.tokenize(text=prompt.encode(), add_bos=False, special=True))

Задача: классифицировать письмо как легитимное, спам или фишинг. Промпт собран в формате чата Qwen (im_start/im_end), а в конце добавлена пустая reasoning-обёртка <think>, чтобы модель сразу перешла к ответу.

Классический приём constrained classification: перечисляем варианты под метками A/B/C и дальше будем смотреть не на свободную генерацию, а только на вероятности токенов этих самых меток.

Шаг 3: от логитов к вероятностям

logits = model.scores[model.n_tokens - 1]
token_ids = [model.tokenize(text=label.encode(), add_bos=False)[0] for label in labels]
choice_logits = numpy.asarray([logits[token_id] for token_id in token_ids])
logprobs = choice_logits - numpy.logaddexp.reduce(choice_logits)
probabilities = numpy.exp(logprobs)

for name, scores in (
    ("Logits", choice_logits),
    ("Log probabilities", logprobs),
    ("Probabilities", probabilities),
):
    values = numpy.round(scores.astype(float), 3).tolist()
    print(f"{name}:", dict(zip(choices, values, strict=True)))

# Logits: {S4: 26.254, S5: 27.262, S6: 29.614}
# Log probabilities: {S7: -3.482, S8: -2.474, S9: -0.122}
# Probabilities: {S10: 0.031, S11: 0.084, S12: 0.885}

Вот здесь и живёт вся «магия» Jev:

  1. Берём логиты последней позиции последовательности — это распределение следующего токена до нормировки.
  2. Находим токен, соответствующий каждой метке (первый токен строк «A», «B», «C»).
  3. Выбираем логиты только этих трёх токенов и делаем log-softmax уже по ним: logits - logsumexp(logits).
  4. Возводим в экспоненту и получаем вероятности.

Для тестового письма модель с вероятностью 88,5% говорит «Phishing» — и она права: письмо просит пароль на не-корпоративной странице входа. Один прогон вперёд, ноль генерации текста, готовое калиброванное решение.

Почему это «тот самый» Jev

Авторы честно перечисляют, чего в их коде нет: названия «System One decision model», вызова API, синтетических данных и RLCD-калибровки. И так же честно перечисляют, что совпадает:

  • Это классификация: промпт с вариантами на входе, вероятности на выходе.
  • Это быстро — один forward-проход вместо авторегрессионной генерации.
  • Это локально — данные никуда не отправляются, что для классификации корпоративной переписки принципиально.

Стоит оговориться: в исходном анализе от Arcturus Labs отмечалось, что вероятности оригинального Jev тоже не всегда корректны — так что отсутствие RLCD-калибровки не делает пародийную версию сильно хуже оригинала.

Практический смысл

Приём, показанный в посте, сам по себе очень полезен — независимо от отношения к Jev:

  • Дешёвая классификация. Модель на 0,6B параметров в Q8 занимает меньше гигабайта. Для маршрутизации запросов, модерации или триажа тикетов этого часто достаточно.
  • Structured output без парсинга. Не нужно заставлять модель выдать JSON и надеяться, что она не сломает формат. Вы просто читаете вероятности из логитов.
  • Каскады. Дешёвая локальная модель фильтрует поток, а дорогая API-модель подключается только для спорных случаев с низкой уверенностью.
  • Приватность по умолчанию. Всё выполняется на своей машине: письма, тикеты и логи не покидают инфраструктуру. Для задач, где данные нельзя отдавать третьим сторонам, это часто решающий аргумент — именно его NobodyWho и подчёркивают.

Такой же механизм лежит в основе constraint-декодирования и методов вроде grammar-based sampling — просто там вероятности маскируются по правилам грамматики, а здесь берутся только токены-метки.

Подводные камни приёма

Если соберётесь использовать этот подход в продакшене, учтите несколько ограничений:

  • Метка должна быть одним токеном. Код берёт первый токен строки метки, поэтому работают короткие метки вроде «A», «B», «C» или «yes»/«no». Многословные варианты («Legitimate») в перечислении логитов не попадают целиком — выбирается только их первый токен, поэтому имена классов живут в списке choices, а в логитах участвуют лишь однобуквенные метки.
  • Нормировка по вариантам, а не по словарю. Log-softmax считается только по выбранным меткам, так что «вероятность» означает «доля уверенности среди этих трёх опций». Модель может считать все три варианта маловероятными на фоне остального словаря — вы этого не увидите.
  • Качество зависит от модели. 0,6B параметров хватает для тривиальной фильтрации, но на тонких задачах вероятности будут заметно шумнее, чем у старших моделей.

Все три ограничения лечатся: метки можно расширять до нескольких токенов (суммируя логиты), в нормировку можно включать «безопасный» вариант-выход, а модель — просто взять побольше. Архитектура приёма от этого не меняется.

Открытые реализации

Если тема зацепила, у пародии есть вполне серьёзные продолжения — авторы сами на них ссылаются:

Весь код из поста — это, по сути, готовый стартовый шаблон: зависимости через PEP 723, GGUF-модель с Hugging Face и двадцать пять строк на получение калиброванных вероятностей классов. Оригинал от NobodyWho — здесь, их open-source-проект NobodyWho — на GitHub.

Источник: https://www.nobodywho.ai/posts/jev-in-25-lines/