0
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アプリ」を先に作る — Eval-Driven Developmentで“感想レビュー”を卒業する実践ガイド

0
Posted at

「動くLLMアプリ」より「測れるLLMアプリ」を先に作る — Eval-Driven Developmentで“感想レビュー”を卒業する実践ガイド

はじめに — 「動いてるっぽい」でリリースしてる人へ

正直に言いましょう。

LLMを使った機能、リリース前のチェックって、けっこう 眺めてヨシ で済ませてないですか。

プロンプトを変えて、何回か叩いて、 ** 「いい感じやな…」 ** と思ったら、そのままmainにマージ。
これ、英語圏では Vibe Check(バイブ・チェック) って呼ばれてて、要は「雰囲気で良しと判断する」やり方なんです。

で、これがめっちゃ怖いのは、 ** 自分は気づかへんけど、ある日急に壊れる ** ってところ。
プロンプトをちょっと直しただけで、別のケースが静かに退行(regression)しても、誰も気づかへん。
本番でユーザーに変な答えを返した瞬間に、初めて気づく。

この記事は、その状態から 「測れるLLMアプリ」 に引っ越すための実践ガイドです。
方法論の名前は ** Eval-Driven Development(評価駆動開発、以下 EDD) ** 。
2026年現在、Anthropicの公式エンジニアリングブログでも、arxiv論文 2411.13768 でも、AIプロダクトを本気で運用するチームの 共通言語 になりつつあるやり方です。

なんかこれって、AI時代の TDD(テスト駆動開発)の親戚 みたいな考え方なんですよね。
でもLLMは「正解が1つに決まらん」「同じ入力でも出力がブレる」っていう、従来のテストと相性が悪い世界。
そこをどう乗り越えるか、最小実装のコードまで一気通貫で見ていきます。


Eval-Driven Developmentとは何か(無知の無知向け)

まず用語をめっちゃ噛み砕きます。

  • ** Eval(エバル) ** = evaluation の略。 「この出力、ちゃんと良いの?」を自動で測る仕組み のこと。普通のソフトのテストに相当します。
  • ** Eval-Driven Development ** = 「先に評価ハーネス(測る装置)を作ってから、プロンプトや実装に手を入れる」 という開発フロー。
  • ** Vibe Check ** = 雰囲気判定。卒業したい敵。
  • ** Golden Set(ゴールデンセット) ** = 「これは正解/不正解が人間視点で決まってる」サンプル集。だいたい20〜200件から始めます。
  • ** LLM-as-judge ** = 別のLLMに 「この答えは仕様に合ってる?」と採点させるやり方 。人間レビューを部分的にスケールさせる手段。
  • ** ルーブリック ** = 学校の成績表でいう「採点基準表」。LLM-as-judgeが採点するときの物差し。

EDDの原則を1行で言うと、こうです。

** 評価ハーネスを最初に作る。プロンプトはそれを通すための手段。 **

順番がめっちゃ大事で、 「実装してから後付けでテスト」じゃなくて、「測る装置を先に作って、その上でプロンプトを動かす」 という方向。
ちょうどTDDの Red → Green → Refactor に近いノリで、 ** Eval Fail → Eval Pass → Refactor ** と回すイメージです。

なんでこの順番が大事かというと、 ** プロンプトを良くする全ての作業が「比較可能な実験」になる ** からです。
今日のプロンプトAと、明日のプロンプトBは、 ** 同じゴールデンセットに対して何点取ったか ** で比較できる。
これがVibe Checkとの決定的な違い。


なぜ普通のテストだと書けないのか — 非決定性という壁

ここで一回、 ** なぜ既存のpytestやVitestのassertEqualで済まないのか ** を整理しておきます。
3つの壁があります。

** 1つめ:出力がブレる。 **
同じプロンプトでも、temperatureが0でも、モデルのバージョンが変わると言い回しが変わる。 ** exact matchは即座に死にます ** 。

** 2つめ:正解が1つに決まらん。 **
「ユーザーの質問に丁寧に答えて」みたいな仕様だと、正解は無数にある。 ** どれが正解かは、文字列一致では判定できない ** 。

** 3つめ:失敗の種類が多すぎる。 **
ハルシネーション、フォーマット崩れ、トーン違反、PII漏洩、トピック逸脱…。 ** 「合ってるかどうか」を1つの式で書けない ** 。

