体系 › エージェント
·無料·論文·15分で読めます

エージェント評価 — ベンチとハーネスの作法

エージェントのスコアは「モデルの点」ではなく「モデル+ハーネス+環境+採点規則の点」です。SWE-benchの1件がどんな部品でできているか、手数が伸びると一手の差がなぜ桁で効くか、リポジトリやネットワークから答えが漏れる四つの経路、そして部分点を安全に設計する条件までを1から扱います。

要点

  • エージェントのスコアは、モデル・ハーネス・環境・採点規則の四点セットで決まる数字である。
  • SWE-benchは「直っている」をFAIL_TO_PASS、「壊していない」をPASS_TO_PASSで測り、両方満たしたときだけ1点とする。
  • 一手あたり99%で50手なら完走率は0.61、95%なら0.077で、4ポイントの差が完走率では8倍近い差になる。
  • 汚染はgit履歴・ネットワーク・採点用テスト・事前学習の四経路から入り、前の三つは自分で塞げる。
  • 部分点は、回帰ゲートを二値で残し、中間目標に置き、二値のresolvedと併記する条件でのみ安全である。
対象textタスクevaluation

SWE-bench: Can Language Models Resolve Real-World GitHub Issues?


「SWE-bench で50%」は何を言っているのか

発表資料に「SWE-bench Verified 50%」と書いてあったとします。素直に読めば「実際のGitHubのバグの半分を直せる」です。ところがこの数字は、モデル単体の性能ではありません。モデル・足回り(ハーネス)・作業環境・採点規則の四点セットで初めて決まる数字で、そのうち三つは棒グラフには出てきません。

比喩は採用試験です。四択の筆記で満点を取る人と、リポジトリを渡されて「このバグ直しておいて」で結果を出す人は、別の能力を測られています。後者で効くのは、テストの回し方を知っているか、直したついでに別の機能を壊さないか、詰まったときに引き返せるか。エージェント評価が測ろうとしているのはこの後者で、だから測り方そのものが設計対象になります。

ベンチマークを読むときの三つの問い — 何を測ったか、どう採点したか、問題が漏れていないか — は、エージェントでもそのまま必要です。その上に、これから見る三つが積み増しになります。

一問一答と違う三つのこと

1. 環境がある。 四択問題の入力は文字列ですが、エージェントの入力はリポジトリであり、シェルであり、ファイルシステムです。しかも入力が大きいだけでなく、エージェントは入力を書き換えられます。極端な話、落ちているテストを削除して「全部通りました」と報告できてしまう。採点する側は、環境の初期状態と最終状態の両方を管理しなければなりません。

2. 手数がある。 一発の生成なら成否は1回の勝負ですが、エージェントは何十手も打ちます。一手あたりの成功率を pp、必要な手数を nn とすると、完走できる確率はこうなります。

Psuccess=∏i=1npi≈p nP_{\text{success}} = \prod_{i=1}^{n} p_i \approx p^{\,n}
(1)

pip_i は ii 手目で破綻しない確率、nn は最後まで行くのに必要な手数、PsuccessP_{\text{success}} は完走する確率です。要するに、一手ごとの生存率を全部掛け算したものが完走率という、当たり前の式です。

当たり前でなくなるのは数を入れたときです。一手99%で50手なら 0.9950≈0.610.99^{50}\approx0.61、95%なら 0.9550≈0.0770.95^{50}\approx0.077。一手あたり4ポイントの差が、完走率では8倍近い差になります。逆側から見ると、完走率という一つの数字だけを眺めていても、どの一手が足りていないのかは永遠に分かりません。

FIG 1対数軸に切り替えると、どの曲線もただの直線になる。指数はnが伸びたところで桁を変える — 一手あたりのわずかな差が完走率で何倍にもなるのは、これと同じ形

3. 副作用がある。 四択の採点は答え合わせで終わりますが、エージェントは外に書き込みます。ファイルを消し、コマンドを叩き、APIを呼ぶ。評価環境が本番の認証情報を持っていた、という事故は採点以前の問題として起きます。エージェントの評価環境は、壊れてよい使い捨ての箱として作るのが前提です。

SWE-benchの1件を開ける

SWE-bench(Jimenez et al., 2023)は、実在のGitHub issueと、それを閉じた実際の修正コミットから機械的に作られたベンチマークです。12個のPythonリポジトリから集めた2,294件のタスクからなり、1件はこんな部品でできています。

{
  "repo": "django/django",
  "base_commit": "…",        // 修正が入る直前の状態。ここから作業を始める
  "problem_statement": "…",  // issue本文。モデルに渡してよいのは原則ここだけ
  "patch": "…",              // 人間が実際に書いた修正(正解・モデルには見せない)
  "test_patch": "…",         // その修正と一緒に入ったテスト
  "FAIL_TO_PASS": ["…"],     // 修正前は落ち、修正後に通るべきテスト
  "PASS_TO_PASS": ["…"]      // 修正の前後どちらでも通り続けるべきテスト
}

