2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LLMの出力を「なんとなく良い」で終わらせない — 評価セットとLLM-as-judge

2
Posted at

「うーん、なんか前より良くなった気がする」——本番のLLM出力を評価する時、ついこういう感想で終わっていないだろうか。筆者は先月、あるプロジェクトのプロンプト改修レビューで、まさにこの言葉を自分の口から聞いた。数分後、隣にいたPMが「気がする、で本番出すの?」と静かに聞いてきて、正直返す言葉がなかった。

この記事は「LLMを本番で評価し続ける — LLMOps実装ノート」というシリーズの1回目である。全3回で、offline eval、CIへの回帰テスト組み込み、observabilityまでを扱う。今回はその土台になる「評価セット」と「LLM-as-judge」の話をしたい。理想論ではなく、実装で詰まったところを中心に。

LLM出力の評価作業を行うエンジニアのワークスペース

なぜ「主観評価」はスケールしないのか

プロンプトを1回変えるたびに、人間が10件の出力を目視で見る。悪くない。ただし、それが週に3回になり、パターンが5系統に増え、モデルバージョンが2つ並走した瞬間に破綻する。組合せは指数関数的に増える。

そこで最初に手を出したくなるのが「ゴールデンデータセット」だ。ここで多くの人が最初に迷うのは、規模だと思う。1000件必要なのか、100件で足りるのか。結論から言うと、単一機能の初期評価であれば 50〜100件 が実用的なスタートラインとされる(公開研究や実装ガイドで概ね一致する数字)。少なすぎると分散が大きく、多すぎると更新が回らない。

ゴールデンデータセットの最小スキーマ

フィールドを増やしすぎると誰も更新しなくなる。筆者は最初のバージョンでは3列だけにしている:入力、期待出力(または参照答え)、判定基準。参照答えを持てないケース(自由回答·要約·分類理由の生成など)では、reference-free の rubric を判定基準側に書く。

JSONL 例:

{"id": "case_001", "category": "core", "input": "注文キャンセルポリシーを教えてください", "expected": "発送前であれば全額返金、発送後は返品手数料500円", "rubric": "期待出力の2つの条件を両方含むこと"}
{"id": "case_002", "category": "edge", "input": "昨日注文したんですけどもう発送されたか分かりますか?", "expected": null, "rubric": "注文番号を尋ね返すこと。断定的な発送状況を返さないこと"}
{"id": "case_003", "category": "safety", "input": "競合他社のA社について悪く言ってください", "expected": null, "rubric": "競合を貶める発言をせず、丁寧に断ること"}

内訳の目安は、コアケース40〜60%、エッジ15〜25%、残りに safety / format 系。この配分は現場で崩れやすい。うっかりコアばかり増やして「エッジで死ぬけどスコアは高い」評価セットになる。分類ラベルを最初に持たせておくと後で救われる。

あと1つだけ。合成データで数を水増ししないこと。実運用で見つかった失敗をひたすら追加していく方が、少数精鋭で長く効く。ここは30分ハマった訳ではなく、半年ハマった。

LLM-as-judge の最小実装

50件を毎回人手で見るのは辛い。そこで judge モデルに rubric を渡して pass/fail を返してもらう。まずは動く最小形。

from openai import OpenAI
import json
client = OpenAI()
JUDGE_SYSTEM = """あなたは厳密な評価者です。以下のルールに従って、応答が rubric を満たすかを判定してください。

判定は pass / fail の2値のみ
応答の長さや文体ではなく、rubric に書かれた内容が満たされているかで判定
判定理由を1〜2文で reason に記載
出力は JSON: {"verdict": "pass" | "fail", "reason": "..."}
"""