なので、テストを 「1つの基準で合否判定する」のではなく、複数の評価器を組み合わせる 方向に切り替えます。
それが次の章の 「3階層の評価」 です。


評価の3階層 — 決定的チェック / LLM-as-judge / 人間サンプリング

EDDで一番大事な設計判断は、 「どの観点を、どの方法で測るか」 の振り分けです。
2026のベストプラクティスは、ざっくり3階層に分けます。

階層 方法 何を測るか コスト 安定性
1. 決定的チェック 正規表現・JSON schema・型チェック・禁止語リスト 形・必須要素・PII混入・出力サイズ 激安 高い
2. LLM-as-judge 別モデルにルーブリックで採点させる 意味の正しさ・トーン・指示追従・忠実性 校正次第
3. 人間サンプリング 人間が定期的に少量を採点 ジャッジの校正・新しい失敗モードの発見 ゴールド

設計の鉄則はこれ。

** 決定的チェックで済むものをLLM-as-judgeに投げない。 **

なんでかというと、 「JSONがvalidか?」を別のLLMに聞くのは、コスト高くて、しかも不安定 やからです。
JSON validは json.loads() で1ミリ秒で終わる。
LLM-as-judgeは、 「決定的に書けないこと」だけ に温存する。これが鉄則。


最小ハーネス実装(Python)— 20件のゴールデンセットから始める

ここからコードです。
最小のEval Harnessを、Pythonで書いてみます。

まず ** 20件のゴールデンセット ** をJSONLで作るところから始めましょう。
(後でCSVやDBに移してもいいですが、最初はJSONLが一番楽です)

{"id": "001", "input": "返金ポリシーは?", "expect_topic": "refund", "must_include": ["7日以内"], "must_not_include": ["保証なし"]}
{"id": "002", "input": "営業時間を教えて", "expect_topic": "hours", "must_include": ["10:00", "19:00"], "must_not_include": []}
{"id": "003", "input": "メールアドレスを教えて", "expect_topic": "contact", "must_include": ["support@example.com"], "must_not_include": []}

そして、評価ハーネス本体。
ここでは ** 決定的チェックの最小セット ** を作っておきます。

import json
import re
from dataclasses import dataclass
from typing import Callable

@dataclass
class EvalResult:
    case_id: str
    passed: bool
    score: float
    reasons: list[str]

def deterministic_checks(output: str, case: dict) -> EvalResult:
    reasons = []
    score = 1.0

    # 必須要素チェック
    for token in case.get("must_include", []):
        if token not in output:
            reasons.append(f"missing required token: {token}")
            score -= 0.4

    # 禁止語チェック
    for token in case.get("must_not_include", []):
        if token in output:
            reasons.append(f"forbidden token present: {token}")
            score -= 0.5

    # PII混入の最低限チェック(電話番号っぽいもの・メールっぽいもの)
    pii_patterns = [r"\d{3}-\d{4}-\d{4}", r"\b[\w.+-]+@[\w-]+\.[\w.-]+"]
    allowed_email = "support@example.com"
    for pat in pii_patterns:
        for m in re.findall(pat, output):
            if m != allowed_email:
                reasons.append(f"unexpected PII-like token: {m}")
                score -= 0.3

    score = max(0.0, score)
    return EvalResult(case["id"], score >= 0.7, score, reasons)

これだけで、 「想定キーワードが入ってないPR」「禁止語が混ざったPR」「予期しないメールアドレスが返るPR」 が即座に検出できるようになります。

ポイントは ** 「最初から完璧を目指さない」 ** こと。
20件・5観点・正規表現5本で十分です。
動かしながら、漏れた失敗ケースを少しずつゴールデンセットに足していく。これがEDDの基本姿勢。


ルーブリック設計とLLM-as-judge(コード)

決定的チェックでは捕まえられない観点、たとえば 「丁寧な口調になっているか」「ハルシネーションせず提供情報の範囲内で答えているか」 を測るのがLLM-as-judgeの仕事です。

ルーブリックは ** YAML / Markdownで外出し ** にして、 ** プロンプト本体と分離して管理する ** のが鉄則。

