LLMを実務で使っていると、ベンチマークのスコアだけでは判断しにくいことがある。
- プロンプトを変えたが、本当に品質は上がったのか
- 安いモデルに切り替えたいが、この用途では十分なのか
- ローカルモデルのJSON出力はどこまで信用できるのか
こういうときに欲しいのは、自分のテストケースを使って、同じ基準で比較できる仕組みだと思った。
そこで、別のLLMを審判役にして候補モデルの出力品質を評価するパイプラインを作った。
いわゆる LLM-as-a-Judge だが、実際に使いやすいように、評価の分離やバイアス対策を入れている。
GitHub:
https://github.com/archminor/llm-as-a-judge
全体像
テストケース(JSONL) + 設定(YAML)
│
▼
┌─────────────┐
│ Inference │ 候補モデルで推論実行
│ │ (フォーマット・スキーマの自動検証を含む)
└──────┬──────┘
▼
┌─────────────┐
│ Judge │ 別のLLMが9メトリクスで評価
│ │ (format / content / expression を分離)
└──────┬──────┘
▼
┌─────────────┐
│ Compare │ 集計・比較レポート生成
│ │ (JSON + Markdown)
└─────────────┘
セットアップして実行するだけなら、基本は3コマンドで動く。
uv sync
cp .env.example .env # APIキーを設定
uv run llm-judge run-all
設計のポイント
3層分離Judge
評価をフォーマット・内容・表現の3層に分け、それぞれ独立したLLM呼び出しで実行する。
| Layer | メトリクス | 狙い |
|---|---|---|
| Layer 1 | format_compliance, harmlessness | 形式と安全性 |
| Layer 2 | accuracy, faithfulness, completeness, relevance, reasoning, citation_quality | 内容の品質 |
| Layer 3 | expression_quality | 文章表現 |
1回のプロンプトで全部を評価させると、たとえばJSONが崩れている回答に対して内容まで引きずられたり、その逆が起きたりしやすい。
分けることで、どこに問題があるかを追いやすくした。
評価モード
| モード | 用途 |
|---|---|
| pairwise | 2モデルの出力をA/B比較する |
| absolute | 1モデルの出力を独立にスコアリングする |
| hybrid | 両方を実行する |
バイアス対策
- ブラインディング: 候補モデル名を隠し、提示順もランダム化
- Think-before-score: 根拠を先に書かせ、そのあとスコアを出させる
-
多数決集計:
judge_repeats: 3のように複数回評価して集計する
一貫性モード
inference_repeats >= 2 に設定すると、同じ入力に対する複数回の出力の安定性も評価できる。
単発では良く見えても、毎回かなり違う答えを返すモデルは運用で扱いづらいので、そこも見られるようにした。
設定ファイル
設定はYAML 1つでまとめるようにしている。
run_id: "eval-001"
dataset:
testcases_path: "data/eval/testcases.jsonl"
candidates:
- candidate_id: "gpt-oss-20b"
vendor: "lmstudio"
model_id: "gpt-oss-20b"
endpoint: "http://localhost:1234/v1"
generation_params:
temperature: 0
max_tokens: 1024
- candidate_id: "gpt-5.4-mini"
vendor: "openai"
model_id: "gpt-5.4-mini"
generation_params:
temperature: 0
max_completion_tokens: 1024
judges:
- judge_id: "gpt-5.4-judge"
vendor: "openai"
model_id: "gpt-5.4"
rubric_version: "v1"
protocol:
evaluation_mode: "hybrid"
blinding:
enabled: true
random_seed: 42
repeats:
judge_repeats: 3
metrics:
- format_compliance
- harmlessness
- accuracy
- faithfulness
- completeness
- relevance
aggregation:
method: "majority_vote" # mean | majority_vote | worst_case
候補モデルとJudgeモデルのベンダーは自由に組み合わせられる。
たとえば GPT 系をJudgeにしつつ、ローカルモデルとクラウドモデルを比較する、といった構成もそのまま書ける。
現時点では、OpenAI、Azure OpenAI、Gemini(ネイティブREST)、OpenAI互換エンドポイントに対応している。
テストケース
テストケースはJSONL形式で、チャット形式と自由形式の両方に対応している。
{"testcase_id": "tc-001", "task_type": "qa", "input": {"messages": [{"role": "user", "content": "What is the capital of France?"}]}}
{"testcase_id": "tc-002", "task_type": "summarization", "input": {"text": "..."}, "constraints": {"required_points": ["mention the fox"]}}
constraints で、必須ポイント・禁止ポイント・出力形式・引用ポリシーなども指定できる。
Judgeはそれらも踏まえて評価する。
ルーブリック
評価基準は Markdown ファイル(rubrics/v1.md)として分離している。
- 9メトリクスの定義
- 1 / 3 / 5 のスコア基準
- 層の割り当て
- バイアスガード
- クリティカルイシュー時のスコアキャップ
評価基準のカスタマイズはルーブリック編集でできるようにし、コード変更は不要にした。
出力
比較結果は JSON と Markdown で出力される。
comparison-report.json には、たとえば次の情報が入る。
- 勝率・敗率(pairwise)
- メトリクスごとの平均スコア
- 95%信頼区間
- クリティカルイシュー件数
- 一貫性スコア(一貫性モード時)
Markdownレポートも同時に生成されるので、共有やレビューに使いやすい。
以下はサンプル実行結果の Claude Code による可視化。
たとえば、あるタスクで高価なモデルAと安価なモデルBを比べた結果、内容面の差は小さいが format_compliance はBのほうが不安定、といった見え方ができる。
その場合、「通常用途はBで十分だが、厳密なJSON出力用途では追加検証が必要」といった判断がしやすくなる。
想定ユースケース
- プロンプト改善の効果測定
- モデル選定
- 回帰テスト
- ローカルモデルの品質検証
これは何ではないか
-
ベンチマークスイートではない
テストケースは自分で用意する前提 -
学習ツールではない
モデルを訓練するものではなく、出力を評価するもの -
エージェントフレームワークではない
バッチ評価パイプラインとして作っている
技術スタック
- Python >= 3.11
- Pydantic, Typer, Rich, httpx, tenacity
- パッケージマネージャ: uv
LLMの評価は、実際にやってみると「何をどう比べればよいか」が意外と難しい。
同じ条件で、ある程度再現可能に比較できる土台があるだけでも、モデル変更やプロンプト変更の判断はかなりしやすくなると思っている。
このパイプラインの実装と記事の執筆には Claude Code を使用した。
フィードバックや質問があればぜひ。