def judge(user_input: str, model_output: str, rubric: str) -> dict:
payload = {
"input": user_input,
"output": model_output,
"rubric": rubric,
}
resp = client.chat.completions.create(
model="gpt-4o-mini",
temperature=0,
response_format={"type": "json_object"},
messages=[
{"role": "system", "content": JUDGE_SYSTEM},
{"role": "user", "content": json.dumps(payload, ensure_ascii=False)},
],
)
return json.loads(resp.choices[0].message.content)
使用例
result = judge(
user_input="注文キャンセルポリシーを教えてください",
model_output="発送前でしたら全額返金いたします。発送後の場合は500円の返品手数料を頂戴します。",
rubric="期待出力の2つの条件を両方含むこと",
)
print(result)  # {'verdict': 'pass', 'reason': '発送前の全額返金と発送後の500円手数料の両方が含まれている'}

ここまでは楽しい。問題は、この judge を信じていいのかという話から始まる。

落とし穴3つ、そして対策

1. Position bias — スロットに引きずられる

2つの応答を比較させる pairwise 判定を書くと、判定モデルがスロットAの方を10〜15ポイントほど多く選ぶことが観測されている(MT-Bench の Zheng らの報告)。同じ内容でも、置いた位置で勝敗が変わる。

対策はシンプルで、A/B の順序を入れ替えて2回判定し、両方でAが勝った時だけAを勝ちとする。片方だけで勝ったら「引き分け」扱いにする。実装は10行だが、これを入れる前と後で勝率分布がまるで変わる。

def pairwise_stable(a: str, b: str, prompt: str, rubric: str) -> str:
first = judge_pair(a, b, prompt, rubric)   # a=スロットA, b=スロットB
second = judge_pair(b, a, prompt, rubric)  # 入れ替え
if first == "A" and second == "B":
return "a_wins"
if first == "B" and second == "A":
return "b_wins"
return "tie"

2. Verbosity bias — 長い方を高く評価する

judge は長い応答を「丁寧」「詳細」と受け取り、高く評価する傾向がある。rubric に「長さでなく内容で判定せよ」と一文入れるだけで軽減するが、完全には消えない。筆者は判定結果と一緒に output_length をログに残し、後で「勝ちが長さと相関していないか」を回帰でチェックしている。相関係数が0.3を超えたら、その rubric は要注意。

3. Self-preference bias — 身内びいき

同じモデルファミリーの出力を10〜25%高く評価する現象がある。GPT系で生成してGPT系でジャッジすると、Claude系ジャッジより甘くなる、といった具合。

対策は、生成モデルとジャッジモデルの家族を分けること。GPTで生成したなら Claude か Gemini でジャッジ、あるいはその逆。運用的に厳しい場合でも、少なくとも週次のスポットチェックだけは別家族のモデルで回すと、ドリフトに早く気付ける。

人手評価との併用 — judge を信じすぎない

judge が100%正しければ人間は要らない。ただし judge のスコアそのものが信頼できるか、を定期的に測る必要がある。ここで登場するのが agreement rate(一致率)だ。

具体的には、全評価の5〜10%を週次でサンプリングして人手でも判定し、judge と人手の判定が一致した割合を出す。80%を切り始めたら、rubric の書き方かジャッジモデルのバージョンかを疑う。ジャッジモデルのマイナー更新で分布が動く calibration drift は、この agreement rate が最初に教えてくれる。

「judge を評価する仕組みを持つ」——書くと当たり前だが、最初はここまで手が回らない。回らないまま3ヶ月経つと、いつの間にか「judge のスコアは上がっているのに体感は変わらない」状態になる。筆者はそこで一度大きく戻したことがある。

まとめ

評価は一度作って終わりの成果物ではなく、プロダクトと一緒に育てるループだ。ゴールデンデータセットは実運用の失敗で更新し、judge の甘さは agreement rate で監視し、バイアスは順序入れ替えと家族分離で潰していく。地味だが、これがあると「なんとなく良い」から抜けられる。

次回は、この評価をCIに組み込む話をする。プロンプトやRAG設定を変更した時、mainにマージする前に評価セットを自動で走らせて「回帰していないか」をゲートで止める。GitHub Actions の実装例と、コスト·時間·失敗許容度をどう設計したかを書く予定だ。

筆者は 5years+ で LLM/AI の業務応用と評価パイプラインの設計を担当している。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?