# rubric/customer_support_v1.yaml
name: customer_support_v1
version: 1.0.0
dimensions:
  - name: faithfulness
    description: 提供されたコンテキストに含まれない事実を捏造していないか
    levels:
      "2": コンテキストの事実のみを使い、捏造ゼロ
      "1": 軽微な言い換えはあるが事実誤認なし
      "0": コンテキストにない情報を断定的に述べている
  - name: tone
    description: 丁寧かつ落ち着いた接客トーンか
    levels:
      "2": 一貫して丁寧・冷静
      "1": 部分的に砕けすぎ/硬すぎ
      "0": 失礼・煽り・命令調が混ざっている
  - name: instruction_following
    description: 質問に直接答え、聞かれてないことを増やしていないか
    levels:
      "2": 質問に直接回答、過不足なし
      "1": 余談あり/軽微な漏れ
      "0": 質問に答えていない/脱線

そしてLLM-as-judgeのコード。
ここで大事なのは、 「ジャッジには出力フォーマットを構造化出力で固定する」 ことです。

from anthropic import Anthropic

client = Anthropic()

JUDGE_SYSTEM = """あなたは厳格な評価者です。
与えられたルーブリックの各次元に 0 / 1 / 2 で点数を付けます。
迷ったら厳しい方の点数を付けます。
JSON以外は出力しません。"""

def llm_judge(rubric_yaml: str, user_input: str, model_output: str, context: str) -> dict:
    user = f"""# Rubric
{rubric_yaml}

# User Input
{user_input}

# Provided Context
{context}

# Model Output (this is what you grade)
{model_output}

Return JSON:
{{"faithfulness": 0|1|2, "tone": 0|1|2, "instruction_following": 0|1|2, "reasons": {{"faithfulness": "...", "tone": "...", "instruction_following": "..."}}}}"""

    resp = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=512,
        system=JUDGE_SYSTEM,
        messages=[{"role": "user", "content": user}],
    )
    raw = resp.content[0].text
    return json.loads(raw)

ジャッジを設計するときのコツは、 ** たった3つ ** だけ覚えれば大丈夫です。

  1. ** 次元(dimension)を3〜5個に絞る ** 。多いとブレる
  2. ** 0/1/2の3値スケール ** 。1〜5やリッカートは校正困難
  3. ** 「迷ったら厳しい方」を明示 ** 。寛容バイアスを潰す

ジャッジの校正 — Cohen's κと人間サンプリング

ここがEDDで一番手を抜いちゃいけないところです。

** LLM-as-judgeは、校正していないと使い物になりません ** 。
理由は3つ。

  • ジャッジモデルが ** 自分自身を採点すると甘くなる ** (self-preference bias)
  • ジャッジは ** トークン数が多い回答に高得点を付けがち ** (length bias)
  • ジャッジモデルを変えると ** 同じルーブリックでもスコアが2割変わる ** ことがある

なので、 ** 定期的に人間と突き合わせて、一致度を測る ** 必要があります。
測定指標としてよく使われるのが ** Cohen's κ(カッパ係数) ** 。 ** 偶然の一致を除いた一致率 ** で、0.6以上あれば実用、0.8以上あれば優秀、と言われます。

from sklearn.metrics import cohen_kappa_score

def calibrate_judge(human_scores: list[int], judge_scores: list[int]) -> float:
    """同じ20件サンプルに対する人間とジャッジの点数を比べてκを返す"""
    return cohen_kappa_score(human_scores, judge_scores)

# 例: 1週間に1度、ランダムサンプリング20件を人間が採点して比較
kappa = calibrate_judge(human, judge)
if kappa < 0.6:
    raise SystemExit(f"judge calibration failed: κ={kappa:.2f}. Rubricを書き直すか、ジャッジモデルを変えてください")

運用ルールはこんな感じが現実的です。

  • 毎週20件ランダムサンプリングして人間が採点
  • κ < 0.6 ならジャッジは ** 使用停止 ** 、ルーブリックを書き直す
  • ジャッジモデルを変更したら ** その日のうちに再校正 **
  • ** ジャッジに評価対象と同じモデルを使わない ** (self-preference bias回避)

ここを怠ると、 ** 「測れてる気がするだけのEDD」 ** になってしまいます。
評価ハーネス自体の品質保証、これがEDDの本丸です。


