LLMアプリの評価とテスト

LLMアプリの評価とテストとは

[[プロンプトエンジニアリング]] では「プロンプトはコードの一部だからバージョン管理する」と述べました。では、そのプロンプトを1行直したとき、アプリが壊れていないことをどうやって確認するのでしょうか。

普通の機能なら [[自動テスト]] を走らせれば済みます。しかし LLM を組み込んだ機能は、同じ入力でも出力の文言が毎回変わります(なぜ揺れるかは [[推論パラメータと出力のばらつき]])。expect(output).toBe('返品は14日以内です') は最初から成立しません。

そこで使うのが評価(eval)です。期待入力と合格条件を並べたデータセットを用意し、変更のたびに一括で流して合格率を見ます。1回で満点を狙う道具ではなく、変更の前後で合格率が下がっていないかを見る回帰検知の道具として使うのがコツです。

通常の自動テストとの違い

通常の自動テストLLMアプリの評価
期待値完全一致条件を満たすかどうか
結果pass / fail合格率(例: 48/50 = 96%)
再実行何度やっても同じ実行ごとに数%揺れる
実行時間数秒数分〜数十分(API呼び出し)
費用ほぼ01回あたり数十円〜数百円

決定的に違うのはしきい値で判定することです。「95%以上なら合格」と決め、94%に落ちたら調べる。1件でも落ちたら赤にすると、揺れのせいで毎回赤になり、やがて誰も見なくなります。

評価セット(ゴールデンデータ)を作る

評価セットは「入力」と「合格条件」の対を並べたファイルです。正解の文章を1つ書くのではなく、外してはいけない条件を書くのが要点です。

- id: refund-policy-01
  input: 返品はいつまでできますか?
  must_include: ['14日']
  must_not_include: ['返品不可', '当社では承っておりません']
  max_chars: 200

作り方の現実解は次の4つです。

  • 本番の失敗から作る — [[ハルシネーション]] を起こした実例やユーザーの苦情を、そのまま1件追加する。バグ修正のたびに回帰テストを足すのと同じ発想です
  • 最初は20〜50件で十分。網羅を目指すより、壊れたら困る代表例を先に入れる
  • 難易度を混ぜる。全部簡単だと合格率が常に100%に貼り付き、悪化を検知できません
  • 「わかりません」と答えるのが正解の行も入れる

判定方法の階段

判定はコストと厳密さのトレードオフです。上から順に試し、どうしても届かないものだけ下の段へ送ります。

  1. 完全一致・正規表現 — 分類や振り分けなど、出力が有限の選択肢に収まるとき。速く、追加費用は0
  2. 含む・含まない — 必須キーワードと禁止語のチェック。事実性の確認はかなりの割合がここで足ります
  3. 形式検証 — JSONとしてパースできるか、必須フィールドが揃っているか
  4. LLM-as-a-judge — 「この回答は資料に沿っているか」を別のLLMに採点させる。分かりやすさなど機械的に判定できない軸に使う
  5. 人手 — 最終手段。全件は現実的でないので、judge の判定が割れた件だけ見る

LLM-as-a-judge には落とし穴があります。judge 自身も間違えるのです。まず人間が採点した数十件を judge に解かせ、人の判定とどれだけ一致するかを測ってから本採用します。judge が信用できなければ、その先の数字はすべて意味を失います。

RAGは検索段と生成段を分けて測る

[[RAG(検索拡張生成)]] の回答が悪いとき、原因が「関連文書を取り逃した」のか「渡したのに無視した」のかで打ち手は正反対になります。まとめて測ると区別がつきません。

  • 検索段 — 正解の文書が上位k件に入っているかを数えるだけ。LLMを呼ばないので速く、費用もかからない
  • 生成段 — 正解の文書を手で与えた状態で回答させ、資料に沿っているかを見る

検索段が悪ければチャンクの切り方や検索方法の問題、生成段が悪ければプロンプトの問題、と切り分けられます。

CIに載せて回帰を検知する

評価は手元で1回流して終わりでは意味がありません。[[CI/CD]] に載せて変更のたびに自動で走らせます。ただし時間と費用がかかるため、通常のテストと同じ頻度では回せません。

  • 全件(数百件)は毎PRでは回さない。代表30件のスモークセットを毎PR、全件は日次かプロンプト変更を含むPRだけ
  • モデルのバージョンを固定する。固定しないと、何も変えていない日に突然合格率が落ちて原因究明に半日溶けます
  • しきい値は絶対値だけでなく「main比で何ポイント下がったか」で見ると運用しやすい
  • 「前回は通っていたのに落ちた件」の入力と出力をPRに出す。そこまで出して初めて [[コードレビュー]] で判断できます

初学者向けポイント

  • 評価セットが無い状態でのプロンプト改善は、直ったつもりの繰り返しです。手元の3件が良くなっても、別の30件が壊れていることに気づけません
  • 指標を1つに縮めない。[[モデル評価と指標]] と同じで、合格率に加えて形式エラー率・応答時間・1件あたりの費用も並べて見ます
  • 評価セットはプロンプトと同じリポジトリに置く。Gitの差分で「いつ合格条件を緩めたか」が追えます
  • 「評価を書く時間がない」ときこそ、失敗した実例を1件足すところから始めます

関連技術とのつながり

  • [[モデル評価と指標]] — 指標の考え方の土台。この記事はそれを開発工程に載せる話
  • [[プロンプトエンジニアリング]] — 改善ループの「直す」側。評価は「測る」側で対になる
  • [[推論パラメータと出力のばらつき]] — 期待値を固定できない原因そのもの
  • [[自動テスト]] — 期待値が完全一致にならない点だけが違う。しきい値判定に置き換える
  • [[CI/CD]] — 評価を回すタイミングと、費用・時間の折り合いをつける場所
  • [[RAG(検索拡張生成)]] — 検索段と生成段を分けて測ると原因を切り分けられる
  • [[ハルシネーション]] — 発生した実例が、そのまま評価セットの1件になる
Q: LLMアプリの評価が通常の自動テストと最も違う点はどれ?
- [ ] テストコードを書く言語が決まっていること
- [x] 期待値が完全一致にならず、合格率としきい値で判定すること
- [ ] 実行結果が必ず毎回同じになること
解説: 出力の文言が毎回変わるため、pass/failではなく「95%以上なら合格」のようにしきい値で判定します。

Q: RAGアプリの評価で、検索段と生成段を分けて測る理由はどれ?
- [x] 「文書を取り逃した」のか「渡したのに無視した」のかで打ち手が正反対だから
- [ ] 検索段はLLMの費用が生成段より高いから
- [ ] 分けないと評価セットが作れない仕様だから
解説: まとめて測ると原因が区別できません。分けることでチャンクの切り方の問題か、プロンプトの問題かを切り分けられます。

Q: LLM-as-a-judgeを導入するとき、本文が最初にやるべきとしているのはどれ?
- [ ] 全件を人手で採点し直してからjudgeに置き換える
- [ ] judgeに使うモデルを毎回最新版に自動更新する
- [x] 人が採点した数十件をjudgeに解かせ、人の判定との一致率を測る
解説: judge自身も間違えます。人の判定とどれだけ一致するかを確認してから本採用しないと、その先の数字がすべて信用できなくなります。