はじめに
2026年10月7日にリリースされた MLflow 3.17.0 で、TypeSafe の Jev を MLflow のジャッジのモデルとして指定できるようになりました。make_judge() の model に typesafe:/... と書くと、Jev の Yes/No 判定と分類の結果が、そのまま MLflow の評価結果として記録されます。
前回の記事では、Jev を呼ぶ処理を自分で書いて @scorer で包み、MLflow の評価に組み込んでいました。
今回は、その自前の処理が make_judge() の引数だけで済むようになったかを、Databricks のノートブックから OpenRouter のキーで試しました。最初は TypeSafe の API に送ろうとして認証エラーになったので、そのあたりも含めて書いていきます。
前回の記事から何が変わったか
前回は、次の処理をすべて自分で書いていました。
- Jev の API にリクエストを送る
- 返ってきた JSON から確率 (noul) を取り出す
- 確率が 0.5 以上かどうかで判定を決め、
Feedbackに詰める
3.17.0 からは、これを MLflow が引き受けます。
| 処理 | 前回 (@scorer で包む) |
今回 (make_judge()) |
|---|---|---|
| リクエストの組み立て | 自分で書く |
instructions と feedback_value_type から MLflow が組み立てる |
| 確率から判定への変換 | 自分で書く |
bool なら確率 0.5 以上で True
|
| 確率の記録 |
metadata に自分で入れる |
typesafe.probability などに自動で入る |
| 分類 (Choice) | 自分で書く |
Literal[...] を渡すだけ |
feedback_value_type に bool を渡すと Jev の Noul (Yes/No の確率) に、有限の Literal[...] を渡すと Choice (分類) になります。整数や自由記述の文字列は指定できません。Jev が返すのは型の決まった判定なので、当然といえば当然です。
OpenRouter のキーで呼ぶ
既定の送り先は TypeSafe の API
ここが最初のハマりどころです。
ドキュメントの例どおり model="typesafe:/jev-latest" と書くと、リクエストは TypeSafe の API (https://api.typesafe.ai/v1/systemone) に送られます。前回の記事で使っていた OpenRouter のキーを渡したところ、HTTP 401 になりました。
TypeSafe evaluation failed with HTTP 401. Check the endpoint configuration, credentials, and evaluation schema.
キーの中身を確かめると、sk-or- で始まる OpenRouter のキーでした。TypeSafe の API に OpenRouter のキーを送っていたので、認証が通らないのは当然です。
base_url で OpenRouter に向ける
make_judge() には送り先を変える base_url 引数があります。ドキュメントでは、System One 互換のホストに向けるための引数として説明されています。MLflow の AI Gateway が OpenRouter の Jev を呼ぶときの送り先をソースコードで確認すると、https://openrouter.ai/api/v1/systemone でした。そこで base_url に https://openrouter.ai/api/v1 を指定しました。MLflow は base_url の末尾に /systemone を付けて送ります。
整理すると、Jev をジャッジに使う経路は次の3つです。
| 経路 |
model の書き方 |
キー | 今回 |
|---|---|---|---|
| TypeSafe の API に直接送る | typesafe:/jev-latest |
TypeSafe のキー | 試していない |
base_url で OpenRouter に送る |
typesafe:/typesafe/jev-1.13 |
OpenRouter のキー | 動いた |
| MLflow の AI Gateway を経由する | gateway:/<エンドポイント名> |
Gateway に登録したキー | 試していない |
OpenRouter での使い方としてドキュメントに書かれているのは、3つ目の AI Gateway を経由する方法です。2つ目の base_url で OpenRouter に向ける書き方はドキュメントに例がないので、今回の実行で動いたという事実として読んでください。
MLflow の AI Gateway はオープンソースの MLflow サーバーの機能なので、使うにはローカルなどで MLflow サーバーを立てる必要があります。Databricks のノートブックだけで完結させたかったので、今回は2つ目の経路を選びました。
Databricks のノートブックで試す
セットアップ
MLflow 3.17.2 を入れます。
%pip install -q "mlflow[databricks]>=3.17.2"
%restart_python
OpenRouter のキーは Unity Catalog のシークレットに置いています。MLflow は環境変数 TYPESAFE_API_KEY の値を Bearer トークンとして送るので、OpenRouter のキーをこの名前で設定します。
import os
os.environ["TYPESAFE_API_KEY"] = dbutils.secrets.get(
catalog="workspace", schema="jev_judge", key="openrouter_api_key"
)
Unity Catalog のシークレットを dbutils で読むには、サーバーレスの環境バージョン 4 以上が必要です。
モデルと送り先を定義します。typesafe:/ の後ろが OpenRouter でのモデル名になります。
import json
from typing import Literal
import mlflow
from mlflow.genai.judges import make_judge
# typesafe:/ の後ろは OpenRouter のモデル名
MODEL = "typesafe:/typesafe/jev-1.13"
# MLflow が末尾に /systemone を付けて送る
BASE_URL = "https://openrouter.ai/api/v1"
Yes/No で判定するジャッジ
回答が質問に答えているかを判定するジャッジを作ります。feedback_value_type=bool を指定し、指示文は yes か no で答える問いにします。ドキュメントでも、bool のジャッジでは yes か no で答えるよう明記することが推奨されています。
answers_question = make_judge(
name="answers_question",
instructions=(
"{{ outputs }} は {{ inputs }} のユーザーの質問に直接答えていますか?"
"yes か no で答えてください。"
),
model=MODEL,
base_url=BASE_URL,
feedback_value_type=bool,
)
question = {"question": "パスワードを忘れた場合はどうすればいいですか?"}
candidates = [
"サインイン画面の「パスワードをお忘れですか?」を押すと、再設定用のリンクがメールで届きます。",
"パスワードは8文字以上で、英字と数字を組み合わせてください。",
]
for answer in candidates:
fb = answers_question(inputs=question, outputs=answer)
# 確率は文字列で入っている
print(f"{fb.value!s:5} 確率={fb.metadata['typesafe.probability']} {answer}")
True 確率=0.96 サインイン画面の「パスワードをお忘れですか?」を押すと、再設定用のリンクがメールで届きます。
False 確率=0.05 パスワードは8文字以上で、英字と数字を組み合わせてください。
再設定の手順を答えている回答は True、パスワードの条件にずれた回答は False になりました。前回の記事で自分で書いていた「確率を取り出して 0.5 で判定する」処理は、ここには1行もありません。
分類するジャッジ
次に、質問を問い合わせ区分に分類するジャッジを作ります。feedback_value_type に Literal で候補を並べます。日本語の候補もそのまま使えました。
request_category = make_judge(
name="request_category",
instructions="{{ inputs }} のユーザーの質問は、どの問い合わせ区分に当てはまりますか?",
model=MODEL,
base_url=BASE_URL,
feedback_value_type=Literal["アカウント", "請求", "その他"],
)
for q in [
"パスワードを忘れた場合はどうすればいいですか?",
"請求書の送付先メールアドレスを変更できますか?",
"オフィスの営業時間を教えてください。",
]:
fb = request_category(inputs={"question": q})
# 候補ごとの確率は JSON 文字列で入っている
probs = json.loads(fb.metadata["typesafe.probabilities"])
print(f"{fb.value} 確信度={fb.metadata['typesafe.confidence']} {probs} {q}")
アカウント 確信度=1.0 {'その他': 0.0, 'アカウント': 1.0, '請求': 0.0} パスワードを忘れた場合はどうすればいいですか?
請求 確信度=0.92 {'その他': 0.0, 'アカウント': 0.05, '請求': 0.95} 請求書の送付先メールアドレスを変更できますか?
その他 確信度=1.0 {'その他': 1.0, 'アカウント': 0.0, '請求': 0.0} オフィスの営業時間を教えてください。
3件とも期待どおりの区分になりました。分類のジャッジでは、選ばれた区分に加えて、候補ごとの確率 (typesafe.probabilities) と確信度 (typesafe.confidence) がメタデータに入ります。請求書の送付先の質問だけ「アカウント」に 0.05 が付いているのは、メールアドレスの変更というアカウント設定寄りの要素を含むためだと思われます。
評価に組み込む
mlflow.genai.evaluate に渡す
作った2つのジャッジは、そのまま mlflow.genai.evaluate() のスコアラーとして渡せます。データには、質問に答えている回答と、答えていない回答を混ぜました。
DATA = [
{
"inputs": {"question": "パスワードを忘れた場合はどうすればいいですか?"},
"outputs": "サインイン画面の「パスワードをお忘れですか?」を押すと、再設定用のリンクがメールで届きます。",
},
{
"inputs": {"question": "請求書の送付先メールアドレスを変更できますか?"},
"outputs": "はい。請求設定の画面で、請求書の送付先メールアドレスを編集できます。",
},
{
"inputs": {"question": "請求書の送付先メールアドレスを変更できますか?"},
"outputs": "プロフィール画面で表示名とアイコンを変更できます。",
},
{
"inputs": {"question": "オフィスの営業時間を教えてください。"},
"outputs": "営業時間は平日の9時から18時までです。",
},
]
results = mlflow.genai.evaluate(
data=DATA,
scorers=[answers_question, request_category],
)
結果は次のとおりです。
| 質問 | 回答 | answers_question | request_category |
|---|---|---|---|
| パスワードを忘れた場合はどうすればいいですか? | サインイン画面の「パスワードをお忘れですか?」を押すと、再設定用のリンクがメールで届きます。 | true | アカウント |
| 請求書の送付先メールアドレスを変更できますか? | はい。請求設定の画面で、請求書の送付先メールアドレスを編集できます。 | true | 請求 |
| 請求書の送付先メールアドレスを変更できますか? | プロフィール画面で表示名とアイコンを変更できます。 | false | 請求 |
| オフィスの営業時間を教えてください。 | 営業時間は平日の9時から18時までです。 | true | その他 |
質問とずれた3件目だけが false になりました。
4件目の営業時間の回答は、実際の営業時間が何時までかに関係なく true です。このジャッジは「質問に答えているか」を聞いているので、回答の内容が正しいかは判定の対象外です。内容の正しさまで見たい場合は、正解を expectations に入れて、それと照らし合わせる別のジャッジを作ることになります。
評価ランは、ノートブックに紐づくエクスペリメントに記録されます。
確率はメタデータに残る
Jev のジャッジは、LLM のジャッジのような判定理由の文章を返しません。その代わりに、確率がフィードバックのメタデータに残ります。評価ランのトレースから取り出せます。
import pandas as pd
traces = mlflow.search_traces(
run_id=results.run_id, order_by=["timestamp_ms ASC"], return_type="list"
)
rows = []
for trace in traces:
# 1つのトレースにジャッジごとのフィードバックが付いている
fb = {a.name: a for a in trace.info.assessments}
rows.append({
"question": json.loads(trace.info.request_preview)["question"],
"answers_question": fb["answers_question"].value,
"answers_question の確率": float(fb["answers_question"].metadata["typesafe.probability"]),
"request_category": fb["request_category"].value,
"request_category の確率": json.loads(fb["request_category"].metadata["typesafe.probabilities"]),
})
display(pd.DataFrame(rows))
| 質問 | answers_question | 確率 | request_category | 請求の確率 |
|---|---|---|---|---|
| パスワードを忘れた場合はどうすればいいですか? | true | 0.96 | アカウント | 0.0 |
| 請求書の送付先メールアドレスを変更できますか? (2件目) | true | 0.97 | 請求 | 0.94 |
| 請求書の送付先メールアドレスを変更できますか? (3件目) | false | 0.03 | 請求 | 0.94 |
| オフィスの営業時間を教えてください。 | true | 0.95 | その他 | 0.0 |
前回の記事では、確率が 0.5 付近の判定だけを LLM のジャッジに回す使い方を紹介しました。確率がメタデータに入っているので、その振り分けも評価結果から直接できます。
組み込みスコアラーは今回は試していない
3.17.0 では、Correctness などの組み込みスコアラーの model にも typesafe:/... を指定できるようになっています。ただし組み込みスコアラーには base_url を渡す引数がないので、typesafe:/... を指定すると TypeSafe の API に送られます。
OpenRouter のキーで組み込みスコアラーを使うには、MLflow の AI Gateway を経由することになります。Databricks のノートブックだけで完結する範囲から外れるので、今回は試していません。
まとめ
MLflow 3.17 で Jev を make_judge() のモデルに指定して、分かったことをまとめます。
-
make_judge()のmodelにtypesafe:/...を指定すると、Jev の判定がそのまま MLflow の評価結果になる。前回の記事で@scorerの中に書いていた、API の呼び出しと確率から判定への変換は不要になった -
feedback_value_type=boolで Yes/No 判定 (Noul)、Literal[...]で分類 (Choice) になる。日本語の指示文と日本語の分類候補で期待どおりの判定が出た - 既定の送り先は TypeSafe の API。OpenRouter のキーを渡すと 401 になる
-
base_url="https://openrouter.ai/api/v1"とmodel="typesafe:/typesafe/jev-1.13"で、Databricks のノートブックから OpenRouter 経由で動いた。 この書き方はドキュメントに例がない - 判定理由の文章は返らない。確率は
typesafe.probability(Yes/No) とtypesafe.probabilities(分類) としてメタデータに残る - 組み込みスコアラーには
base_urlの引数がないので、OpenRouter のキーで使うには AI Gateway を経由する
一番の収穫は、前回の記事で書いた自前のスコアラーが、make_judge() の呼び出し1つで置き換えられたことでした。一方で、どのキーでどこに送るかは自分で意識しておく必要があります。ドキュメントの例をそのまま動かして 401 が出たら、まずキーと送り先の組み合わせを確かめるのが近道です。
参考リンク
- MLflow 3.17.0 リリースノート
- TypeSafe Judge Models (MLflow ドキュメント)
- Unity Catalog のシークレット
- Jev は LLM ジャッジに使えるか、紛らわしい誤答72件で確かめた