CIゲートにする — GitHub Actionsで回帰検出

評価が手元で回るようになったら、次は ** プロンプトを変えるPRごとに自動で走らせる ** 段階です。
これでようやく 「Vibe Checkからの脱出」 が完成します。

# .github/workflows/eval.yml
name: eval-gate
on:
  pull_request:
    paths:
      - "prompts/**"
      - "src/llm/**"
      - "rubric/**"

jobs:
  eval:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: "3.12" }
      - run: pip install -r requirements.txt
      - name: Run deterministic checks
        run: python -m evals.run --suite deterministic --fail-under 0.95
      - name: Run LLM-as-judge
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: python -m evals.run --suite judge --fail-under 0.85
      - name: Upload report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: eval-report
          path: out/eval-report.html

Pass ラインの引き方は、 ** いきなり高くしない ** のが鉄則。

段階 決定的チェック LLM-as-judge 備考
導入1週間目 0.80 0.70 落ちる箇所を学ぶ期間
1ヶ月目 0.90 0.80 ベースライン確立
安定運用 0.95+ 0.85+ 回帰検出専用

最初から ** 1.00を要求すると、誰もマージできなくなって形骸化 ** します。
「壊れた時に必ず落ちる」「正常時はほぼ通る」のバランスが、ゲートとして機能する閾値です。


落とし穴と撤退基準

EDDを導入するときによくハマるところを、先に潰しておきます。

** 1. ジャッジを校正しない **
→ 評価ハーネス自体が信用できない。 ** κ < 0.6 のジャッジは即廃棄 ** 。

** 2. 評価対象と同じモデルでジャッジさせる **
→ self-preference biasで自分に甘くなる。 ** 別モデル系列のジャッジを使う ** 。Sonnetを評価するならGPT、その逆、など。

** 3. ゴールデンセットを増やしすぎる **
→ コストが膨らみPR時間が延びてCIゲートが形骸化。 ** Tier分け ** (PRはfast 50件、nightlyはfull 500件)が現実解。

** 4. ジャッジに長いほうを高評価される **
→ 回答長を分位数で正規化するか、 ** 同じ長さに揃えてからジャッジに投げる ** 。

** 5. PII / 個人情報をジャッジに送る **
→ 評価データは ** 必ずマスキング ** 。本物のメールアドレスや電話番号をjudgeに渡さない。

** 6. 「ジャッジが言うから正解」病 **
→ ジャッジは ** 人間の補助輪 ** 。月1で人間がランダム5件を最終裁定する場を残す。

撤退基準もはっきり書いておきます。 ** これに該当したら一旦CIゲートを外してでも見直す ** べきタイミング。

  • κが2週連続で0.6を下回った
  • 「Pass率は高いが本番事故が増えている」状態
  • ゴールデンセットの正解が古くなって誰も更新してない

EDDは ** 評価ハーネスをメンテし続ける覚悟 ** とセットでようやく機能します。
逆に言うと、 ** ハーネスを育てる勇気さえあれば、AI機能の品質は静かに底上げされていきます ** 。


人間とAIの役割分担

ここをぼんやりさせると、「全部AIに任せて結局事故る」パターンに戻ります。

工程 人間がやる AIに任せる
ゴールデンセット作成 ◎ 正解判定の最終裁定 ○ 入力候補の生成
ルーブリック設計 ◎ 何を測るか ○ 採点理由文の生成
ジャッジ採点 △ サンプリング校正 ◎ 通常採点
校正(κ計算) ◎ 解釈と撤退判断 ○ 計算
プロンプト改善 ○ 設計判断 ◎ 候補生成
CIゲートのPassライン引き ◎ ビジネス判断 × 任せない

ポイントは ** 「何を測るか」「測れない結果をどう判断するか」 ** は最後まで人間が握ること。
AIは ** 「測る作業の高速化」「採点理由の言語化」 ** に張り付かせる。これが2026の現実解です。


プロンプト例3本

プロンプト1: ゴールデンセット候補を生成する

あなたはLLMアプリのテスト設計者です。
以下の機能仕様と禁止事項を踏まえ、
ゴールデンセット候補を20件、JSONLで出力してください。

# 機能仕様
{spec}

# 禁止事項
{forbidden}

# 出力フォーマット
{"id": "###", "input": "...", "expect_topic": "...", "must_include": [...], "must_not_include": [...]}

