System One Adapterを使ったJevとローカルモデルの分類タスク比較
前回の記事では、文章を渡すと型付きの判定と確率を返すJevというモデルを紹介しました。その後、TypeSafeに新しく登録しようとしたら、画面には「Sorry, new signups are paused」と表示されており、新規登録が止まっていました(9月25日時点)。Vercelによると、JevはAI Gatewayに登場してから24時間で、過去のどのモデルよりも多くの有料チームに使われたそうです。すごい。現在は新規登録できないため、本記事のサンプルを実行できるのは、TypeSafeの既存アカウントを持つ人だけです。
このビッグウェーブに乗って私も自分でJev互換のモデルを作ってみたいと思ったので、まずはJevのAPIと手元のモデルを同じ題材で比べる環境を作ることにしました。
そして、すでに独自のJevライクモデルを公開している人たちもいます。KevはQwenの公開重みを追加学習したモデルで、LayaはModernBERT系。Tev1もQwenを使った学習方法を公開しています。私もKevを日本語データセットで追加学習して精度の変化を見たり、麻雀の「何切る」問題で検証したりしました。そのうち、分類するならModernBERTをタスクに特化して学習した方がいいのではと思い、こちらも試しました。実際、学習データを増やしたModernBERTの方が、手元で追加学習したKevより打牌の一致率は高くなりました。
今回の記事では、まず比較の基準を作りました。我が家のゲーミングPCのGPUではGemmaが現実的な速度で動いたので、最初の相手はこれに決めました。QwenやBonsaiの軽量モデルも候補にありますが、Gemmaで動けば次はもっと高性能なモデルにも差し替えて試せるはずなので、まず試すモデルとしておすすめです。比較コードは公開しています。
しかし、Jevと普通のLLMでは呼び出し方が違います。そこで使えるのが、TypeSafeのSystem One Adapterです。Jevに渡す形式の設問を既存のLLMにも送れるので、Gemmaをファインチューニングしなくても結果を比較できます。

今回はLM Studioでgoogle/gemma-4-12b-qatを動かし、OpenAI互換APIで接続しました。System One Adapterには構造化出力を要求し、確率を答えさせる場合は値を正規化するよう設定しています。
各モデルに送るのは、Kevの日本語分類を評価するために私が自作した合成データセットです。問い合わせメールを意識した文面で、返品・配送・請求・その他の4分類に、直接表現・言い換え・否定・文脈の4種類を組み合わせ、各組から1件ずつ選びました。
stateには問い合わせ本文を入れます。たとえば、届いた額縁の交換依頼を送る場合、設問は次のように作ります。instructionsは何を判定するか、criteriaは4つの選択肢の説明です。以下は比較コードの呼び出し部分を抜粋。
from typesafe_sdk import Choice
question = dict(department=Choice(
instructions=INSTRUCTIONS,
criteria=CRITERIA,
))
jev_response = jev.system_one(state, question)
gemma_response = adapter.system_one(state, question, model=provider)
JevとGemmaに同じ本文と設問を渡し、Gemma側だけにLM Studioにつなぐproviderを指定します。上のコードは呼び出し部分の抜粋で、クライアントの初期化や16件のループは比較コード全文にあります。
選んだ16件をJevで1回、Gemmaで2回走らせました。GemmaではSystem One Adapterの設定を2パターン試しました。一つは4つの選択肢の確率を出し、最大のものを採用する「確率出力」。もう一つは確率を求めず、答えを一つだけ選ばせる「選択のみ」(discrete)です。正答数は、その答えが私の付けた分類ラベルと一致した件数です。
| 条件 | 正答 | 時間の中央値 |
|---|---|---|
| Jev(Choiceの確率出力) | 16/16 | 0.284秒 |
| Gemma(アダプターの確率出力) | 3/16 | 2.541秒 |
| Gemma(アダプターの選択のみ) | 7/16 | 1.117秒 |
Gemmaは、返品・配送・請求・その他の4分類のうち、確率まで求めた16件の半分にあたる8件で「その他」を選びました。選択のみの方が正答数は多いです。確率出力と選択のみの記録も公開しています。
環境と手順
Python 3.10以上、Jevを使えるTypeSafeの既存アカウント、google/gemma-4-12b-qatを導入したLM Studioが必要です。LM StudioのDeveloper画面でモデルを読み込み、APIサーバーを起動します。CLIならlms server startです。
git clone https://github.com/laiso/playground.git
cd playground/jev-adapter
python3 -m venv .venv
. .venv/bin/activate
python -m pip install 'system-one-adapter[openai]==0.2.1' 'typesafe-sdk==0.7.1'
export LM_STUDIO_BASE_URL=http://localhost:1234/v1 # 別マシンなら変更
curl -fsS "$LM_STUDIO_BASE_URL/models" # モデルIDと接続を確認
read -r -s -p 'TypeSafe API key: ' TYPESAFE_API_KEY; echo
export TYPESAFE_API_KEY
python compare.py --output results-rerun.jsonl
python compare.py --mode discrete --output results-discrete-rerun.jsonl
最初の実行でJevとGemmaの確率出力を、2回目でGemmaの選択のみを、それぞれ16件記録します。行数、正答数、時間の中央値は次のコマンドで確認できます。
wc -l results-rerun.jsonl results-discrete-rerun.jsonl
python - <<'PY'
import json
from pathlib import Path
from statistics import median
base = Path('.')
for filename, clients in (
('results-rerun.jsonl', ('jev', 'gemma_adapter')),
('results-discrete-rerun.jsonl', ('gemma_adapter',)),
):
rows = [json.loads(line) for line in (base / filename).read_text().splitlines()]
for client in clients:
valid = [row for row in rows if 'choice' in row[client]]
correct = sum(row[client]['choice'] == row['label'] for row in valid)
seconds = median(row[client]['wall_seconds'] for row in rows)
print(filename, client, f'{correct}/{len(rows)}', f'{seconds:.3f}秒')
PY
