誰も訓練されてこなかったスキル
どのチームもLLM機能を出しています。しかし、その機能が本当に良いかどうかを知る方法を持っているチームはほぼありません。
Hamel HusainとShreya ShankarのAI Evals For Engineers & PMsコホートは、OpenAIやAnthropicのエンジニアやPMを含む2,000人以上の実践者を訓練してきました。これが満員になった理由は理論ではありません。実際にこれを作っている人たちが、自分たちのQAプロセスが「プロンプトエンジニアに30件の出力を読ませて、勘を信じる」だけだと気づいたからです。
Hamelはこれを「vibes eval」と呼んでいます。これが今も支配的なやり方であり、多くのAI機能が出荷され、デモではうまくいき、その後ひっそりと劣化していく理由でもあります。Lenny's Newsletterのポッドキャストで、HamelとShreyaは主張しました。AIプロダクトの仕事においてevalsは今や最もレバレッジの効くスキルであり、プロンプトの作り込みより重要で、正しいモデルを選ぶよりも重要だと。理由は構造的なものです。品質を計測できなければ、改善することもできません。
ほとんどのチームはこれを分かっています。それでもやりません。なぜか。evalsはインフラ仕事に見えるし、明確なオーナーが不在で、最初のバージョンは必ず手動で出力を読むより悪く感じるからです。鍵は、「手動で出力を読む」こと自体がevalだと気づくことです。規律として、一度だけそれを行い、何を見たかを書き留め、二度と同じやり方で繰り返さないこと。それが核心です。
この記事はそのプレイブックです。あえてツール非依存にしてあります。このスタックは、英国AI Safety InstituteのInspect AI、ArizeのPhoenix、Promptfoo、Langfuse、OpenAI Evals framework、Anthropicのeval用ツール、あるいはPythonスクリプトとGoogle Sheetの組み合わせでも動かせます。重要なのは方法論です。
「良さそう」がスケールで通用しなくなる理由
最初にLLM機能を出すとき、「良さそう」というのは十分な基準です。プロンプトを書き、10個の入力を試し、出力が妥当に見えたので、デプロイを押す。それで構いません。
しかし2回目以降は、もう困ったことになります。
LLMの出力は設計上、非決定的です。temperature 0はばらつきを減らしますが、ゼロにはなりません。同じプロンプトでも同じモデルで実行ごとに微妙に異なる回答を返すことがあり、モデルのバージョン違いになれば確実に変わります。さらに、足元で変わっていくものを加えてみてください。
- モデルドリフト。プロバイダは同じ名前のままモデルを更新します。今日のGPT-4oは3か月前のGPT-4oではありません。設定の文字列は同じでも、挙動は異なります。通知は来ません。
- 分布シフト。実ユーザーが送ってくる入力は、あなたがテストした入力とは違います。短いプロンプト、別の言語、変なフォーマット、添付ファイル。evalsが最も意味を持つのはロングテール部分です。
- プロンプトのエントロピー。誰かが1人のユーザーの苦情を直すためにシステムプロンプトを編集するたびに、忘れていた他の3つのユースケースを壊すリスクが生まれます。evalsがなければ、サポートチケットが急増するまでこれは見えません。
- 検索のドリフト。RAGを使っているなら、インデックスは常に変わっています。パイプラインの検索部分は、静かに劣化していきます。
隠れたコストはチームの時間です。変更のたびに、誰かが半日かけてプロンプトをクリックし続けます。モデル更新のたびに、「ちょっと確認してくる」が1週間がかりの作業になります。エスカレーションのたびに、「これはリグレッションなのか、こういうクエリでは前からずっと壊れていたのか」と誰も答えられない一回限りの調査になります。
きちんとしたevalsがあれば、こうした問いに安く答えられます。コストは先払いで、節約は複利で効いてきます。
LLM Evalsスタックの構造
レイヤーは5つあります。互いに積み重なっています。あるレイヤーをスキップすれば、その上のレイヤーはノイズを採点していることになります。
| レイヤー | 目的 | ツール例 | 落とし穴 |
|---|---|---|---|
| 1. トレース | 本番実行から、入力、出力、ツール呼び出し、検索結果、レイテンシ、コストをすべて捕捉する | Langfuse、Phoenix、OpenTelemetry exporters | サンプリングが極端すぎる、ツール呼び出しのペイロード漏れ、ログ内のPII |
| 2. エラー分析 | 実トレースを50〜100件open codingし、失敗モードをクラスタリングし、エラータクソノミーを作る | Notion、Airtable、CSV | このステップを飛ばす、1人だけで作業しキャリブレーションがない |
| 3. ゴールデンデータセット | 各失敗モードとハッピーパスをカバーする、手作業でラベル付けされた50〜100例 | Inspect AIのデータセット形式、JSONL、独自スキーマ | 合成データのみ、Step 2で発見した失敗モードがカバーされていない |
| 4. LLM-as-judge | ルーブリックを持つスコアリングモデル。人間ラベルとの一致率85%以上で検証されている | OpenAI Evals、Promptfoo、Inspect AI、カスタム | 検証していないjudgeを使う、judge 1回でスコア確定、ルーブリックのドリフト |
| 5. CI統合 | PRごとにevalを実行し、リグレッションでマージをブロックし、劣化をアラートする | GitHub Actions、GitLab CI、カスタムランナー | 実行が遅すぎる、コスト上限なし、judgeモデルの価格急騰 |
順序は重要です。どんな失敗モードがあるかを知らなければ(エラー分析)、ゴールデンデータセットは作れず、トレースなしにエラー分析はできません。ラベル付きのゴールデンセットなしにはjudgeを検証できません。judgeが本当に信頼できる状態になるまでは、CIでパイプラインを回せません。
Aakash GuptaのHamel/Shreyaカリキュラムのステップバイステップ解説は同じ骨格をやや異なる命名で説明しており、この分野で最もコンセンサスに近い形に整理されています。ここからレイヤーごとに見ていきます。
Step 1:トレースを捕捉する
トレースとは、1回のLLMインタラクションの完全な記録です。入力と出力だけでなく、その間にシステムが行ったすべてを含みます。
役に立つトレースには次の情報が含まれます。
- 完全なメッセージ履歴(システムプロンプト、ユーザーターン、アシスタントターン)
- 呼び出した通りの正確なモデル名とバージョン
- すべてのツール呼び出し、その引数、レスポンス
- RAGの場合:検索クエリ、返ってきたドキュメント、スコア、最終的にプロンプトで実際に使われたドキュメント
- トークン数、ステージごとに分解されたレイテンシ、ドル建てのコスト
- ユーザーセッションに紐づけ可能なリクエストID
- フィードバック信号(thumbs up、コピークリック、再生成、放棄など)
原則として、トレースは「真実のソース」です。トレースだけからモデルが何を見て何をしたかを正確に再構成できないなら、そのevalは推測の下流に存在することになります。
サンプリングについては、ボリュームとコストのトレードオフです。トラフィックの多いプロダクトでは、100%のトレースをログしつつ、フルペイロードはサンプル(5〜10%)のみ保持し、残りはメタデータのみに削ぎ落とすやり方が現実的です。エラー系のトレース(ツール呼び出しの失敗、タイムアウト、信頼度の低い検索結果、ネガティブフィードバック)は100%ログしてください。ここがあなたの金鉱です。
PIIについて。トレースにはユーザーデータが含まれます。他の本番ログに適用するのと同じレダクションとリテンションのルールを適用してください。多くのチームは、ユーザーIDをハッシュ化し、メールアドレスを除去し、ログを30〜90日のスケジュールでローテーションしています。
自前で組むなら、LLM SDKの薄いラッパーを書いて、構造化JSONをロギングパイプラインに流すだけで十分です。フォーマットよりも、常にログを取るという規律のほうが重要です。
Step 2:エラー分析(Open Coding)
これが誰もが飛ばすステップで、これこそが他のすべてを機能させるステップです。
Shreya Shankarの研究は、質的研究で使われるopen codingという手法を借りています。実トレースを50〜100件並べて読み、何が悪い(あるいは良い)かを注釈していきます。事前にカテゴリを決めません。カテゴリはデータから立ち上がってくるに任せます。
具体的には次の手順です。
- 本番から100件のトレースを引きます。ランダムサンプルと、ネガティブフィードバック、エラー、高レイテンシでフラグが立ったトレースを混ぜます。
- スプレッドシートを開きます。列は、トレースID、入力サマリ、出力サマリ、問題(自由記述)、深刻度(1〜3)、タグ(最初は空)。
- 各トレースについて、何が悪いかを平易な言葉で書きます。「出力が悪い」ではダメです。具体的に、「ユーザーのコードに存在しない関数名をハルシネートした」「言語が間違っている」「無害なリクエストを拒否した」と書きます。
- 20〜30件進めると、同じ問題が繰り返し出てきます。そこからタグ付けを始めます。2語タグで構いません。
- 100件すべて終えたら、タグをグルーピングします。典型的には、「特に問題なし」を含めて5〜15個の異なる失敗モードに落ち着きます。
これがあなたのエラータクソノミーです。HamelはField Notes on LLM Evalsの中で、これを「曖昧な懸念がテスト可能な主張に変わる瞬間」と表現しています。「ボットがときどきハルシネーションしている」が「ボットはコード関連クエリの8%で関数名をハルシネートしており、特に変数名が一般的でないときに顕著」になります。これならevalを書くことができます。
第二の目があると効きます。同じ30件のトレースを別の人にも独立してコーディングしてもらってください。意見が割れる箇所こそ、タクソノミーがぼやけている部分で、研ぎ澄ますべきところです。これは後でLLM judgeに適用するのと同じ、アノテーター間一致のチェックです。
Step 3:ゴールデンデータセットを構築する
ゴールデンデータセットは、パイプラインを評価する対象となる、入力・期待される挙動・ラベルの固定された集合です。LLM版のリグレッションテストスイートだと考えてください。
サイズは、最初のバージョンとして50〜100例が現実的なスタート地点です。10件では少なすぎ(ノイズが大きい)、1,000件では多すぎ(コストも時間もかかりすぎる)です。新しい失敗モードを見つけるたびに育てていきますが、初版は人間が午後の時間で全例を読み切れるサイズに留めるべきです。
カバレッジについては、エラー分析で見つかった各失敗モードに少なくとも5〜10例を割り当てます。さらにハッピーパスの例を20〜30件、加えていくつかのエッジケース(非常に長い、非常に短い、非英語、敵対的なもの)を入れます。arXivの論文Constructing Domain-Specific Evaluation Sets for LLM-as-a-judgeはカバレッジ戦略について読む価値があります。
採用基準は次の通りです。
- 合成データよりも実ユーザー入力を優先します。合成のみは既知の落とし穴です。それは「チームが書きそうなプロンプト」に寄りがちで、実際のユーザーが送るものとはずれます。
- 各例にラベル付きの期待される結果が必要です。オープンエンドな生成タスクでは、文字列の完全一致ではなく、ルーブリック(Xでなければならない、Yであってはならない、できればZ)になります。
- 各例には1つ以上の失敗モードのタグが付いています。
- センシティブデータはスクラブされています。
ラベル付けのルーブリックでは、「良い」がどう見えるかを書き出します。一部のタスクでは完全一致でよい(正しいAPIを呼んだか)でしょう。多くの場合は、プロパティのルーブリックになります。「少なくとも1つの検索済みドキュメントを引用していること」「拒否していないこと」「ユーザーの言語で答えていること」「関数名を捏造していないこと」。各プロパティはバイナリのチェックです。
更新サイクルは、四半期に1度、本番トレースから50〜100件を新たに引いてopen codingを繰り返します。新しい失敗モードを追加し、もう発生しない古いものはそのまま残します(リグレッションのカバレッジとして必要だからです)。モデルやプロンプトが大きく変わったら、サイクル外のリフレッシュも行います。
避けるべき罠は、プロンプトを書いているチームに同じくゴールデンデータセットも一手に書かせないことです。プロンプトの強みに過剰適合します。日々のプロンプト作業の外にいる人に、少なくとも半分はラベル付けを担当してもらってください。
Step 4:人間の検証を伴うLLM-as-judge
単一の正解がないタスク(要約、ぼんやりしたカテゴリでの分類、オープンエンドなQ&A)では、出力を大規模にスコアリングする方法が必要です。手動スコアリングはPRごとに走らせるわけにはいきません。
LLM-as-judgeはそのための手法です。別のLLM呼び出しを使って、自分のパイプラインの出力をルーブリックに照らしてスコア付けします。これは実際に機能します。Confident AIのLLM-as-judge手法の分析によれば、2025〜2026年に複数のベンチマークでjudgeと人間の一致率が85%を超えたと報告されています。これは主観的タスクで人間2人が互いに示す一致率とほぼ同等です。完璧ではありませんが、有用に足る水準です。ただし、検証してこそ、です。
方法論は順番が重要です。
- judge用のルーブリックを書く。「この回答は良いか」ではダメです。具体的に、「この回答は提供されたドキュメントの少なくとも1つを引用しているか? Y/N。拒否すべきでない場面で拒否していないか? Y/N。ユーザーのコードに存在しない関数名を捏造していないか? Y/N」。バイナリか短いLikertにしてください。judgeは1〜10スケールが苦手です。
- judgeモデルを選ぶ。強力な汎用モデル(GPT-4クラスやClaude Sonnet/Opusクラス)が小型モデルを上回るのが普通ですが、大量実行のevalランでは、検証さえすれば安価なjudgeでも十分です。
- ゴールデンセットを人手でラベル付けする。2人が独立に。アノテーター間一致(Cohen's kappa、あるいはバイナリでの単純な一致率)を計算します。人間同士が80〜85%以上で一致しないなら、ルーブリックがぼやけています。研ぎ澄ましてください。
- 同じセットでjudgeを走らせる。judgeのラベルと人間のラベルを比較します。judge-human一致率が80%を下回るなら、judge用ルーブリックのプロンプトが間違っているか、judgeモデルが弱すぎます。judgeプロンプトを反復改善してください。
- 一致率が85%以上に到達したときだけjudgeを出荷する。それ未満では、ノイズを自動化しているだけです。
よくある間違いは次の通りです。
- 検証せずに単一のjudge呼び出しを使う。一致率は60%かもしれません。それを知る術はありません。
- 生成と判定に同じモデルを使う。既知の自己選好バイアスがあります。可能ならjudgeには別のモデルファミリーを使ってください。
- judgeのルーブリックが人間のルーブリックからドリフトすることを許す。両者は同じプロンプトでなければなりません。一方を調整したら、再検証してください。
- judge呼び出し1回をグラウンドトゥルースとして扱う。高ステークスなevalでは、judgeを独立に3回呼んで多数決を取ります。judge呼び出し間の不一致それ自体がシグナルです。
検証されたjudgeは、数千の出力を数分・数ドルでスコアリングできます。それが報酬です。「リリース前にevalする」から「PRごとにevalする」へ移行できます。
Step 5:CI/CDに組み込む
このスタックの本質は、誰もevalsを走らせるのを覚えておく必要がないことです。CIがやります。
うまく機能する形は次の通りです。
- プロンプト、モデル設定、パイプラインコードに触れるすべてのPRで:ゴールデンevalを実行します。任意のメトリクスが閾値以上下がったら(例:精度で3パーセントポイント、または「無害なリクエストを拒否しないこと」のようなハードチェックでの任意のリグレッション)、マージをブロックします。
- モデルのバージョン更新ごとに:本番同等のインフラでフルevalを実行します。並べて比較します。
- 毎晩のスケジュールで:新しい本番トレースのサンプルに対してevalを実行します。これにより、静的なゴールデンセットが見逃す分布シフトを捕捉できます。
- コスト上限:PRごとのeval予算をキャップします。実用的なデフォルトは1ランあたり1〜5ドルです。evalがそれより高くつくなら、ゴールデンセットをサンプリングするか、PR時には安価なjudgeを使い、リリース時にフルjudgeを使うようにしてください。
機能するパターンとして、コミットごとに20〜30例の「fast eval」(2分以内)、PR承認時とmainで100〜300例の「full eval」を走らせる、というやり方があります。同じパイプラインでサンプルサイズだけ変えます。
レポーティングが重要です。PRコメントには、全体パス率、失敗モードごとのパス率、mainとのdiff、失敗トレースへのリンクを表示してください。これをログファイルに埋もれさせないでください。
ベースラインのドリフトでアラートを出してください。毎晩のパス率が5ポイント下がってそれが2日間続いたら、何かが変わっています(モデル更新、検索インデックスの変更、下流サービスの変更など)。evalはあなたのモニタリングレイヤーでもあるのです。
作り手が出荷し続けるアンチパターン
実際のチームで繰り返し出てくるため、注意しておきたいパターンをいくつか挙げます。
本番に出してしまったvibes eval。エンジニアが20件の出力をざっと見て「出して大丈夫」と言い、機能がリリースされます。3週間後、「これは先週より良くなっているのか」に誰も答えられません。
検証していない単一judgeでのスコアリング。誰かがLLM-as-judgeについて読み、「これを1〜10で評価して」という一行プロンプトを書き、スコアを根拠に意思決定を始めます。スコアはノイズです。
エラータクソノミーがない。チームはopen codingをスキップしました。ゴールデンセットは誰かの頭の中だけで書かれました。明らかなケースに過剰適合し、実ユーザーが踏むあらゆる興味深い失敗モードを取りこぼします。
人間同士の一致チェックがない。人間2人がラベルで一致していない状態では、judgeとそのうちの1人との「一致」には意味がありません。
合成データのみ。ゴールデンセットを別のLLMで生成しています。見映えは良く、すべてのevalをパスします。本番では、実ユーザーの入力がLLM生成入力と違うので壊れます。
1つの数字ですべてを要約する。「うちのevalスコアは87%です」。何の? どの失敗モードを通じて? 失敗モードごとのパス率が必要です。集計値は最も重要な失敗を隠してしまいます。
evalを後付け扱い。継続的なパイプラインではなく四半期ごとのイベントとして扱われています。リリースサイクル中に陳腐化し、誰も信頼しなくなります。
Evalsゼロのチームのための7日間プラン
ゼロから始めるなら、動くパイプラインに到達する1週間の進め方は次の通りです。
Day 1:トレースを捕捉する。LLM機能を1つ選びます。入力、出力、ツール呼び出し、(あれば)検索コンテキスト、モデルバージョン、リクエストIDを捕捉するロギングを追加します。ログ基盤に流します。この時点ではツールを選ぶ必要はありません。最初の週はJSONLファイルで十分です。1日の終わりの目標は、本番から100件以上のトレースを得ることです。
Day 2:Open codingラウンド1。50件のトレースを引きます。1人が読みます。何が悪いか、何が良いかを自由記述で注釈します。まだカテゴリ分けはしません。とにかく描写します。
Day 3:Open codingラウンド2。2人目が同じ50件を読みます。メモを突き合わせます。パターンから失敗モードのタクソノミーを構築します。5〜12個の名前付き失敗モードを目標にします。それぞれのおおよその頻度を見積もります。
Day 4:ゴールデンデータセットv0。各失敗モードとハッピーパスをカバーする50〜80例を選びます。各例にルーブリック(良い回答に必要な条件)を付けてラベル付けします。JSONLでもスプレッドシートでも構いませんが、バージョン管理に入れてください。
Day 5:judgeを作る。各ルーブリックプロパティをバイナリでスコアリングするjudgeプロンプトを書きます。本番モデルとは別ファミリーのjudgeモデルを選びます。ゴールデンセット上で走らせます。
Day 6:judgeを検証する。同じゴールデンセットを2人の人間が同じルーブリックでラベル付けします。人間同士の一致率とjudge-human一致率を計算します。judge-humanが80%未満なら、judgeプロンプトを反復します。85%に到達するか、6日目が尽きるまで止めません。85%に到達できないなら、問題はルーブリックです。バイナリの質問を研ぎ澄ましてください。
Day 7:CIに繋ぐ。プロンプトやパイプラインコードに触れるPRでevalを実行するGitHub Actionを書きます。パス率、失敗モードごとの内訳、失敗トレースへのリンクをコメントとして出力します。予算を設定します。リグレッション閾値を設定します。
週末時点で、あなたにはトレース、タクソノミー、ゴールデンセット、検証済みjudge、CIゲートがそろっています。スタック全体です。小さいですが、それが要点です。ここから複利で積み上がります。
1週目以降、evalsに使う時間はおもに次の3つに集約されます。ゴールデンセットを育てること、曖昧さを見つけるたびにルーブリックを磨くこと、CIが捕まえたリグレッションを調査すること。どれも追加の大規模ビルドは要りません。
よくある質問
vibes evalと本物のevalの違いは何ですか?
vibes evalは「20件の出力を読んでみたら、良さそうだった」というものです。本物のevalには次のものがあります。固定された入力セット、書かれたルーブリック、読む人によって変わらないスコアリング機構、そして2回の実行を比較する方法。最も速いリトマス試験は、「先週と今週のevalスコアはいくつで、どの失敗モードがリグレッションしたか」に答えられないなら、本物のevalは持っていない、ということです。
ゴールデンデータセットはどれくらいの規模にすべきですか?
初版は50〜100例です。各失敗モードを5〜10ケースでカバーできるくらいに大きく、人間が午後の時間で全部読めるくらいに小さく。本番で新しい失敗モードを見つけるたびに育てていきます。成熟したパイプラインの多くは200〜500例に収まっています。多くの異なるユースケースを横断的に評価しているのでない限り、1,000件を超えても得は少ないです。
LLM-as-judgeは本当に人間と一致しますか?
検証すれば、します。Confident AIの研究は、検証済みルーブリックでは2025〜2026年にjudge-human一致率が85%を超えたと報告しており、これはおおむね人間2人が互いに一致する水準です。ただし、judgeを人間ラベルに対して先にキャリブレーションしている場合に限ります。チュートリアルからコピーしただけで検証していないjudgeプロンプトは、ただの意見出力マシンです。
専用のevalツールが必要ですか、それとも自前で組めますか?
1週目は自前で十分です。JSONLファイル、Pythonスクリプト、ラベル用スプレッドシート。これでは手狭になったら(多くの場合、複数人がラベル付けする必要が出てきたとき、あるいはトレース閲覧用UIが欲しくなったとき)、ツールを選びます。Inspect AI、Phoenix、Promptfoo、Langfuse、OpenAI Evals frameworkはどれも同じ基本形をカバーしており、人間工学的な差があるだけです。堀になるのは方法論であって、ツールではありません。
ゴールデンデータセットの再ラベル付けはいつすべきですか?
基本は四半期ごとです。サイクル外では、モデルを変えたとき、プロンプトを大きく変えたとき、サポートチケットが新しい失敗モードで急増したとき、新しい本番トレースに対する毎晩のevalがゴールデンセットがもう代表的でないと示したときに行います。
おわりに
これからの2年間、AIプロダクトの仕事で勝つチームは、最も賢いプロンプトを持っているチームではありません。火曜日のどんな日でも、「うちのAI機能は先月より今日のほうが良いのか、どの軸で、どのユーザーに対して」に答えられるチームです。それはプロンプトの問いではなく、evalsの問いです。
方法論は安定してきました。トレース、エラー分析、ゴールデンデータセット、検証済みjudge、CIループ。5つのレイヤーで、それぞれ小さなチームなら1日か2日で構築可能です。evalスタックを持つチームと持たないチームの差は、急速に広がっています。
LLM機能を出荷していて、リグレッションの問いに自信を持って答えられないなら、1週目のタスクはまた別のプロンプト調整ではありません。JSONLファイル1つ、トレース50件、ラベルのスプレッドシート1つです。スタックの残りは、ここから自然に組み上がります。小さく始め、容赦なく検証し、次の機能を出す前にパイプラインを出してください。