# 設計原則
- 通常ケース10件、境界ケース5件、敵対的入力5件のバランス
- must_includeは曖昧な表現を避け、固有名詞や日付などの検証可能な語に限定
- 同じパターンの繰り返しは避け、観点を最大化
最後に「観点カバレッジ」セクションで、どの失敗モードを狙ったかを箇条書き

プロンプト2: ルーブリックを設計レビューする

以下のルーブリックを、評価設計の専門家としてレビューしてください。

# Rubric
{rubric_yaml}

# レビュー観点
- 次元同士が独立しているか(独立してないと校正困難)
- 各レベルの境界が客観的に判定可能か
- 長さバイアスや言い回しバイアスを誘発しないか
- 評価対象と同じモデルにジャッジさせて自己採点バイアスが入らないか

# 出力フォーマット
- 致命的な問題
- 推奨修正(差分つき)
- 校正運用上の注意(κ目標値、再校正頻度)
最後に「採用可否(adopt / fix / reject)」を1行で。

プロンプト3: CI Failレポートを要約して原因仮説を出す

以下のEval CI失敗レポートを読んで、
失敗の塊(cluster)と最も可能性が高い原因仮説、
次に試すべきプロンプト変更案を出してください。

# Eval Report
{eval_report_json}

# 観点
- 同じ次元(faithfulness / tone / instruction_following)で連続して落ちているか
- 同じ入力カテゴリ(refund / hours / contact)に偏っているか
- 直近のプロンプト変更履歴のどの差分と相関するか

# 出力フォーマット
- 失敗クラスタの要約(最大3つ)
- 各クラスタの仮説と根拠(証拠引用つき)
- 提案するプロンプト差分(before/after)
- ロールバック推奨かどうかの判断
最後に「リスクの最も低い次の1手」を1行で。

今日からの最初の一歩(4ステップ)

順番が大事です。

  1. ** ゴールデンセットを20件、JSONLで書く ** 。完璧じゃなくていい。本番ログから10件、想定する敵対入力10件
  2. ** 決定的チェック5本だけ書く ** 。must_include / must_not_include / JSON valid / 文字数上限 / PIIパターン
  3. ** LLM-as-judgeを1次元だけ作る ** 。最初はfaithfulnessだけでOK
  4. ** 人間1人で20件採点してκを測る ** 。0.6を超えるルーブリックに育てる

ここまでで、 ** プロンプトを変えた時に「良くなったのか悪くなったのか」を数字で言える状態 ** になります。
これが、 ** EDDの最小構成 ** です。

CIゲート化は ** その次の週 ** でいい。
焦らず、まず手元で「測れる」状態を作るのが先決です。


まとめ — 測れるは資産、明日の自分にあざっす

LLMアプリの世界では、 ** 「動く」ことより「測れる」ことのほうが、長期では何倍も価値が高い ** んです。
動くプロンプトは1日で陳腐化するけど、 ** ちゃんと校正されたゴールデンセットとルーブリックは、半年後も自分とチームを守ってくれる ** 。

これって、未来の自分への保険であり、未来の自分への ** 「あざっす」 ** なんですよね。
今日ゴールデンセット20件を書いた自分に、3ヶ月後の自分は確実に感謝します。 ** プロンプトを変える勇気が出る ** からです。

そして、評価ハーネスは ** Code as Capital(コードは資本) ** の典型例です。
1度書いたら、回数を重ねるたびに ** 静かに価値を積み上げてくれる ** 。
プロンプトは消耗品、評価ハーネスは資本。この区別を持てるかどうかで、AI機能の運用ロードマップは全然違う景色になります。

なんかこれって、 ** 「動くLLMを作るエンジニア」から「測れるLLMを設計するエンジニア」 ** への、静かなアップグレードな気がします。
明日からの最初の一歩、 ** 20件のJSONL ** から始めてみてください。

未来の自分が、きっとあざっすって言ってくれます。


** 参考 **

  • Anthropic Engineering — Demystifying evals for AI agents
  • arxiv 2411.13768 — Evaluation-Driven Development of LLM Agents
  • DeepEval / RAGAS / Promptfoo / Braintrust / Langfuse (主要OSS/PaaS)
0
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
0
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?