設計の勘所は、正解を「人間のパッチとの一致」で測っていないことです。同じバグの直し方は何通りもあるので、文字列で比べれば正しい修正も不正解になります。代わりに置かれたのが実行による検証で、FAIL_TO_PASS が「直っていること」を、PASS_TO_PASS が「壊していないこと」を担当します。この二本立てが、動くけれど別の機能を巻き添えにするパッチを弾きます。

派生セットの名前も押さえておきます。Lite(300件)は評価を回しやすいものを選び直したサブセット、Verified(500件)は人手で1件ずつ確認し、issue本文だけでは仕様が定まらない問題や、テストが厳しすぎて正しい修正でも落ちる問題を除いたものです。数字を比べるときは、まずどのセットの数字かを確認します。母集団が違えば、同じ「50%」は別の量です。

resolved の定義は、論理積である

SWE-benchの主指標 resolved(解決率)は、1件ごとに0か1です。

resolved=[⋀t∈F2Ppass(t)]∧[⋀t∈P2Ppass(t)]\text{resolved} = \Big[\bigwedge_{t \in \mathrm{F2P}} \text{pass}(t)\Big] \wedge \Big[\bigwedge_{t \in \mathrm{P2P}} \text{pass}(t)\Big]
(2)

F2P\mathrm{F2P} と P2P\mathrm{P2P} はさきほどの FAIL_TO_PASS と PASS_TO_PASS、⋀\bigwedge は「そのすべてについて成り立つ」、pass(t)\text{pass}(t) はテスト tt が通ったかどうかです。要するに、直すべきテストが全部通り、かつ壊してはいけないテストが1つも落ちなかったときだけ1点、それ以外は0点。

この二値性は評価としては誠実です。八割方直った修正は、利用者から見れば直っていません。ただし開発の計器としては粗すぎて、「惜しかった」と「そもそも環境構築で終わった」が同じ0点になります。ここが部分点の設計動機で、後で扱います。

ハーネスがスコアの半分を決める

同じモデルでも、周辺の作りで数字は動きます。少なくとも次の四つが効きます。

だから正確には「モデルAは50%」ではなく「モデルA+ハーネスB+設定Cで50%」です。比較するときは一度に一つだけ動かす、という実験の基本がそのまま効きます。

そしてここに、公開ベンチ特有の力学が働きます。300件や500件を見ながらプロンプトとツールを何十回も調整すれば、その集合には効くが自社のリポジトリでは効かない作り込みが積もります。一回ごとの調整は正当な改善で、悪意はどこにもありません。それでも結果は過学習です。

FIG 2次数を「ベンチに合わせた作り込みの量」と読み替える。公開500件(訓練側)には効き続けるのに、社内リポジトリ(テスト側)では途中から外れていく

過学習と評価設計の検証データへの過学習と同じ構造なので、防ぎ方も同じです。調整に使う集合と、最終判断に使う集合を分けるしかありません。

環境そのものが答えを持っている

エージェント評価の汚染は、学習データだけの話ではありません。作業環境の中に答えが置いてあることがあります。経路は主に四つです。

  1. git履歴: base_commit にチェックアウトしても、cloneのオブジェクトに後続のコミットが残っていれば git log --all で正解パッチが読めます。履歴を切る、リモートとタグを外す、が最低条件です。
  2. ネットワーク: issue番号で検索すれば、それを閉じたPRが出てきます。ネット越しの調査を許すなら、そのスコアは「検索がうまいか」の測定に化けます。既定は遮断(コンテナならネットワークを無効にする)で、必要なら許可した事実を数字と一緒に書きます。
  3. 採点用テスト: test_patch を最初から置いておくと、期待される関数名も戻り値もそのまま読めます。だから採点用テストは作業が終わったあとに入れます。さらに採点の直前で、テストファイルを正本の内容に戻してからテストパッチを当てる。エージェントがテストを書き換えて通した場合を、機械的に無効化するためです。
  4. 事前学習: 公開リポジトリの修正コミットはウェブ上に存在し、モデルの学習データに入りえます。対策はカットオフより後のissueで作り直すこと。同じ設計で問題を作り直して測り直す、という評価一般の常套手段と同じ発想です。

疑う手がかりは暗記の症状です。issue本文を言い換えたら急に解けなくなる、変数名を変えたら落ちる、パッチは合っているのに説明が食い違う。1と2は自分で塞げるので、まずそこから確認します。

部分点をどう設計するか

二値採点は正しいのですが、開発中は分散が大きく、進捗も見えません。そこで部分点を入れるなら、形はこうなります。

