かつて私はpromptの変更をリリースした。その変更で、監視していたメトリクスではサポートエージェントが4%改善された。しかし実際にチケットをクローズする能力は30%悪化していた。Evalは緑だった。ユーザーは怒り狂っていた。そのとき初めて、ほとんどのエージェントのevalが間違ったものを測っていることを学んだ。つまり、アウトプットが正しく聞こえるかどうかを測定していて、エージェントが正しいことをしたかどうかを測定していないのだ。
エージェントを構築していて、evalが別のモデルで採点された「レスポンスはよく見えるか?」という確認の集まりでしかないなら、evalを持っているとは言えない。数字を貼り付けた感覚があるだけだ。ここでは本当にリグレッションを捕捉するAIエージェントのevalをどう書くかを説明する。
散文ではなく、動作にアサートする
最大のアップグレードはこれだ。最終的なテキストを採点するのをやめ、エージェントが何をしたかを採点し始めること。適切な順序で正しい引数を使って正しいツールを呼び出したか?修正すべきファイルを修正して残りはそのままにしたか?これらは確認できる事実であり、意見ではない。
コーディングエージェントの場合、私のevalはサマリーを読まない。確認するのはこれだ。実行後にテストスイートが通るか、エージェントはスコープ内のファイルだけに触れたか、lockfileの編集を避けたか。3つのブールアサーション。LLMジャッジはどこにもない。これらは絶対にフレーキーにならず、重要な失敗を捕捉する。
assert run_tests() == 0, "tests fail after agent run"
assert touched_files <= allowed_files, f"out of scope: {touched_files - allowed_files}"
assert "package-lock.json" not in touched_files
最後のは実際のインシデントから生まれた。あるエージェントが「親切に」lockfileを再生成し続けてCIを壊していた。アサーション1つで、二度と起きなくなった。
エンドポイントだけでなく、軌跡をテストする
エージェントは途中で失敗する。最終的な答えは、壊滅的なパスの後に運よく正しいことがある。冗長な検索が6回、ゴミの引数でたまたまうまくエラー処理されたツール呼び出し、回復できた誤った方向転換など。エンドポイントだけを確認するなら、一つの小さな変更で完全に失敗する軌跡に対して盲目だ。
だから私はすべてのイベントをログに記録し、パスについてアサートする。新鮮な情報が必要な質問に答える前に検索を呼び出したか?冗長なツール呼び出しを2回以上したか?ツールの結果で is_error: true になったことがあるか?ジャッジなしで軌跡を採点できる。そのほとんどはイベントストリームのカウントとパターンマッチングだ。
ちなみに、最近モデルの動作が変わった。新しいOpusモデルはツールをより保守的に使う。古いモデルで通っていた「まず検索」というアサーションが失敗し始めることがある。エージェントが悪化したからではなく、今は使うべきでない場面でコンテキストから答えるからだ。これがまさに、軌跡evalが捕捉して出力evalが見逃すリグレッションの種類だ。
LLMジャッジが必要なときは固定する
本当に判断が必要なものもある。トーンは正しかったか、サマリーは重要なポイントを捉えたか、説明は実際に正確だったか。それならLLMジャッジを使う。しかしジャッジを本番コードと同じように扱うこと。実際にそうだから。
絶対に破らない3つのルールがある。1つ目:ジャッジモデルとバージョンを固定する。ジャッジが黙ってアップグレードされると、スコアがずれて、実はジャッジの変更であるリグレッションを追い続けることになる。2つ目:ジャッジには「1〜10で評価して」ではなく、具体的な基準を与える。「レスポンスは特定のファイルと行番号を引用しているか?はい/いいえ」は感覚スコアを常に上回る。3つ目:サンプルで人間のラベルとジャッジを照合する。一度も監査したことのないジャッジは、PRの良い乱数生成器だ。
現在のモデルに特有のトラップがある。ジャッジに「深刻度の高い問題だけを報告して」や「保守的に」と伝えると、新しいモデルはそれを文字通りに従う。バグを見つけて、閾値以下のものを報告することを拒否する。測定されたリコールが低下し、エージェントがリグレッションしたように見えるが、実際にはジャッジが従順になっただけだ。ジャッジには信頼度と深刻度を付けてすべてを報告させ、ダウンストリームでフィルタリングする。フィルタリングをジャッジのステップから切り離すこと。
失敗からevalセットを構築する
座ってテストケースをブレインストーミングしてはいけない。すでに処理できているものを書くことになる。代わりに、エージェントが本番でしくじるたびに、その正確なシナリオがevalケースになる。lockfileのインシデント、行ったり来たりのループ、古いコンテキストから答えたあの時、それぞれが永続的なリグレッションテストになった。
これは地味だから人々が飛ばす部分だ。しかし実際のインシデントから育ったevalセットは、ブレインストーミングで作ったものの10倍の価値がある。合成ケースには絶対にない形で負荷を担っている。なぜなら、すべてのケースは実際に自分を噛んだバグだからだ。
すべてのprompt変更で実行する
全体の要点はループにある。promptを変更し、evalを実行し、リリース前に軌跡のアサーションを確認する。私のサポートエージェントの惨事が起きたのは、いくつかのアウトプットを目で見て、勘でリリースしたからだ。軌跡eval――「実際に解決したか、それとも解決したように聞こえただけか」――があれば、30%の低下は1人のユーザーに届く前に赤くなっていたはずだ。
Evalは最後にボルト留めするクオリティゲートではない。「このpromptの方がいいと思う」と「このpromptはいい」の間に立つ唯一のものだ。一方は工学だ。もう一方は白衣を着た推測だ。
傷跡から育てる。エージェントが何をしたかにアサートする。ジャッジを固定する。そして感覚でpromptの変更をリリースするのは二度としない。
