はじめに
前回の記事では、TypeSafe 社の Jev を LLM ジャッジとして使えるかを、紛らわしい誤答72件で確かめました。
その記事の最後で触れたとおり、Databricks のブログで、Jev と同じ使い方ができるオープンなモデルを Databricks 上にデプロイし、SQL の ai_query から呼ぶ方法が紹介されています。
今回はこの手順でモデル (SemIf-OpenJev) をデプロイし、前回と同じ72件で LLM ジャッジとして試しました。
先に結論を書きます。
- ブログのノートブックを「すべて実行」するだけで、GPU のサービングエンドポイントまでできあがり、SQL の
ai_queryから呼べた - LLM ジャッジとしての一致率は81.9%で、Jev (88.9%) より低かった。見抜ける誤答と見逃す誤答の顔ぶれも Jev とは違った
- Jev と違って、見逃した誤答の確率が0.97まで上がることがあり、確率だけでは正しい回答と見分けにくかった
- 同じ入力なら、何度呼んでも確率はまったく同じだった
- 自分のワークスペースの中で動き、テーブルの行ごとに SQL から判定できる
そのまま Jev の代わりに LLM ジャッジとして使うより、分類のように選択肢がはっきりした判定を、自分のデータに対して大量に回す用途に向いていると感じました。
SemIf-OpenJev とは
SemIf-OpenJev は、Jev と同じ使い方ができるように作られたオープンソースのプロジェクトです。MIT ライセンスで公開されています。リポジトリには「Jev や TypeSafe とは無関係」と書かれており、TypeSafe の Jev そのものではありません。
中身は Qwen/Qwen3.5-4B です。モデルを再学習せずにそのまま使い、選択肢ごとの確率を1回の処理で返します。文章は生成しません。
入力の形は、Jev と少し違います。
| Jev (OpenRouter) | SemIf-OpenJev | |
|---|---|---|
| 判定対象 | state |
state |
| 質問 |
questions に複数書ける |
question に1つ |
| 答えの形 | yes/no、選択肢、段階評価の3種類 | 選択肢だけ (2〜16個)。yes/no も選択肢2つとして渡す |
| 返ってくるもの | yes の確率、または選択肢ごとの確率 | 選ばれた選択肢と、選択肢ごとの確率 |
Databricks にデプロイする
ブログのノートブックを実行する
ブログからリンクされているノートブックを、ワークスペースにインポートして実行しました。
ノートブックは次の順に進みます。
- SemIf のリポジトリと Qwen の重みをダウンロードする
- ホテルのレビュー6件で、ノートブック上でモデルを動かして確かめる
- MLflow に記録し、Unity Catalog にモデルを登録する
- GPU のサービングエンドポイントを作る
- エンドポイントを呼んで、同じレビューで確かめる
- SQL の
ai_queryからエンドポイントを呼ぶ
GPU は2か所で使います。
- ノートブックを動かすコンピュート: AI Runtime (サーバレス GPU) の A10 で、環境は AI v5 です。ノートブックの冒頭で、この条件を満たしていないと止まるようになっています
- サービングエンドポイント: AWS では A10G (
GPU_MEDIUM)、Azure では A100 (GPU_LARGE) です
AI Runtime は AWS と Azure で使えます。GCP では使えないので、このノートブックも動きません。
ウィジェットに4つの値を入れる
最初のセルを実行すると、ノートブックの上部にウィジェットが4つ出ます。
- カタログとスキーマ: モデルを登録する場所です。モデルを作成できる権限が必要です
- モデル名: Unity Catalog に登録するモデルの名前です
- エンドポイント名: 新しく作られるサービングエンドポイントの名前です
エンドポイント名には注意が必要です。ノートブックは、同じ名前のエンドポイントがすでにあると、新しく作らずにそのエンドポイントの設定を書き換えます。ワークスペースで使われていない名前を付けてください。
値を入れたら「すべて実行」です。
登録とエンドポイントの作成
モデルの登録では、env_pack="databricks_model_serving" を指定しています。登録するときに Python の環境と重みをまとめておく方法で、エンドポイントを作るときにコンテナを作り直さずに済みます。
version = mlflow.register_model(model_info.model_uri, UC_MODEL, env_pack="databricks_model_serving").version
エンドポイントは、GPU の Small サイズで作られます。使われないときは0台まで縮小する設定です。
ログには、次のようなメッセージが出ていました。どれも処理は止まらず、そのまま最後まで進みました。
- 重みを読み込むときの
'tuple' object has no attribute 'config' -
causal_conv1dが入っていないので、遅い代わりの処理で動くという警告 - Hugging Face の認証トークンが設定されていないという警告
ホテルレビューの判定
ノートブックの例題は、ホテルのレビューを good と bad に分けるものです。
| レビューの内容 | 判定 | good の確率 |
|---|---|---|
| 部屋も朝食も良く、また予約した | good | 1.00 |
| エアコンが3日壊れたまま | bad | 0.00 |
| 立地は良いが、壁が薄くてうるさい | bad | 0.16 |
| 「駐車場に40ドル払えて最高」という皮肉 | bad | 0.01 |
| 普通だが清潔で静か | good | 1.00 |
| チェックインで1時間待ったが、スイートに変更してもらえた | good | 0.98 |
皮肉を書いたレビューも、bad と判定しています。
SQL から呼ぶ
エンドポイントは、SQL の ai_query から呼べます。SemIf の入力は OpenAI 互換のチャット形式ではありませんが、ai_query はこうした独自の形式のエンドポイントにも、named_struct で組み立てた値をそのまま送れます。
SELECT
decision.choice AS verdict,
decision.options[0].probability AS p_good, -- 選択肢は送った順に返るので、0番目が good
review
FROM (
SELECT
review,
ai_query(
endpoint => 'taka-jev-endpoint',
request => named_struct(
'id', review_id,
'state', review,
'question', 'Was this a good or bad hotel stay?',
'options', array(
named_struct('id', 'good', 'description', 'The guest had a good stay and would recommend the hotel.'),
named_struct('id', 'bad', 'description', 'The guest had a bad stay and would not recommend the hotel.')
)
),
returnType => 'STRUCT<choice: STRING, options: ARRAY<STRUCT<probability: DOUBLE>>>'
) AS decision
FROM hotel_reviews
)
テーブルの行ごとに、判定と確率が列として返ってきます。結果は、Python からエンドポイントを呼んだ場合と同じでした。
前回の72件で LLM ジャッジとして試す
渡し方
前回の Jev の呼び出しに合わせて、次のように渡しました。
-
state: ドキュメントの抜粋、質問、回答をまとめた文 -
question: 前回と同じ評価基準の文 -
options:correctとincorrectの2つ。説明文は前回の Jev と同じ
CRITERION = (
"Is the candidate answer factually and technically correct for the question, "
"using the supplied documentation excerpt and respecting any version specified in the question?"
)
OPTIONS = [
{"id": "correct", "description": "The answer is correct and consistent with the documentation excerpt."},
{"id": "incorrect", "description": "The answer is wrong, contradicts the excerpt, or ignores a version constraint in the question."},
]
def make_request_row(r):
state = (
f"Documentation excerpt:\n{r['context']}\n\n"
f"Question:\n{r['question']}\n\n"
f"Candidate answer:\n{r['answer']}"
)
# id はリクエストの中で重複しないようにする
return {"id": f"{r['aid']}-{r['lang']}", "state": state, "question": CRITERION, "options": OPTIONS}
def query(rows):
body = {"dataframe_records": rows}
return w.api_client.do("POST", f"/serving-endpoints/{ENDPOINT}/invocations", body=body)["predictions"]
「正しい」の確率が0.5 以上なら「正しい」と判定します。前回と同じ基準です。
一致率
人手で付けた正誤と、判定が一致した件数です。Jev は前回の記事の2回分の結果です。
| 英語 | 日本語 | 全体 | |
|---|---|---|---|
| SemIf-OpenJev | 30/36 | 29/36 | 59/72 (81.9%) |
| Jev (1回目) | 32/36 | 32/36 | 64/72 (88.9%) |
| Jev (2回目) | 32/36 | 32/36 | 64/72 (88.9%) |
SemIf の間違いは13件で、Jev の8件より多くなりました。
見抜ける誤答と見逃す誤答の顔ぶれが違う
件数だけでなく、どの回答を間違えるかも Jev とは違いました。
Jev が見逃した誤答のうち、2件は SemIf が見抜きました。
| 回答 | SemIf | Jev (1回目 / 2回目) |
|---|---|---|
| MERGE の句の名前は正しいが、対象の説明が逆 (英語版) | 0.27 | 0.68 / 0.76 |
| シークレットの作成に必要な権限が1つ抜けている (日本語版) | 0.41 | 0.71 / 0.67 |
逆に、Jev が2回とも見抜いた誤答を、SemIf が見逃したものがありました。
| 回答 | SemIf | Jev (1回目 / 2回目) |
|---|---|---|
| 「マテリアライズドビューはパイプラインからしか作成できない」(英語版) | 0.85 | 0.09 / 0.09 |
| 同上 (日本語版) | 0.97 | 0.34 / 0.38 |
VERSION AS OF に日付を渡す (日本語版) |
0.88 | 0.08 / 0.09 |
スコアラーの引数名が expected_response になっている (英語版) |
0.56 | 0.13 / 0.15 |
| INSERT のほかの行は書き込まれるという補足 (日本語版) | 0.59 | 0.21 / 0.17 |
正しい回答を「誤り」と判定したものが1件ありました。 ボリュームのパスを聞く問題 (英語版) で、回答は /Volumes/main/sales/raw/report.csv というパスだけです。確率は0.15 でした。Jev は、正しい回答を「誤り」と判定したことは一度もありませんでした。
一方、次の誤答は、SemIf も Jev も見逃しています。
- 削除ベクトルで、後で実行するコマンドが違う回答 (英語版と日本語版)
- 権限が1つ抜けている回答 (英語版)
- CHECK 制約で、ほかの行は書き込まれるという補足 (英語版)
- タイムトラベルで、日付だけの文字列はエラーになるという補足 (日本語版)
どれも、回答の前半は正しく、後半や補足の一部だけが誤っているものです。前回の記事で Jev が見逃しやすいと書いた種類の誤答は、SemIf にとっても見逃しやすいようです。
確率では見分けにくい
前回の記事では、Jev が返した確率が次のように分かれていました。
- 正しい回答: 0.91以上
- 見逃した誤答: 0.5〜0.8
- 回答全体が誤っている誤答: 0.01〜0.04
見逃した誤答が「正しい」と判定されても、確率は正しい回答より明らかに低かったので、確率が中途半端な判定だけを LLM ジャッジに回せば見逃しを補えました。
SemIf では、確率が次のように分かれました。
| 回答の種類 | 件数 | 最小 | 中央値 | 最大 |
|---|---|---|---|---|
| 正しい回答 | 24 | 0.15 | 1.00 | 1.00 |
| 見抜いた誤答 | 36 | 0.00 | 0.03 | 0.47 |
| 見逃した誤答 | 12 | 0.53 | 0.81 | 0.97 |
見逃した誤答の確率が0.97まで上がり、正しい回答と重なっています。前回と同じく「確率が0.2〜0.8の判定だけを LLM ジャッジに回す」形で計算すると、13件の間違いのうち7件がこの範囲の外に残りました。前回、LLM ジャッジはこの72件をすべて正しく判定しているので、回した分は正しく直る前提での計算です。
ブログのノートブックにも、確率について次の注意書きがあります。
確率は、与えた選択肢の間での相対スコアであり、較正済みの信頼度ではありません。
しきい値で判定する前に、自分のデータで人手の正誤と照らし合わせ、temperature (確率に変換する前にスコアを割る値) を調整するように、と書かれています。今回は既定値の1.0 のままで試しています。
同じ入力なら、確率はまったく同じ
同じ入力で2回呼んだところ、72件すべてで確率が完全に一致しました。1件ずつ呼んでも、まとめて呼んでも、ai_query から呼んでも同じです。
Jev では、同じ入力でも呼ぶたびに確率が少し変わり、0.5付近の回答では判定が入れ替わることもありました。評価の結果を後から再現したい場合は、SemIf のほうが扱いやすいと思います。
時間
| 呼び方 | 1件あたりの時間 |
|---|---|
| 1件ずつ (中央値) | 約104ms |
| 36件ずつまとめて | 約72ms |
ai_query (72件で10.1秒) |
約141ms |
前回の Jev は、OpenRouter 経由で1件あたり約200ms でした。ただし、SemIf は自分専用の GPU エンドポイントで動いていて、Jev は外部の API です。条件が違うので、この数字だけでどちらが速いとは言えません。
料金の考え方も違います。Jev は呼び出した量に応じて料金がかかります。SemIf は、GPU のエンドポイントが動いている時間に応じて料金がかかります。エンドポイントは使われないと0台まで縮小しますが、縮小した後の最初の呼び出しは、GPU の起動に数分かかります。
まとめ
Databricks のブログの手順で SemIf-OpenJev をデプロイし、前回と同じ72件で LLM ジャッジとして試して、分かったことをまとめます。
- ブログのノートブックを「すべて実行」するだけで、GPU のサービングエンドポイントまでできあがる。必要なのは AI Runtime (A10) と GPU のサービングエンドポイント
- エンドポイント名に既存の名前を入れると、そのエンドポイントの設定が書き換わる
- SQL の
ai_queryから、テーブルの行ごとに判定と確率を取り出せた - LLM ジャッジとしての一致率は81.9%で、Jev (88.9%) より低かった
- Jev が見逃した誤答を見抜いたものが2件、Jev が見抜いた誤答を見逃したものが5件あった
- 回答の一部だけが誤っている誤答は、SemIf も Jev も見逃しやすかった
- 見逃した誤答の確率が0.97まで上がり、確率だけでは正しい回答と見分けにくかった
- 同じ入力なら、何度呼んでも確率はまったく同じだった
一番の収穫は、「Jev と同じ使い方ができる」ことと「Jev と同じように判定できる」ことは別だと分かったことでした。入力と出力の形がそろっていても、中身のモデルが違えば、どの回答を見逃すかも、確率の分かれ方も変わります。一方で、自分のワークスペースの中で動き、SQL からテーブルの行ごとに判定でき、結果が毎回同じ、という点は SemIf ならではです。LLM ジャッジとしてそのまま置き換えるより、分類のように選択肢がはっきりした判定を、自分のデータに対して大量に回す用途で試してみると良さそうです。どちらの用途でも、自分のデータの一部に人手で正誤を付けて、確率がどう分かれるかを先に確かめておくのがおすすめです。
参考リンク
- Jev は LLM ジャッジに使えるか、紛らわしい誤答72件で確かめた
- Running open-Jev in SQL on Databricks
- serve-semif-decision-models (ノートブック)
- SemIf-OpenJev (GitHub)
- AI Runtime
- モデルサービングエンドポイントの高速展開
- ai_query を使用する