s=1 ⁣[全 P2P が通る]⏟回帰ゲート×∑iwi 1[pass(ti)]∑iwis = \underbrace{\mathbb{1}\!\left[\text{全 } \mathrm{P2P} \text{ が通る}\right]}_{\text{回帰ゲート}} \times \frac{\sum_i w_i \, \mathbb{1}[\text{pass}(t_i)]}{\sum_i w_i}
(3)

1[⋅]\mathbb{1}[\cdot] は中身が成り立てば1、そうでなければ0。tit_i は FAIL_TO_PASS 側の各テスト、wiw_i はその重みです。要するに、「既存を壊していない」を通行条件にした上で、直せたテストの割合を点にするという形です。

設計の原則は四つあります。

1回の実行では足りない

エージェントの実行は、温度0に設定しても再現しません。推論側のバッチ構成、外部コマンドの応答時間、環境の時刻依存。同じ入力から違う軌跡が出るのが常態です。したがって1回の実行で出た差は、実力差ではなく揺らぎかもしれません。

コードで書く: 最小の採点ハーネス

これまでの原則を、そのまま採点関数にするとこうなります。

def score(inst, model_patch):
    repo = checkout(inst.repo, inst.base_commit)   # 履歴もネットも切った使い捨ての複製
    if not apply(repo, model_patch):
        return 0.0                                 # 当たらないパッチは0点。例外にしない
    restore(repo, inst.test_files)                 # エージェントによるテスト改変を捨てる
    apply(repo, inst.test_patch)                   # 採点用テストは最後に入れる
    r = run_tests(repo, timeout=900)               # 打ち切り時間も採点規則の一部
    if any(r[t] != "pass" for t in inst.PASS_TO_PASS):
        return 0.0                                 # 回帰ゲート: 壊したら無条件で0
    return mean(r[t] == "pass" for t in inst.FAIL_TO_PASS)

resolved = score(inst, patch) == 1.0               # 対外的な数字はこちら

順番に意味があります。restore を apply(test_patch) より前に置くことで漏洩と改竄の両方を塞ぎ、回帰ゲートを平均より前に置くことで部分点が回帰を隠すのを防いでいます。逆にすると、どちらも静かに壊れます。

現場ではこう使う

エージェント評価を書くのは、社内にコーディングエージェントを入れるか判断する人、エージェント基盤を作っている開発者、そしてベンダーやモデルの乗り換えを決める人です。使う場面は、モデル差し替えの意思決定、リリース前の回帰確認、そして「うちのリポジトリで本当に効くのか」の検証です。

実際に触る対象は、ベンチ側では FAIL_TO_PASS / PASS_TO_PASS、Lite と Verified の選択、1件ごとのDockerイメージのタグ固定。ハーネス側では最大ステップ数、トークン上限、テストのタイムアウト、コンテナのネットワーク無効化、温度と乱数種。運用側では、全軌跡(コマンドと出力)のログ保存と、正解パッチによる対照実行です。ログを捨てると、後から「なぜ落ちたか」を一件も説明できなくなります。

事故になる落とし穴は、だいたい決まった顔ぶれです。ハーネスを直した直後に、過去の数字と並べて比較する(比較不能になっています)。ネットワークを開けたまま測ってスコアが跳ねる(検索能力を測っています)。テスト改変を検知していない。1回の実行で3ポイント差を報告する。部分点を最適化目標にしてテスト特化の実装を育てる。不安定なテストの失敗をモデルの失敗として計上する。コストを書かない。

面接でも実務でも問われるのは、たいてい同じ一問です。「このエージェントはSWE-benchで50%だが、うちのリポジトリでは何%か」。答えの筋道は三つに分けます。分布のずれ(SWE-benchはPythonの著名OSSで、テストが揃っていることが前提。社内コードのテスト密度は違う)、汚染(そのOSSの修正は学習済みかもしれない)、ハーネス差(公開の数字は作り込まれた足回りの数字)。だから答えは「測ってみないと分からない、まず社内のissueを30〜50件でベンチ化する」になります。この自前セットの作り方は、LLMエージェントを1から解説で見たループの失敗モードを、そのまま観測項目に変換していくのが早道です。

まとめ

参考文献

  1. SWE-bench: Can Language Models Resolve Real-World GitHub Issues?. arXiv:2310.06770論文ページ·PDF
  2. SWE-agent: Agent-Computer Interfaces Enable Automated Software Engineering. arXiv:2405.15793論文ページ·PDF

本記事は上記論文の本文にもとづいて執筆しています。数値・主張は原典を優先してください。

運営: TechAthletes · 編集方針 · 出典の付け方 · 訂正の記録

コメント

コメントにはログインが必要です