断層:リーダーボードのスコアと業務での挙動は別のものだ
同じモデルでも、公開ベンチマークのリーダーボードでは首位を独走していたモデルが、実業務に繋いでみると期待外れに終わる。わずかな差ではなく、決定的に離れている。サポートボットの雑談は印象的なのに、ユーザーが細かな点を三つ連続で尋ね始めると、途端に回答がずれる。コード補完ツールは単一関数の補完なら高い正答率を出すが、複数ファイルにまたがる変更になると、構文は正しくても論理が破れた patch を平然と生成する。抽出タスクは形式が整ったチケットではほとんど誤らないが、一年前の古いチケットに手挂かると、フィールドを落とす。
この種の落差はランダムなものではなく、モデルが「ある日突然賢くなくなった」ことでもない。系統的なものであり、互いに独立した複数の要因に分解できる。eval データの分布は業務の分布とは別のものだ。eval タスクの形式と本番タスクの形式も別のものだ。単一能力は複合条件の下では非線形に劣化するのに、公開のリーダーボードはほぼ単一能力しか測っていない。これらを業務と重ね合わせれば、リーダーボードのスコアは相関の弱い参考値に退化してしまう。
より深い問題は、リーダーボードのスコアがスカラー一つに圧縮されている一方、必要なのは地図一枚だということだ。知りたいのは「このモデルは強いか」ではなく、「このモデルは私の業務入力において、どこで破綻するのか」だ。70点のモデルで、失点がすべて長文コンテキスト中盤の情報想起に集中しており、業務は1セッションあたり三千字を超えないなら、十分使える。逆に88点のモデルで、失点が多輪指示の遵守にちょうど重なっており、毎日「さっきの案を少し変えて」系の依頼を大量にさばくなら、そのモデルは危険だ。
平均値がいかに悪い指標であるか
単一の数値にまとめる手法には、必ず一つ問題がある。分布が隠れてしまうのだ。eval レポートに並ぶ正答率、勝率、通過率は、本質的に多次元の能力ベクトルをスカラー軸に射影したものにすぎない。射影は不可逆であり、能力プロファイルがまったく違う二つのモデル,只要射影値が近ければ、リーダーボード上は同一に見える。
工学的に実用的なのは、一つの総合スコアを追いかけをやめて、ラベル付きの分項通過率の集合を追いかけはじめることだ。欲しい成果物は「モデルAはモデルBより3点高い」ではなく、「出力 schema を厳密に守る必要がある場面ではAの通過率が91%、Bは99%でこの場面はBにルーティングする。10ファイルにまたがる一貫性リファクタが必要な場面ではAは46%、Bは12%なのでAを使う」というものだ。この2種類の情報は、アーキテクチャ判断における価値が1桁違う。
これが、eval set の作り方、採点の方法、サンプル数の決め方まで長々と扱う理由だ。これらは瑣末に見えるが、どれも同じ一件事の別の面である。最終的に手にする「能力境界像」の質は、その像を生成した測定のプロセスで完全に決まる。測定にバイアスがあれば像にもバイアスが入る。しかもそのバイアスは通常は無音だ。エラーを目撃することはない。ある日突然、本番の障害の中で、当初の eval 結論が間違っていたと分かるだけだ。
公開リーダーボードと業務での挙動が乖離する4つの具体的な仕組み
以下の4つは理論的な推測ではなく、LLM 機能を一度でも本番導入したチームが遭遇する具体的な仕組みだ。これらを理解することが、「自前の eval を作るべきか」を決める前提になる。
仕組み1:データ分布のずれ
公開 eval set のデータは、その収集経路自体に由来しており、通常は強い領域バイアスがある。一般知識に偏るもの、学術的推論に偏るもの、標準化された指示に偏るもの。これらは複数の方向で同時に業務のデータとずれている。
- 領域のずれ:eval の「推論問題」は多くは数学と論理形式推論だが、業務は欠けた説明書きのチケット群からユーザーの本当の目的を推測するものである。前者にはきれいな題面があり、後者には情報の半分が欠けている。
- 言語と表現スタイルのずれ:ユーザーの実際の入力には誤字、くだけた話し方、省略、方言的な表現、業界用語、コピー&ペーストによる書式のノイズが満ちている。eval set の入力はしばしばきちんと正規化されて書かれている。
- 長さ分布のずれ:eval set のサンプルの大半は短い入力である。本番で遭遇する入力の長さ分布は極端に偏っており、かつ長い部分がちょうど最難の部分である。
- 敵対的なずれ:eval set はわざと厄介なサンプルを含むが、その厄介な方向は設計者が想像できる範囲に収まる。実際のユーザーは想像もできない方向から攻撃してくる。
分布のずれには見落とされやすい増幅効果もある。入力が学習分布から外れると、モデルのvariance は明確に上昇する。同じ問題でも、分布内なら複数回サンプリングしてすべて正解になることがあるが、分布外なら半分しか正解にならないこともある。つまり「1回走らせて正解か確認する」方式で集計すると、分布外のサンプルでは過小評価と誤判定が同時に起きる。失敗率がどれくらい高いかも、安定性がどれくらい悪いかも、わからない。
仕組み2:タスク形式の違い
同じ能力目標でも、eval と本番ではまったく別のタスク形式が使われることがある。この差だけで結果は何倍も変わる。よくあるのは次の数種類である。
- 単輪と多輪:リーダーボードの設問は条件を一度にすべて渡して答えさせる。本番では条件が少しずつ追加され、モデルは前十数ターンに現れた制約(「2段落目の口調は使わない」「金額は小数点以下2桁で統一」)を覚えて統合しなければならない。多輪では状態追跡の能力が新たに求められるが、多くのリーダーボードはこの項目を測らない。
- 直接回答とツール呼び出し:eval は「この注文をどう処理するか」と尋ね、モデルは直接答える。本番ではまず注文表を引いて、次にユーザーの等級表を引いて、次に返金APIを呼んで、最後に3つの結果を1つの回答にまとめる必要がある。ツール呼び出しは「生成能力」を「振り分け能力」に変える。後者は書式と順序に対してより敏感である。
- 自由テキストと構造化出力:リーダーボードは「モデルが正しく答えていればよい」ことを受け入れることがある。本番は出力が下流のプログラムで解析できる JSON を求め、フィールド名、型、入れ子の構造、既定値の扱いに硬い制約がある。意味が完全に正しくても、末尾に余計な説明文が1文付いただけでチェーン全体が解析に失敗する。
- 完全な入力と検索の組み立て:本番のモデルが見る「文書」は検索システムが選んだ断片である。関連する3断片と無関係な5断片の組み合わせかもしれない。eval で与えられるのはきれいな、すべて関係ある資料である。両者がモデルに求める干渉への頑健性はまったく異なる。
これらの差を重ねると、よく目にする現象が現れる。モデルはあるリーダーボードでは優れた記録を出すのに、実際の製品で「普通に使える」割合は、そのスコアが示唆する水準よりはるかに低い。これはリーダーボードが操作されているのではなく、そもそも別のことを測っているからだ。
仕組み3:複合条件下での能力の非線形な劣化
各能力を単体で測ると、得られるスコアは通常そこまで悪くない。本当の問題は能力を組み合わせたときにはじまる。長さ、複雑さ、干渉、ターン数、構造制約。どの次元も単体ならモデルの能力範囲内だが、2つから3つを同時に課すと正答率は崖のように落ちる。この劣化はしばしば線形ではなく、相転点が存在することもある。その点より先では、入力がわずかの増加しかしていないのに、結果はまったく使えないものになる。
工学的にはこれが eval set の設計を直接左右する。単純な単体サンプルだけで評価すると、業務におけるモデルの可用性を体系的に過大評価してしまう。本番のシーンはほとんど複合領域にあるので、eval set もそうあるべきだ。粗い分解ならできる。単体の通過率が0.95なら、2つの組み合わせの通過率は0.75から0.85までに留まり、3つの組み合わせは0.5を下回ることもある。この乗算近似は直感としてしか使えないが、実際の減衰はタスクの種類と実装によって変わる。それでも複合 eval が必要であることの説明には十分だ。
仕組み4:リーダーボードのスコアが安定性とコスト構造を隠してしまう
リーダーボードは variance、温度への感度、出力長をほとんど報告しない。貪欲デコードでは安定しているように見えても、本番で使うサンプリングパラメータでは明显に揺れるモデルがあり得る。わずかに正答率が高くても出力長が3倍長いモデルは、実際のコストがむしろ高くなることもある。これらの次元は自分の具体的な構成で測って初めて意味を持ち、構成から切り離されたスコアは必ず不完全だ。
見落とされやすい点がもう1つある。リーダーボードのスコアが表すのは、1回きりで、状態がなくて、副作用もない推論の呼び出しなのである。本番の失敗はしばしば答えが誤ることではない。間違っても誰も気づかないのだ。抽出でフィールドが1つ欠けても、例外も警報も出ず、エラーがそのまま下流に流れる。こういう静かな失敗(silent failure)は、副作用を伴う自分の eval 手順の中でしか表に出てこない。
本番の失敗から逆算して eval set を作る:もっとも価値のある1歩
eval set には3つの入手元がある。公開データセットをダウンロードして変換する、自分の経験で手で書く、本番で実際に起きた失敗から逆算する。最初の2つは入門の做法で、3つ目が測定をエンジニアリング資産に分ける分水嶺である。理由は単純だ。最初の2つで生じるサンプルは、測っているのがモデルが苦手かもしれないとあなたが推測するものだ。3つ目が測るのはモデルが確実に壊しているものだ。前者の価値は探索、後者は回帰の保障であり、製品はまず後者を必要とする。
第1歩:失敗サンプルを拾える経路を作る
逆算のデータセットを作れる大前提は、失敗サンプルを収集できることだ。これは明らかだから当然のように聞こえるが、実際には多くのチームができていない。典型的な状況とは、本番にはユーザーのフィードバックチケットが大量にあるだけで、構造化された失敗サンプルの倉庫がないという状態だ。この経路を作るための最小構成は4つの要素からなる。
- 全量の記録:本番のすべてのリクエストについて、入力、出力、モデルのバージョン、プロンプトのバージョン、サンプリングパラメータ、その後のすべてのツール呼び出しの軌跡を永続化する。どれか1つでも欠ければ、あとから再現できない。
- 明確な失敗シグナル:必ず「今回は失敗した」と印を付ける何かが存在しなければならない。ユーザーがネガティブフィードバックを付けた可能性はある。下流のプログラムが解析に失敗した可能性はある。業務ルールの検証に落ちた可能性はある。あるいは、非同期で稼働する人手による審査キューがそれを監視しているかもしれない。
- サンプルの安定した識別子:同じセッション、同じチケットはまとめられるようにする。種類が同じエラーの異なる発生同士を関連付けられなければならない。さもないと、「この種類の入力を処理するときに酷い間違いをする」ではなく、「モデルが時々間違える」記録が1000件見えるだけになる。
- 定期的な洗浄と重複排除:失敗サンプルには重複とほぼ重複のものが多数含まれる。これをそのまま eval set にすると、問題が実際より何倍も大きいと思ってしまう。
この経路のコストは嵩む。しかし一度きりのインフラ投資であり、同時に本番の問題切り分け、プロンプトのチューニング、データのフライホイールなど複数の目的にも役立つ。モデルの把握を1つだけ上げるとしたら、eval スクリプトを先に書くことではなく、これである。
第2ステップ:失敗に原因を帰属させる。サンプルを積むのではなく
失敗サンプルを拾ったあと、それをそのまま全部 eval set に放り込むのが最もよくある誤りだ。理由は、これらのサンプルにはモデルの能力とまったく無関係な失敗が大量に混ざっているからだ。検索が正しい素材をそもそも召回していなかった、コンテキストが重要な情報の手前で切られていた、ツールのインターフェースそのものが汚れたデータを返していた、提示語テンプレートがそのときちょうど変数を1つ欠けていた——こういう類だ。これらを区別せず、このままだと eval set の通過率が極端に低くなり、何を直せばよいのかまったく分からなくなる。
正しい方法は、失敗サンプル1件ずつに原因帰属ラベルを付けることだ。そのラベルの値は、漠然とした言い回しではなく、行動として使える engineering action にしておくべきだ:
| 原因帰属ラベル | 典型的な症状 | 手を入れるべき箇所 |
|---|---|---|
| 知識欠落 | 存在しない実体やルールを捏造した | コンテキストを補うか検索戦略を変える |
| 推論の断裂 | 条件の認識は正しいのに結論が成り立たない | タスクを分割するか中間検証を挟む |
| 指示違反 | 明示的に書かれた制約や順序を無視した | 提示語の構造を変え、制約を前出しする |
| コンテキストの喪失 | 前に示された情報を使い、後ろで切られた部分に手を伸ばした | コンテキスト予算を調整するか要約を入れる |
| 多輪漂流 | 何ターン目かから初期の制約に背くようになった | コンテキストに頼らず外部状態を持ち込む |
| ツール誤用 | 引数の誤り、順序の誤り、無意味な再試行 | schema を厳しくするかオーケストレーションを直す |
| 形式崩壊 | 意味は正しいが構造の解析に失敗した | 制約デコーディングか後処理での検証 |
| 拒否すべきなのに拒否しなかった | 能力の限界や権限の外側でも答えてしまった | 拒否戦略と権限境界を明示的に定める |
| モデルの問題ではない | 検索の欠落、切り詰め、上流の汚れたデータ | まずパイプラインを直す。提示語には触れない |
この表の価値は、「モデルが駄目だ」という使えない結論を、具体的な engineering action に翻訳できたところにある。帰属づけを終わると、失敗サンプルのうち「モデルの問題ではない」の比率は、予想をはるかに超えていることが分かる——この発見自体がパイプラインの問題を1度直す理由になり、そのあと eval set のベースラインは自然と持ち上がる。
第3ステップ:「本来ならモデルは正解できた」サンプルだけを残す
帰属づけをしたあと、もう一度篩をかける必要がある。判断基準は単純で、モデルの出力を正しい出力に置き換えたとき、この経路は正常になるかだ。置き換えても正常ならそれはパイプラインの問題であり、この eval set に入れるべきではない。eval set に入るすべてのサンプルは、1つの性質を満たしているべきだ。その失敗が環境側ではなく、モデル自身の挙動に帰属できるということ。
この篩の規律は非常に重要だ。パイプラインの問題が混ざった eval set のスコアは,没有任何決定を導き出すことになる——提示語を直すとスコアが動いたが、それがモデルの改善なのか、それとも検索が運良く1件多く召回されたのかが分からない。スコアは以後、因果的な説明力を失い、eval set も事実上の機能を失う。
難易度の分布を制御する:スコアに情報量を与える
サンプルがあってもまだ不十分だ。90% が簡単な問題で、10% が極めて難しい問題なら、正解率 0.9 の大半は簡単な問題の寄与になる。「モデルが 0.85 から 0.90 に上がった」のか「難しい問題で 0.1 から 0.15 に上がった」のかを区別できない。この2つの改善は、業務上の価値が10倍違う可能性があるのに、総点はほぼ同じになる。
方法は、サンプルの自然な分布に受動的に受け入れず、難易度の stratify を能動的に設計することだ。実用的な stratify の次元としては次のようなものがある:
- 入力長による stratify:token 数でいくつかのバケットに切る。たとえば千の桁、五千の桁、二万の桁。バケットごとにスコアを報告する。長いバケットの崩壊が最も多い。
- 条件数による stratify:1つのタスクでユーザーが何個の制約を出し、何件の作業を同時に満たすことを求めているか。制約数と通過率にはしばしば明瞭な非線形関係がある。
- 干渉強度による stratify:コンテキストに、意味的には関連するが実際には無関係な素材をどれだけ入れるか。これは retrieval augmented なシーンで最も差がつく次元。
- adversarial stratify:誘導、否定、曖昧さ、多義的な指示語、能動的に覆すべき暗黙の仮定を含むかどうか。
- ターン深度による stratify:多輪のタスクが、何ターン目で崩壊し始めるか。
これらの次元でバケットに切ってからスコアを報告すると、どの総合スコアより役立つ表が得られる。それは直接教えてくれる——このモデルはあなたのような仕事を引き受けてくれるのか、断点はどこで、そして断点より先に救う価値が残っているのか。
テストセットの漏洩を防ぐ:三つの規律
eval set が死ぬ一番の方法は、不正確さではない。こっそり汚染されたあと、なおもに高すぎるスコアを出し続けることだ。評価は、同じ人が作り、調整し、見る。身分が重なっている。漏洩はほぼ確実に起きる。制度で分離しない限り避けられない。
規律その一、テストセットとチューニングセットを物理的に分離する。チューニングセットは,反复走らせ、結果反复見て、提示語を直す場所だ。ここが過学習されることは不可避だ。その過程自体は合理的だ。むしろ必須である。テストセットは走る頻度を制限する。「今回の変更は改善か、退化か」に答えるためだけに使う。
規律その二、本番で出た新サンプルはまずチューニングセットへ入れる。テストセットには入れない。逆算した失敗サンプルは、現在のシステムの境界から出てきたものだ。直接テストセットへ入れると、リリースのたびにテストセットで選定していることになる。新サンプルはまずチューニングセットへ入れる。何ラウンドかの反復を経て、安定した能力次元のひとつだと確かめてから、テストセットの一部として昇格させる。そのうえで、テストセットの中に、それまで撹乱されていない双子のサンプルを1つ添える。入力は似ていて、結果が違う。それをによって、モデルや人が表層のパターンを覚えるのを防ぐ。
規律その三、テストセットの正解ラベルを手書きの提示語担当に見せない。この条文は行き過ぎに聞こえる。防いでいるのは、きわめて目立たない漏洩だ。正解を知った人は、無意識に提示語へその特定の答えに向けた誘導を足すようになる。その誘導は新サンプルには効かない。しばしば有害だ。実際には、テストセットの判定工程を提示語のチューニング工程から独立させるのがよい。
この3つのほかに、実装する価値のある手段がもう1つある。テストセットの撹乱版を生成することだ。各テストサンプルに対して自動的に複数のバリアントを作る。表現の言い換え、順序の変更、実体の置換、無関係な文の追加削除だ。リリース前に撹乱版を走らせる。モデルが撹乱の下でも安定しているかを確かめる。あるモデルが元のテストセットで 95%、撹乱版で 62% なら、それは能力ではなく表層のパターンを学習しているということだ。この種のモデルは新しい表現に直面すると、すぐに崩れる。元のテストセットでは、そのことが永久に伝わらない。
能力の階層分け:それぞれ独立した8つの天井
「このモデルは使えるのか」という問いには、エンジニアリング上の価値がない。価値のある形は、能力を比較的独立した数次元に分けることである。それぞれについて通過率と崩壊点を測る。これらの次元は崩壊点の位置が異なる。対処方法もまったく違う。だから必ず分けて測る。
次元一:知識
知識次元の天井を決めるのは、パラメータ数ではない。コンテキストに詰め込める情報量と、検索システムが供給できる精度が決める。この次元を測る正しい方法は、エンドツーエンドで測ることだ。実際の業務上の問いを1つ与える。検索と生成のチェーン全体を走らせる。最終的な答えが正しいかを見る。
「モデルが特定のプライベートな知識を知っているか」だけを単体で測る意味は小さい。プライベートな知識をプロンプトへ直接注ぎ込むことは決してない。必ず検索を通すからだ。ここで本当に測りたいのは2つのことである。検索された素材が答えを支えるのに十分か。これは検索の仕事だ。素材が十分なとき、モデルが忠実に使うか。これはモデルの仕事だ。分けて測るには、正しいコンテキストを与えたサンプルを1組作る。このときモデルはもう間違ってはならない。間違れば生成の問題だ。
次元二:推論
推論次元では、推論チェーンの長さと推論チェーンが存在するかという2つの問題を区別しなければならない。前者は歩数が増えたときに正解率がどう減衰するかだ。後者はモデルが途中でステップを飛ばしたり、誤読したり、自己矛盾したりするかどうかだ。
推論の上限を探る最も有効な方法は、より難しい問題を探すことではない。既知の推論問題を制御的に引き伸ばすことだ。原題の条件をより細かく分割する。中間量を明示的に書き出す。無害な中間計算を1ステップ挿入する。そのうえで、正解率が推論チェーンが伸びるにつれてどう変化するかを見る。曲線の形は、単点のスコアより情報量が多い。緩やかな線形減少なら、推論チェーンの減衰は予測可能だ。思考予算を増やすことで補える。急激な崩壊なら、構造的な欠陥が存在する。予算を足すのではなく、タスクの分け方を変える必要がある。
次元三:指示の遵守
最も過大評価され、しかも単独で直しやすい次元である。指示の遵守とは、モデルが明示的に書かれた、形式化された、検証可能な制約をどれだけ守るかのことである。出力はある構造でなければならない。どのフィールドを含めなければならないか。どの順序でなければならないか。どの長さ以内か。どの口調か。どの言語か。
測る鍵は、制約を自動検証できる形に書くことだ。たとえば「出力は合法な JSON であり、フィールド a と b を含まなければならない」という制約なら、採点は人手でも言語モデルでもなく、パーサとフィールド検査で直接行う。制約の遵守度は、ほぼ客観的に判定できる能力次元の数少ないひとつである。評価コストが非常に低い。提示語を直した効果が通常は即座に現れる。先に着手する価値がある。
指示の遵守には、単独で取り出す価値のある特殊なサブクラスがある。否定形制約だ。モデルに何かを「するな”的要求するのは、「しろ」と要求するよりずっと難しい。忘れられやすい。とくに否定制約が肯定指示と衝突する場面では 그렇である。「説明はいらない、答えだけを出せ」と「不確かな場合は理由を述べよ」が衝突する場合などだ。否定制約を測るときは、「誘い」のサンプルを特別に用意する。禁止された振る舞いを、いちばん引き出されやすい位置に置く。
次元四:長コンテキスト
長コンテキストの次元は、崩壊のメカニズムがまったく異なるので、3つのサブ項目に分けて測る。
- 大海捞針(needle retrieval):長い材料の中から特定の文字列を見いだす。最も簡単な層だ。長コンテキストを謳うモデルの大半はこれを突破する。実際には位置エンコードがその長さを処理できることを証明しただけで、理解能力の証明ではない。
- 情報の集約:長い材料の異なる場所に散らばる複数の事実を組み合わせなければ答えられない。業務的长度に真正に対応するのはこちらである。
- 長い材料による干渉:材料の中に意味的にきわめて近い干渉項目が多数含まれ、目的はそれらを区別することである。最も難しい層であり、本番で最も問題が起きやすい層でもある。検索がモデルに渡す10件の結果は互いに高度に関連している。モデルはそれを正確に区別しなければならない。
業務上で最も価値のある報告形式は、token 長さでバケットに切った通過率の曲線である。「12.8万 token の位置で通る」とは違う。実際の業務の入力長は連続分布している。知りたいのは、「自分の入力は通常どのくらい長いか」に対応する曲線上のどの位置かだ。
次元五:多輪の一貫性
多輪の一貫性が問うのは、不要な情報が追加されたとき、モデルの出力が変わるかだ。これは正確性ではなく安定性についての性質である。完全に正しいモデルでも、ユーザーが同じことを別の言い方で聞いたときに違う答えを返せば、用户体验の意味で同様に悪い。
この次元のサンプルを作る方法は「等価な言い換え」である。k 輪目の問いを、意味は等価だが表現が異なる形に言い換える。そのうえで、後続の出力が本質的に変化するかを観察する。あわせて「制約ドリフト」のテストを設計する。初期の輪でいくつかの制約を提示し、数十輪後にその制約を満たし続ける産物を求めてみる。いつ忘却が始まるかを見る。忘却の輪の深さはモデルによって異なり、初期の制約がコンテキスト内のどの位置にあったかと強く相関する。
次元六:ツール呼び出し
ツール呼び出しは、いま本番で最も過大評価されやすい次元である。少なくとも4つの独立した能力が含まれる。まとめて1つのスコアで出す意味はない。
- 選択の正しさ:使えるツール集合が大きく、機能が近いツールが複数あるとき、正しいものを選べているか。
- 引数の正しさ:必須項目、型、列挙値、そしてコンテキストから正しい値を抽出できることまで含む。この項目の誤り率は通常もっとも高い。「自然言語から構造化フィールドへの対応付け」だからだ。この種の対応付けは、一貫性を保つのがもっとも難しい部分である。
- 呼び出し順序の正しさ:複数ツールのあいだの依存関係を満たしているか。循環呼び出しや無意味な再試行が出ていないか。
- 結果の利用の正しさ:ツールから返ってきた結果を受け取ったあと、それを正しく使えているか。無視していないか。過度に依存していないか。
ツールの次元には特殊な制約がある。副作用はロールバックできない。評価時には読み取り専用のツールと副作用のあるツールを区別しなければならない。読み取り専用のツールは eval set で自由に呼べる。副作用のあるツールはサンドボックス環境か、記録の再生が必要だ。これを無視すると、eval set を走らせた瞬間に実データが汚染される。これは多くのチームが踏んだ、高くつく落とし穴である。
次元七:構造化出力
この次元は表面上は指示の遵守の部分集合と同じに見えるが、単独で取り出す価値がある。独立した故障モードがあるからだ。意味は正しいが文法が崩れる。モデルの出力の内容そのものは完全に正しい。なのに、外側に説明文が1層ぶん付いたり、全角の引用符が1つ使われたり、末尾にカンマが余ったりするだけで、解析全体が失敗する。
この種の故障に対する工学的手段は制約デコーディングだ。サンプリングの段階で文法によって出力を拘束する。事後の解析修復ではない。制約デコーディングは「形式が正しい」ことを確率の問題から構造保証の問題へ変える。この次元では最も有効な解法である。代償として、モデルの自由な表現空間が狭まる。複雑な schema での実装は、具体的なエンジンの能力に依存する。使えるかどうかは実装による。
次元八:拒答能力
最も忘れられやすいのに、リリース後のリスクへの影響はもっとも大きい次元である。測るには2つの方向があり、どちらも欠けない。
- 拒むべきときに拒むか:知識の範囲を超えるとき、権限の範囲を超えるとき、あるいは入力そのものが答えを支えるのに足りないとき、モデルは捏造せずに明確に拒むか。この方向の誤りは代償が極めて大きい。
- 拒むべきでないときに誤って拒まないか:答えるのに十分な情報があるとき、あるいは常識的な推論だけでよいとき、モデルは過度に保守的になるか。この方向の誤りは可用性の低下であり、たいていもっと隠れている。「モデルが能力不足」に見えて、「モデルが事故を起こした」とは違うからだ。
この2つの方向の誤りは、それぞれ別々に測り、それぞれ別に最適化する必要がある。駆動要因がまったく違うからだ。拒むべきときに拒む能力は、明確な境界宣言と権限検証の前置で上げる。誤って拒む率は、クリアな問いの言い換えと「まず試して、必要なら降格させる」戦略で下げる。2つをまとめて「拒答の正確さ」という1つの指標にすると、使えない情報がすべて消える。
プローブの設計:制御された撹乱で断点を探す
通常の評価で分かるのは「この分布で通過率はいくらか」だ。それには答えられない。「どんな条件下で破綻し始めるのか」には答えられない。後半のほうが、能力境界が本来関心を持つ問いである。断点を見つける基本手は制御された撹乱だ。一度に1つの変数だけを変え、指標がどう動くかを見て、その変数に沿って1本の曲線を走査する。
この曲線の価値は、まったく異なる3つの故障モードを区別できるところにある。緩やかな線形減衰は、能力が衰退しているが予測可能だ。资源を増やすことで補える(思考予算、サンプリング数の増加、タスク分割)。相転移点が存在することは、明瞭な閾値があり、そこを越えると急激に崩れることを意味する。この場合はリソースを足すのではなく、タスク形式を変える必要がある。完全に平坦な崖、つまり微小な撹乱一つで通過率がゼロになる場合は、能力を欠いているのではなく、形式や解析の層の脆さを指していることが多い。この種の問題は、たいてい良いプロンプトひとつで直る。3つのモードは3つのまったく異なる対処に対応する。総スコアだけを出していると、自分がどれに直面しているのかは永久に判別できない。
撹乱次元のチェックリスト
実用的な撹乱次元は次の数種類だ。1つずつ走らせてみる価値がある。
- 規模の撹乱:入力長、制約の数、ツールの数、ターンの深さ、コンテキストに入れる干渉素材の本数。
- 構造の撹乱:要求される出力形式の階層深度、フィールドの数、入れ子関係の複雑さ。
- 表現の撹乱:同義の言い換え、語順の変更、語調の変化、能動文を受動文へ変更、口語的な表現の追加。
- 情報の撹乱:重要な情報を冒頭から中間や末尾へ移す、中間が高類似度の干渉項を挿入する、重要な情報を複数箇所に分散する。
- 対抗の撹乱:材料に正解と矛盾する記述を入れる、多輪の中で前に出した約束を取り消す、問いの前提を誤りにする。
- パラメータの撹乱:サンプリング温度を変える、思考予算を変える、最大出力長を変える。
各次元で撹乱の強度を3つか5つ取れば、傾向は十分に見える。密走査は不要だ。密走査のコストは高く、たいてい3点から5点で「緩やかな減衰」と「崖」を区別できる。
boundary case の作り方
境界サンプルは、ランダムに掘り出した難サンプルではない。能力の断点のすぐ近くに意図的に構築するサンプルである。作り方には汎用的な手法が4つある。
二分収斂法。まず極端な2サンプルを見つける。一つは安定して通り、一つは安定して失敗する。どちらが「失敗」に必要なのか、長さなのか、制約数なのか、情報の位置なのかを判定する。一度では判別できなければ、複数の次元で同時に内挿して、分界線がどこにあるかを探す。二分を繰り返せば、境界は通常浮かび上がる。
最小反例化。失敗サンプルを1つ選び、成分を1つずつ削る(制約を1つ削除する、干渉素材を1つ削除する、長さを半分にする)。通過するまで繰り返す。残った最小の構造が、この能力境界の形である。この方法の副産物は非常に価値が高い。本番でどの入力成分を必ず止めなければならないかを直接教えてくれるからだ。最小反例が商品の中にまだ出現するなら、リスクは残っていることを意味する。
対照双子の方法。入力を2つ作る。重要な1つの成分以外はすべて同じにする。一つはその成分あり、もう一つはなしとする。通過率の差が、その成分の限界的な寄与である。この方法は「このサンプルがもともと難しい」という交絡を排除できる。
対抗生成に人手の収束を加える。モデルで特定の種類の失敗を誘発しそうな入力を一括生成し、実際に難しすぎないものを人手で絞り込む。このステップはサンプルプールをすばやく増やせる。ただし人手による篩は残さなければならない。自動生成された「難しいサンプル」には、割合の大きい部分が奇妙な言い回しのままで、本質的に能力の上限に触れていないものが多いからだ。
再利用できるプローブ実行器
以下のコードはプローブ実行器の骨格である。中心的な責務は、「撹乱 → 実行 → 採点 → バケット単位の集計へこのループを標準化することにある。。次元ごとに別々のスクリプトを書くと結果が比較できなくなるのを避けるためだ。
#encoding:utf-8
from dataclasses import dataclass, field
from typing import Callable, Dict, List
@dataclass
class ProbePoint:
"扰动坐标系里的一个点"
dim: str #扰动维度名,例如 "input_len"
level: int #该维度的强度档位
payload: Dict #构造好的实际请求参数
bucket: str = "default"
@dataclass
class ProbeResult:
dim: str
level: int
passed: bool
judge_scores: Dict = field(default_factory=dict) #保留原始判分,不只留对错
def run_axis(probe_points: List[ProbePoint],
invoke: Callable[[ProbePoint], dict],
judge: Callable[[ProbePoint, dict], tuple],
repeats: int = 3) -> Dict:
"沿单个维度扫一遍,按强度聚合"
agg = {}
for p in probe_points:
outcomes = []
raw = []
for _ in range(repeats):
out = invoke(p)
ok, detail = judge(p, out)
outcomes.append(ok)
raw.append(detail)
n = len(outcomes)
agg.setdefault(p.level, []).append({
"n": n,
"pass_rate": sum(outcomes) / n,
"samples": raw,
})
return agg
def find_breakpoint(agg: Dict, hi: float = 0.90) -> int:
"返回通过率首次跌破 hi 的档位;找不到返回 -1"
for level in sorted(agg):
rows = agg[level]
cnt = sum(a["n"] for a in rows)
rate = sum(a["pass_rate"] * a["n"] for a in rows) / cnt
if rate < hi:
return level
return -1
このコードには3つの意図的な設計がある。第一に、採点は真偽値だけでなく生の明細を残す。言語モデルによる採点から得られる連続スコアは、二値化した後より情報量が多いからだ。「完全に誤っている」と「結論は正しいが理由が誤っている」を区別できる。この2つに対応する対処はまったく異なる。
第二に、各点は既定で3回繰り返す。精度を稼ぐためではない。variance 自体を見えるようにするためだ。ある強度の水準で通過率が繰り返しの間で大きく跳ねるなら、そこは能力の境界の近くにいるということだ。その点のスコアで決定を下すべきではない。
第三に、断点の探索は関数として分離してある。「断点がどこか」は繰り返し議論し、共有すべき結論だからだ。スクリプトの print 出力に散らばらせるべきではない。モデルや提示語を変更するたびに軸をもう一度走らせ、断点をもう一度特定する。その曲線そのものが、バージョン管理できる資産になる。
プローブの結果を決定表にする
プローブを走らせたら、断点という数字1つだけ覚えて서는いけない。それを決定表に翻訳する。この表が、以降のすべてのアーキテクチャ判断の入力になる。形式はおおむね次のようになる。
| 次元 | 現在の断点 | 業務の入力分布 | リスク | 対処 |
|---|---|---|---|---|
| 入力長 | 約 1.2 万 token 以降、集約系の問題で崩れはじめる | 95% が 3 千未満 | 低 | 何もしなくてよい。ただしただし長さの監視を入れる |
| 制約の数 | 4 条を超えると形式の遵守が明確に落ちる | 通常 6 から 8 条 | 高 | 制約を前出しでマージするか、検証で遮断する |
| ターンの深さ | 約 12 ターンで初期の制約を忘却しはじめる | 長いセッションで 40 ターンに達する | 高 | 重要な制約を状態として外部に置く |
| ツールの引数 | 自由テキストから数値を抽出すると誤り率はおよそ 1 割 | 高頻度 | 中 | 型検証と二段確認を追加する |
| 拒答 | 権限超過時の拒答はまずまず。情報不足のときは推測に走る。 | 高頻度 | 高 | 情報不足のときは生成ではなく質問に戻す |
表の数字は例示にすぎない。具体的な値は実測に依存する。しかし重要なのは表の構造のほうだ。「モデルが強いか」という問いを、どの次元で、どんな形式で、どれだけのリスクがあり、どんな対処が必要かへと翻訳している。1行目は長さがボトルネックではないことを示す。だからコンテキストを広げるための予算を使う必要はない。2行目と3行目は本当の問題が状態管理にあることを示し、対処の方向はモデルの交換ではなくアーキテクチャの調整になる。この表があれば、選定の議論は趣味の争いからエンジニアリングの判断に変わる。
採点の方法そのものが、測られる対象である
評価の中で最も忘れられやすい事実は、測定の道具そのものに誤差があり、その誤差は測ろうとしている差より大きい場合が多いということだ。モデル A がモデル B を 2 ポイント上回ったと気づいたら、最初に確認すべきは採点器が、この 2 点を安定して区別できるかどうかだ。改善を祝うのではない。採点のノイズの標準偏差が 5 ポイントある採点器は、2 ポイント差を決して測り取れない。
判分モデル(LLM-as-judge)の4つのバイアス
生成結果に言語モデルで点をつけるのは、いま最も実用的な手法だ。コスト低廉、網羅が広く、人手判断との相関もまずまずだ。ただし4つの体系的なバイアスがあり、それぞれを見分け補正しなければならない。
位置バイアス(position bias)。比較対象の2つの出力を同時に提示したとき、判分モデルは内容と無関係に前に置かれたほうを好む。成対比較(pairwise)では、一定のオフセットとして現れる。A が先なら、A が何ポイントか多く勝つことだ。補正は各サンプルについて双方向で評価して順序を入れ替えること。2回の判断が一致したサンプルだけを数える。代償は採点コストが2倍になることだが、成対比較にはこの出費が要る。
長さバイアス(length bias)。判分モデルは長い回答ほど高く点をつける傾向がある。長さと表面的な完全さの相関が高いからだ。これは有害な最適化方向を生む。提示語の調整が無意識に「より長く書く」方向へ流れる。本番のユーザーが求めているのは、より短い答えだ。検証は簡単だ。優劣は分かっているが長さが反対になっているサンプル群で判分器を測る。長い側を判分器が選んでいるなら、バイアスは存在する。
自己選好(self-preference)。あるモデルを判分器に用いて、それ自身のファミリーの出力を評価すると、系統的に高く出る。異なるファミリーを比べればこの問題は消えるが、日常の評価に同じファミリーのモデルを使っている場合、見えている「向上」の一部は自己選好による偽像にすぎない。
表面の一致性バイアス。判分モデルは表面的な特徴に流されやすい。出典を引用していれば加点する。リスト構造を使っていれば加点する。自信러운口調なら加点する。これらの特徴と実際の正しさには必然的な関係がない。判分器はこれに抵抗しにくい。緩和策は、採点プロンプトの中でこれらの次元を明示的に分離して個別に点をつけることである。全体の「良い/悪い」を1回で判定させるのではなく。
較正:判分器を較正すべき計器として扱う
正しい方法は先に較正してから使うことだ。較正セットの規模は大きなものが要らない。人手で丁寧にラベル付けしたサンプルが20から30条あれば通常は足りる。その目的はバイアスの向きを見つけることであり、精度を精密に推定することではないからだ。以下は較正と双方向比較の実装の骨格である。
#encoding:utf-8
import random
from typing import List, Tuple
def bidirectional_compare(sample, judge) -> Tuple:
"交换顺序各判一次,只有两次一致才算有效"
vd = {}
for mode in ("A_first", "B_first"):
x, y = (sample["x"], sample["y"]) if mode == "A_first" else (sample["y"], sample["x"])
vd[mode] = judge(sample["prompt"], x, y)
first, second = vd["A_first"][0], vd["B_first"][0]
if first == "A" and second == "A":
return ("A", 1.0, vd)
if first == "B" and second == "B":
return ("B", 1.0, vd)
#两次不一致:该样本判为平局,不计入胜负统计
return ("tie", 0.0, vd)
def calibrate(judge, gold: List[dict]) -> dict:
"gold 每条含 gold_winner 与可选的 long_sample_name"
n = agree = pos_collapse = len_resist = len_n = 0
for g in gold:
win, _, vd = bidirectional_compare(g, judge)
n += 1
agree += (win == g["gold_winner"] or win == "tie")
#位置偏差探测:两次都判给先出现的那个
pos_collapse += (vd["A_first"][0] == "A" and vd["B_first"][0] == "A")
if g.get("long_sample_name"):
len_n += 1
len_resist += ((win == g["gold_winner"] or win == "tie")
== (g["gold_winner"] == g["long_sample_name"]))
return {"agreement": agree / n, "positional_collapse_rate": pos_collapse / n,
"length_bias_resistant": len_resist / max(1, len_n)}
def compare_with_ci(wins, n_boot=2000, seed=7):
"成对比较的胜率与自助法置信区间"
rng, n, boots = random.Random(seed), len(wins), []
p = wins / n
for _ in range(n_boot):
boots.append(sum(1 for _ in range(n) if rng.random() < p) / n)
boots.sort()
return p, boots[int(0.025 * n_boot)], boots[int(0.975 * n_boot)]
設計上の要点がいくつかある。不一致なら引き分けという規則が最も重要だ。位置バイアスの直接的な現れは、2回の判断が反転することだ。それを 一方をそのまま採るより、引き分けと判定するほうがよほど誠実である。代償は有効サンプル数が減ることだ。だから判分器の予算は双方向比較を優先して配るべきである。
較正結果は参考情報ではなくゲートとして使う。位置崩壊率がしきい値(たとえば2割)を超えたなら、その判分器は成対比較に使えない。データ収集を続けながら使うのではなく、採点方式を変えるか判分モデルを変える必要がある。
勝率には必ず confidence interval を付ける。下の関数がなぜ必要かを見よう。2つのモデルに各200サンプルを走らせ、勝率0.52と0.55なら差があるように見える。しかし bootstrap 法での区間が大きく重なる可能性が高い。この状態で「新モデルのほうが良い」と宣言するのは、純粋な自己欺瞞である。新しいバージョンが本当に良いかを見るには、区間が重ならないかを見るか、標本対照検定を行う。同一のサンプル群に対する成対比較なら、標本対照検定の検出力は独立した2群を比べる場合より格段に高いからだ。
言語モデルで採点してはいけない場面
判分器が最も弱いのは、外部知識が要る場面と、精密な照合が要る場面である。具体的な境目は次のようになる。
- 決定的な検査で判定できるものは、決まって判分器を使わない:出力が合法な JSON か、フィールドが揃っているか、数値が参照解答と厳密に等しいか、集合が一致しているか。この種の判定は評価全体の多数を占めるべきである。そうでなければ、自動化できるものに課金しつづけ、ノイズを持ち込むことになる。
- 外部事実との照合が要るものは、検索と引用の照合を使う:生成された内容の事実的主張が、与えられた材料の中で裏づけられるか。この種は半自動化できる。まずプログラムで主張を抽出し、人手または判分器で照合し、最後に自動フロー自体の精度をサンプリングで評価する。
- 多段実行の結果が要るものは、実行結果で採点する:コード系、SQL 系、ツールオーケストレーション系のタスクでは、正解は「動作して、結果が期待どおり」であるべきだ。プログラムに実行させるほうが、モデルに判定させるより1桁信頼できる。
- 本当に主観的なもの(口調、有用性、よさ)だけを判分器に渡す。かつ、双方向比較と定期的な人手による抜き取り検査を併用して行う。
この境目の背後には、より重要な原則がある。eval set の品質上限を決めるのは採点方式であって、サンプルの品質ではない。丁寧に設計した1000本のサンプルでも、弱い判分器で採点すれば測定精度は、決定的な検査で採点した100本のサンプルに及ばないことがある。サンプルの作業に手を入れる前に、まず採点できる部分を決定的にしておくべきである。
variance と統計の視点:1つのスコアに意味がない理由
評価報告書で最も頻出する現れ方は「通過率 87.3%」だ。この数字にはほとんど情報量がない。測定精度がどれだけかも、次の実行でも同じ数字になるかも教えてくれない。スコアに意味を持たせるには、varianceとconfidence intervalを併せて報告しなければならない。
variance の3つの出どころ
観測される variance は、互いに独立な3つの部分から来る。混ぜてしまうと問題を特定できない。
- サンプリングのランダム性:温度パラメータによって、同じ入力でも出力が変わる。モデル自体の性質であり、下げられる(事実上の決定的なサンプリングに近づければよい)。0まで下げるとモデルが繰り返しのループに入り、質が低下することがある。
- 採点のランダム性:判分器そのもののゆらぎ。上で、その大きさと補正のしかたをすでに論じた。
- 標本の抽出誤差:抽出したサンプル群が実際の分布を表していない。この項目こそ、通常の意味で「サンプル数が足りない」と言っているやつであり、eval set を大きくすることで緩和できる唯一の項目である。
3つの対処のかたはまったく違う。サンプリングのランダム性を下げるのはパラメータ調整の問題で、採点のランダム性は双方向比較とゲートで制御し、抽出誤差はサンプル数と層別で対処する。3つを区別する最も簡単な実験は、eval set を固定し、判分器を固定し、サンプリングだけを数回繰り返すことである。通過率の揺れの幅を観察する。この幅が純粋にモデルのサンプリングから来る variance であり、単一点スコアの固有ノイズの下限を与える。この幅より小さい差分はまったく信用できない。
見落とされがちな variance の出どころがもう1つある。コンテキストと外部サービスの非決定性だ。eval set が実際の検索に依存していて、検索インデックスが2回の実行の間で変化するなら、2回のスコアの差分は完全にインデックスから来ている可能性がある。対策は評価時にスナップショットインデックスを使うことか、検索結果を eval のサンプルに事前に固定しておくことである。検索結果を固定するほうが厳格な做法で、eval を「純粋な生成能力」の評価に変える。検索の品質は別の評価が要る。
サンプル数は結局どのくらいか
この問題に万能な答えはないが、落とし所のある判断基準がある。confidence interval を計算し、それが意思決定に必要な精度まで狭いかを見る。つまり、サンプル数を先に決めて誤差を計算するのではなく、「どの程度の誤差を許容できるか」を先に問い、標本数を逆算する。
粗い直感はこうだ。通過率の推定誤差を2パーセント未満に抑えたいなら、通過率が中間の範囲にあるとき、数百サンプル程度の量が必要になる。2つのバージョンの間の2〜3パーセントの差を区別したいなら、通常はもっと必要で、千の桁までいく。この見積もりは通過率によって変わる。通過率が高いほど、あるいは極端な値に近いほど、必要なサンプル数は増える。通過率が0や1に近い領域では、失敗数を報告するほうが実用的なことが多い。分母が小さいと通過率の変化が非常に不安定になるからだ。
「サンプル数を揃える」よりも効く做法が成対比較(paired comparison)である。2つのバージョンが同じサンプルの群を走らせるなら、各サンプルについて「旧版が正、新版が誤」と「新版が正、旧版が誤」の2つのカウントを記録し、その不一致の部分だけを見る。大部分が一致したサンプルは、識別能力に何も寄与しない。それらを除くと、等価なサンプル数は大きく増える。固定の eval set で新旧のバージョンを比べる方が、異なる2つの eval set で比べるより常に敏感であるのは、このためである。
計算と提示
以下のコードは variance の3つの出どころを分離する方法と、Wilson 区间(小さな比率や極端な比率で正規近似より安定している)の実装である。
#encoding:utf-8
import math, statistics
from typing import List, Dict
def wilson_interval(k: int, n: int, z: float = 1.96) -> tuple:
"Wilson 区间,通过率接近 0 或 1 时比正态近似稳健"
if n == 0: return (0.0, 1.0)
p = k / n
denom = 1 + z * z / n
center = (p + z * z / (2 * n)) / denom
half = z * math.sqrt(p * (1 - p) / n + z * z / (4 * n * n)) / denom
return (max(0.0, center - half), min(1.0, center + half))
def decompose_variance(runs: List[Dict]) -> Dict:
"每条 run 含固定评测集的分数与仅重复采样的分数,用来分离两类方差"
var_total = statistics.pvariance([r["score"] for r in runs])
var_sampling = statistics.pvariance([r["score_resampled"] for r in runs])
var_subsample = max(0.0, var_total - var_sampling)
return {"var_sampling": var_sampling, "var_subsample": var_subsample,
#单个分数的固有噪声下限,用标准差表达
"sd_single_run": math.sqrt(var_sampling + var_subsample)}
def mcnemar_counts(pairs: List[tuple]) -> Dict:
"成对比较:只保留不一致的样本对做符号检验"
b = sum(1 for old, new in pairs if old and not new) #旧对新的错
c = sum(1 for old, new in pairs if new and not old) #新对旧的错
n = b + c
if n == 0:
return {"significant": False, "reason": "no_discordant_pairs"}
#符号检验的精确二项 p 值(无校正的快速版本)
from math import comb
k = min(b, c)
p = 2 * sum(comb(n, i) for i in range(0, k + 1)) / (2 ** n)
return {"old_only_right": b, "new_only_right": c,
"discordant": n, "p_value": min(1.0, p),
"significant": p < 0.05}
注意 decompose_variance の設計である。固定した eval set(サンプリングだけを繰り返して、サンプリング由来の variance を隔離するため,用来)和、部分集合で再走させた記録(抽出誤差を推定するため,用来)の両方を要求する。この二重の構造は面倒に見えるが、「モデルのランダム性」と「サンプルの代表性の欠如」という2種類の失敗を区別できる唯一の測定法である。そしてこの2つの対処はまったく違う。前者はサンプリングの調整か複数回からの最良値の採用で対処し、後者は eval set の拡充と層別で対処する。
必ず再走させるべき変更
古いスコアを即座に無効化する変更がいくつかある。再走が必須である。
- モデルのバージョンまたはサプライヤの変更:同じサプライヤによる無通知のバージョン更新を含む。後者は特に危険だ。通知がないまま挙動が変わることがある。
- 提示語テンプレートの変更:無害に見える表現の調整,甚至は例の追加削除を含む。
- サンプリングパラメータの変更:温度、思考予算、最大出力長のいずれの変化も含む。
- 検索の設定またはインデックス内容の変更:入力分布とコンテキスト長分布の両方を同時に変えてしまう。
- ツール集合またはツール説明の変更:ツール説明の表現はツール選択に直接影響する。この種の変更の影響度は直観をはるかに超える。
- 評価の判分器の変更:判分器を换代すると新旧のスコアは比較不能になる。ベースラインは必ず再走させる。
- 業務分布の漂移:システムが変わらなくても入力分布が変わり、古い断点の位置は成り立たなくなる。この種の漂移は定期的に監視すべきである。監視しないと、最近の自分の変更が退化の原因だと誤診する。
これと対称の規律がもう1つある。見栄えのためだけに再走させてはならない。スコアが不好看だから設定を調整して再走させ、通るまで繰り返す、ということをしたら、サンプリングノイズで結論を選んでいることになる。規則は単純である。あらかじめ決めておいた手順どおりに再走させるか(繰り返す回数を、中位数を取るのか平均を取るのかまで手順に書いておく)、あるいは最初の結果をそのまま受け入れるか、どちらかである。
評価からエンジニアリング判断へ:能力境界はどうアーキテクチャ制約になるか
評価の成果物はレポートではなく、明確なアーキテクチャ制約の集合であるべきである。各次元で測った断点は、具体的な対処の決定に対応していなければならない。そうでなければ測定は価値に変わっていない。この節では、これまでのすべての測定を4種類の対処行動にマッピングする。
対処その1:必ずモデルを差し替える工程を明示する
モデルの差し替えはもっとも費用がかかる手段である。したがって、モデル側がまさにボトルネックであり、しかも業務側で他の方法で吸収できない次元のために残しておくべきである。ある次元がモデルの差し替えに値するかは、2つの条件を同時に満たすかどうかで判断する。
第1に、その次元が自分の業務の入力分布に高頻度で現れること。極端な入力のときだけ崩れる能力が、自分のトラフィックにほとんど出現しないなら、それは選定に影響させない。存在しない問題に対して課金していることになる。逆に、長入力のときだけ誤る能力で、自和产品がすべて長入力なら、それが首要のボトルネックである。
第2に、その次元の失敗を下流で緩和できないこと。失敗したあと簡単な決定的な検査で止められるなら、モデル側に投資する意味は薄い。逆に、失敗が静かに下流へ流れていくなら(抽出でフィールドが1つ欠ける、誤った事実を1つ生成する)、モデル側か検査側で強く処理しなければならない。
この2条件を同時に満たす次元だけが、モデル差し替えの根拠になる。実践では、この2条件を満たす次元は実務上それほど多くはない。ARGETした多くの評価のあとで判明するのは、真のボトルネックが「モデルが弱いから」ではなく「コンテキストの管理得当でないから」あるいは「検査層が欠けているから」であるという事実である。この結論は意外だが非常に常见で、評価の既定の仮説として疑う価値がある。
対処その2:必ず検査を追加する工程を明示する
構造化出力、指示の遵守、ツール引数の抽出という3つの次元は、より強いモデルに頼るのではなく,ほぼ常に決定的な検査で守るべきである。理由は3つある。
- 検査のコストはモデル能力向上のコストより圧倒的に低い。JSON schema の検査は関数1回の 호출である。モデル能力の向上は数倍の推論コストとレイテンシになりうる。
- 検査の信頼性は構造的なものである。検査を通れば100%確定であり、確率的な失敗が存在しない。モデル側の改善はつねに確率的なものである。
- 検査の失敗を良いプロダクトの挙動に変換できる。schema が不正なら1回リトライする、規則ベースの解析にフォールバックする、「もう少し情報をください」とユーザーにはっきり伝える。いずれも、自信はあるが形式が誤った回答よりはるかによい。
検査層を置く正しい位置は、モデル出力の後で、下流が消費する前であり、モデルの内部ではない。典型的な3層の構成は次の通りである。第1層は制約デコーディングで文法的な合法性を保証する。第2層は schema と型の検査で、失敗したらリトライしてエラー情報をモデルに返す(ここ自体が多輪の指示遵守のテストになるので、並行して監視するとよい)。第3層は業務ルールの検査で、金額の合計が合っているか、参照したフィールドが本当にソースデータに存在するか、などを確かめる。
第3層はしばしば省略されるが、これが静かな失敗を捕まえる唯一の手段である。意味が正しく、形式も正しいが、内容が事実と食い違っている場合、前2層では止められない。
対処その3:必ず人がを受け持つ工程を明示する
人の受け持ちは失敗ではない。ある次元にとってはもっとも経済的な選択である。人が必要になる典型的な状況は4つある。
- 低頻度・高リスク:発生率は千分の一未満だが、一度間違えれば取り返しがつかない操作。評価がここでモデルの成功率が 0.985 だと教えてくれたなら、3層の検査を追加しても残る残余リスクには人が受け持つ必要がある。
- 採点そのものが主観的である:ある品質次元(法律条文の妥当な解釈、複雑な紛争の公平性の判断)は、人同士でも安定した一致した答えがない ,这类场景里「モデル答案 + 人工复核」是唯一可靠的结构,而且应该把模型定位成初筛器而非决策器。
- ユーザーの信頼に敏感である:医療、法律、財務の助言の場面では、モデルの正答率が高くても、人による複核がなければコンプライアンス上必ずしも可能とは限らない。
- モデル自身が信頼できないというシグナル:入力自体の情報量が足りないとき(ユーザーの説明が曖昧、添付が欠けている)、最良の挙動は生成ではなく質問である。この種の「言うべきことを言う」場面を、人に当てるか、強制的な質問に変えるのは、モデルを差し替えるよりずっと有用である。
人の受け持ちの設計の要点は、人をパイプラインの不可視のボトルネックにしないことである。正しい方法は、モデルが確実に失敗した場所(検査不通過、リトライ枯渇、信頼度がしきい値未満)に人を置くことである。人に異常経路だけを処理させ、同時に異常率が十分に低いことを保証する。10% のトラフィックが人に落ちるなら、それは受け持ちではなく、システムを半自動化へ退化させたことになる。
対処その4:アーキテクチャを変えるべき問題とモデルを変えるべき問題を明示する
ある種類の問題の正しい解き方は、まったくモデル側にない。この種類の問題の共通の特徴、コンテキストが信頼して維持できる範囲を超える状態をモデルに維持させるという要求である。具体的な形式は次の通りである。
- 数十ターン前に設定した制約をモデルに「覚えて」いてもらうこと。対策は、制約を明示的な状態オブジェクトとして外部に出し、毎ターン注入し直すことである。モデル自身に覚えさせることを期待しない。
- 1つのタスクの中で、互いに制約し合う10来个のサブ目標を同時に維持させること。対策は、明確に境界のあるサブタスクに分割し、各サブタスクに独立した1回の生成を割り当てることである。
- 生成時に、モデル自身が前に書いた長い内容を繰り返し参照させること。対策は、まず構造を書いてから中身を入れること。「自分自身を参照する」を「データを参照する」に変える。
- 長い流れの中で、一貫した文体や用語を維持させること。対策は、文体制約を変換層のルールにすること。生成層の期待にすることではない。
この段階で評価が価値を持つのは、断点の具体的な位置を与えてくれることで、それによってアーキテクチャ改造の優先順位がどこかを見極められるからだ。さっきの決定表で「ターンの深さ」の行が、モデルが10来个のターンの后期的初期の制約を記憶しはじめると示しているなら、答えは明確である。モデルを差し替えるのではなく、状態を外部に出しなさい。逆に、断点が「2〜3ターンという短くてコンテキストも短い段階で、モデルはすでに明示的な形式の制約に背きはじめている」と示しているなら、それはモデル能力そのものが足りないということで、この場合にこそモデルの差し替えや制約デコーディングへの切り替えが意味を持つ。
決定を実行可能なルールとして書く
4つの対処は、最終的にはシステム上のいずれかのルールに帰着する。実行可能な形式はおおむね次のようになる。
#encoding:utf-8
#每条规则由"触发条件 -> 动作"构成,来自能力边界评测的结论
POLICY = {
#来源:约束数量维度断点约 4 条,格式遵循在超过后明显下降
"constraints_over_4": "merge_constraints_before_call",
#来源:轮次深度断点约 12 轮,早期约束开始被遗忘
"turn_depth_over_12": "externalize_state_and_reinject",
#来源:长输入聚合类问题在较长上下文后塌陷
"input_len_over_12000": "route_to_long_context_path",
#来源:工具参数抽取在自由文本数字上有残余错误
"tool_args_from_text": "type_check_then_confirm",
#来源:信息不足时模型倾向猜测而非追问
"insufficient_info_detected": "ask_user_not_generate",
}
def apply_policy(features: dict, base_call):
"features 是运行时提取的输入特征,决定走哪条路径"
actions = []
if features.get("constraint_count", 0) > 4:
actions.append(POLICY["constraints_over_4"])
if features.get("turn_depth", 0) > 12:
actions.append(POLICY["turn_depth_over_12"])
if features.get("input_len", 0) > 12000:
actions.append(POLICY["input_len_over_12000"])
if actions:
#把动作拼进提示结构,而不是靠模型自觉遵守
actions.append("inject_structured_preamble")
return base_call(features, actions)
この設計の向きに注意してほしい。これらの規則はいずれもモデルに「自觉」を期待していない。制約数が超えたらオーケストレーション層が制約をマージするのであって、プロンプトに「制約が多いときは形式を優先してください」と書き込むのではない。ターンが深くなったらオーケストレーション層が状態を注入するのであって、モデルに覚えさせることを期待しない。この原則は繰り返す価値がある。評価によってモデルにできないと分かったことは、モデルにやらせるな。決定的なコード層に任せろ。
継続的な評価:回帰の発見をパイプラインの一部にする
一回限りの評価は一回限りの資産である。本当に価値があるのは、変更のたびに自動的に「退行したかどうか」を教えてくれる評価パイプラインである。そしてまさにここで、能力境界像の投資対効果が最も高い。既に各次元の断点の位置を知っているからだ。だから回帰検出を方向のあるものにできるのである。毎回すべての次元であらゆる変化を捉えるために eval set 全体を走らせる必要はない。断点の近辺で十分な密度を保てばよい。
層別化:速い系と遅い系の2本のパイプライン
継続的な評価は層別化しなければならない。そうしないとコストが高すぎて継続できない。実用的な分け方は3層である。
| 層 | サンプル数 | 起動のタイミング | 用途 |
|---|---|---|---|
| スモーク層 | 数十本 | コミットごと | 明らかな崩壊を捕まえる。形式と構造化出力が中心 |
| 回帰層 | 数百から千 | 毎日、またはマージごと | 各次元の断点探知。固定サブセットを使う |
| 全量層 | eval set 全体 | 毎週、またはリリース前 | 断点を再特定し、能力像を生成する |
スモーク層は小さく、対象を絞り、採点が確定していることが必要である。採点が確定していて、通ればパイプラインが壊れていないとわかるサンプルだけを置く。能力の問題を発見させるために使ってはいけない。回帰層が日々の主力である。調整に使わない固定のサンプル群で、各次元の撹乱軸を走らせ、断点の近辺にある水準が動いていないかを見る。全量層は重い仕事である。その産出物そのものが能力境界像であり、周期的に作り直す。
回帰検出の正しい判据
回帰検出でもっとも起こりやすい誤りは、ノイズをシグナルとして扱うことである。あるバケットの通過率が前回より2ポイント下がった,于是宣布してロールバックする——ところが、この2ポイントは完全にサンプリングのゆらぎの範囲内である。正しい判据は3つのことを同時に考えなければならない。差がノイズの下限に対して有意か、差があったサンプル対の方向が一致しているか、その変化が業務上重要か。3つすべてが満たされて初めて回帰と呼ぶ。
以下のコードは層別パイプラインの骨格であり、重点はスケジュールではなく回帰判定にある。
#encoding:utf-8
from dataclasses import dataclass
from typing import List, Dict
@dataclass
class BucketResult:
"某个维度某个档位的聚合结果"
dim: str
level: int
pass_rate: float
n: int
ci: tuple
def detect_regression(current: BucketResult,
baseline: BucketResult,
sd_single_run: float,
min_delta: float = 0.03) -> Dict:
"回归判据:超过噪声下限 + 超过业务重要度 + 区间显著不重叠"
delta = baseline.pass_rate - current.pass_rate #正数表示变差
exceeds_noise = delta > 2 * sd_single_run
matters = delta > min_delta
lo_cur, hi_cur = current.ci
lo_base, hi_base = baseline.ci
separated = (lo_base > hi_cur) or (lo_cur > hi_base)
is_regression = exceeds_noise and matters and separated
return {"dim": current.dim, "level": current.level,
"delta": delta, "exceeds_noise": exceeds_noise,
"matters": matters, "ci_separated": separated,
"is_regression": is_regression}
def sweep_all(current: List[BucketResult],
baseline_by_key: Dict, sd_single_run: float) -> List[Dict]:
"扫全部档位,只返回需要关注的项,按恶化幅度排序"
flagged = []
for c in current:
b = baseline_by_key.get((c.dim, c.level))
if b is None:
continue #新增档位没有基线可比,先积累
r = detect_regression(c, b, sd_single_run)
if r["is_regression"]:
flagged.append(r)
flagged.sort(key=lambda r: r["delta"], reverse=True)
return flagged
def gate_release(flagged: List[Dict], blocking_dims: set) -> Dict:
"只有属于高风险维度的回归才拦截发版,其余记录但不阻塞"
blocking = [r for r in flagged if r["dim"] in blocking_dims]
return {"pass": len(blocking) == 0,
"blocking": blocking,
"watchlist": [r for r in flagged if r["dim"] not in blocking_dims]}
ここには3つの意図的な設計がある。第一に、「ノイズ下限を超える」を明示的な判据として書き、sd_single_run を渡すことを要求している。この値は、さっき言った variance の分解から来るべきもので、主観で決めてはいけない。第二に、有意性の判据として confidence interval の非重なりを使っている。これは固定の阈値のハードコードより誠実だ。サンプル量に自動的に適応するからだ。第三に、リリースのブロックと観察のみを区別している。すべての次元の回帰をブロック対象にしてしまうと、ゲート全体がすぐに無視されるようになる。迂回されたゲートはゲートではないからだ。
もう1つの実践上の要点がある。ベースラインは追跡可能でなければならない。すべてのベースラインスコアは、どのモデルバージョン、どのプロンプトバージョン、どの eval set バージョンの下で生じたかを記録するべきである。そうしないと、3ヶ月後にある時点から曲線が落ちているのを見ても、その時点に何があったかまったくわからない。
本番からのフィードバックの還流
継続的な評価の最後の環は、本番で新しく現れた失敗を eval set に再び組み込むことである。しかしこの段階は非同期であり、レビューを伴うものでなければならず、自動昇格させてはいけない。本番のサンプルはまず候補プールに入り、帰属づけ(モデルの問題かパイプラインの問題か)と難易度の評価を経て、「新しい安定した能力次元を表している」と判定されたものだけが、正式な回帰サンプルに昇格する。
この規律の代償は、本番の問題がすぐにテストケースにならないことである。得られる利益は、テストセットが数週間で業務ノイズに埋もれないことである。実際にはその2つの間でバランスを取る必要がある。すでに複数回現れていて帰属ラベルが明確な失敗サンプルは高速経路を通すようにし、残りは完全なレビューを通す、という運用になる。
eval set 自体にもバージョン管理与汚染監視が必要である
しばらく動かし続けると、eval set は2種類の腐敗に直面する。どちらも隠れている。第一はサンプルの劣化である。業務が変わるにつれて、早期に追加したサンプルは少しずつ実際の入力を代表しなくなる。それでも走らせられ、明確な採点結果が出るので、静かに回帰用の集合に残り続け、総スコアを古い方向へ引きずり込む。第二は結論の陳腐化である。eval set の構成は変わらないが、その背後にある能力の前提が変わる。たとえば当初ある種類の入力を重視せず2本3本しか置いていなかったが、後からそれが主なトラフィックになった、という場合である。
対策は eval set 自体にバージョンとメタデータを持たせることである。各サンプルについて、eval set に入った時期、由来、帰属ラベル、現在タグ付けされた難易度水準、最後に再確認した日時を記録する。このフィールドがあれば、次の重要な問いに答えられる。ある時点のあとは一度も実際の失敗に現れなかったサンプル群はどれか。この種の見かけは、すでに解決された可能性と、もはや発生しなくなった可能性の両方があるので、確認する価値がある。継続的に減少しているものの削除されていない帰属ラベルはどれか。このラベルに対応する能力はアーキテクチャ改造で既に覆われた可能性があるので、重みを下げられる。
このフィールドがあれば、次の重要な問いに答えられる。ある時点のあとは一度も実際の失敗に現れなかったサンプル群はどれか。この種の見かけは、すでに解決された可能性と、もはや発生しなくなった可能性の両方があるので、確認する価値がある。継続的に減少しているものの削除されていない帰属ラベルはどれか。このラベルに対応する能力はアーキテクチャ改造で既に覆われた可能性があるので、重みを下げられる。新たに追加されたサンプルに特定の由来が偏っていないかも確認する。eval set が単一のチャネルに支配されている可能性である。
メタデータに加えて定期的な再訪の仕組みが必要である。一定期間ごとに eval のサンプルをランダムに1つovsk抽出し、業務に詳しい人に判断し直してもらう。今も重要か。採点の基準はまだ成り立つか。難易度の水準は正確なままだか。この過程は全サンプルをlius coveredする必要はない。1〜2割を抽查するだけで、系統的なずれを見つけるには十分だ。その価値は、「eval set が静かに腐敗している」という不可視の問題を、定期的で、責任者のいる検査項目に変えることにある。
分布の漂移:評価の重みを実際のトラフィックに追従させる
eval set が機能しなくなるもっとも常见な原因は、汚染ではない。時代に置いてかれることである。半年が wiser、上流のデータ源の構造が変わり、ユーザー集団の構成が変わり、プロダクトの入口 scenarios が変わり、そうして eval set が表す分布と実際のオンライン分布の間に系統的なずれが生じる。このときに見えるスコアはすべて「正確」のだが、正確に測っているのは存在しない問題を測っている。
この種のずれを検出するには、線上の入力を粗い粒度でクラスタリングするかテンプレート抽象して、各クラスタの構成比の変化を定期的に集計する。実務上は複雑なクラスタリングアルゴリズムは不要で、入力長でバケット分けする、特定種類のエンティティを含むかでバケット分けする、タスク意図でバケット分けする、このあたりでほとんどのドリフトは見つかる。重要な指標は2つある。各クラスタの構成比の絶対的な変化(かつて1%しか占めていなかった入力タイプが今や3割を占めるようになったようなもの)と、eval set カバレッジの変化(線上に eval set にはまったく存在しない入力タイプが現れたようなもの)。後者は特に重要で、eval が盲点の中で回っていることを直接に突き止めてくれる。
ドリフトを見つけたからといって eval set 全体をすぐ作り直すのはやめたほうがいい。高くつきすぎるうえ必要がない。実務的なやり方はサンプルを置き換えるのではなく重み付けを調整することである。既存のサンプルは基礎層として残して比較可能性を確保し、その上に新しい分布をカバーするサンプル層を積み重ねる。そしてスコアを報告するときには、全量スコアと線上分布で再重み付けした後のスコアの2つの数字を必ず並べて出す。前者はバージョン間の回帰比較に使い、後者は現在のシステムが実トラフィックに対してどれだけの性能を出しているかを判断に使う。
このやり方の副次的な効果として、ある入力クラスタが完全に消えたときに、レポートの中でその重み付き係数がゼロに近づくのが見えるようになる。そのうえで基礎層から取り除くかどうかを判断する材料がはっきりし、勘で片付ける必要がなくなる。eval set のメンテナンスのリズムは、データが変わる速さで決めるべきであって、カレンダーで決めるべきものではない。
オフラインスコアとオンライン指標の間のループ
eval 体制を組み上げると、ほぼ確実にこういう失望に突き当たる。オフラインの全次元のスコアは伸びているのに、オンラインのビジネス指標はまったく動かない。あるいは逆に、オンラインのユーザーからの苦情が明らかに増えているのに、オフラインスコアは正常を告げている。この2つの乖離はいずれも専用の説明を必要とする。さもないと eval システム全体は、数回あれば信頼性を疑われ始める。
オフラインは伸びるがオンラインが動かない3つのよくある理由
第一に、変更が業務が気にしていない次元に落ちている。eval set は8つの次元をカバーしているが、実トラフィックがある次元ではほとんど露出していないため、その次元の改善はユーザからまったく見えない。典型的な例は、multi-turn 一貫性を2週間かけて7割から9割に引き上げたが、ユーザは1往復で同じ問いを2回しないので何も変わらない。修正方法は、各次元に「業務重み」を付け、報告では高重みの次元の変化だけを強調することだ。
第二に、変更がエラーの一種から別のに挪した。総合スコアは横ばいあるいはやや上がったのに、エラーの種類が変わっている。かつては内容で間違っていたが、いまは形式で間違っている。あるいはかつては自動フォールバックが発動したが、いまは静かに 통과してしまった。次元ごとのスコアではこの種の変化を検出できないこともある。比較すべきなのは「通過率の分布」ではなく「エラーの結果の分布」のほうだからだ。だから通過率に加えて、eval レポートにはエラー種別の分布を項目として入れ、バージョンをまたいでその移動を比較する必要がある。
第三に、オンラインには eval がカバーしきれない要因が残っている。もっとも多いのはレイテンシとコストだ。ある変更で精度が2ポイント上がったが、首字レイテンシがユーザに知覚できるレベルまで悪化し、純効果はマイナスになった。あるいはコストが3倍になり、同じ予算で支えられるトラフィックが減って間接的にカバレッジが下がった。eval set にはレイテンシとコストの2列が必須である。さもないとオフラインの結論は本質的に不完全になる。
オンラインは動くがオフラインが動かない3つのよくある理由
第一に、スコアの分布が平均値に隠されている。全体の通過率は変わらないが、ある小さなクラスタの通過率が崩れた。そのクラスタはトラフィックに占める割合は小さいのに、たまたま高価値のユーザだった。まさに前に強調したバケット別のスコア出しを行っている理由である。総合スコアだけを見ていると、この種の退行はまったく見えない。
第二に、失敗がモデル側から下流に移った。モデルの出力は良くなったが、下流の解析やステート管理、あるいは業務ルールで新たな問題が生じた。表面上はユーザ側の失敗として現れるが、eval 上ではモデルの出力は正しい。判定方法は、モデルのその1回の出力だけを見るのではなく、線上で見つけた失敗サンプルの完全なトレースを見ることだ。
第三に、分布はすでにドリフトしているのに eval set は元のまま立っている。これは直前のドリフト監視に直接対応する。特点是、ある日を境にスコアがゆっくり乖離していくことである。そして実際の引き金側はトラフィックにあるため、特定のシステム変更と対応づけにくい。
階層検証で変更が本当に効いたかを確認する
どちらの乖離であっても、正しい対処は階層検証に戻ることに尽きる。まずオンライン指標の変化が本物かどうかを確認し(トラフィック構造の変化を排除し、統計的なノイズを排除する)、次にそれを特定の環節(モデル、プロンプト、検索、オーケストレーション、下流)のいずれかに帰属させ、最後にようやくオフラインの eval に戻って対応する回帰サンプルを作る。
このループの最後の環節が最も飛ばされやすい。すなわち、線上で見つけた新しい失敗を eval set に差し戻す作業だ。これを飛ばすと、同じ問題が繰り返して発生し、たびに調査はゼロから始まるになる。ここで触れた「本番フィードバックの還流」仕組みは、本質的にこの最後の環節が飛ばされないようにするための仕組みである。eval 体制の成熟度を測る物差しは、いま何点を出しているかではなく、発見された新しい問題のうち、どれだけが再発しなくなった回帰サンプルとして沈殿しているかにある。
2026年の進展:eval手法そのものが再構成されつつある
これまでの各節で述べた方法論は比較的安定している。しかしここ1〜2年、eval領域自体の方向にいくつかの進展があり、その共通点は従来の eval の「測りが不正確」という根本問題に正面から立ち向かうことだ。以下で扱うのは仕組みとトレードオフであり、具体的なランキングの数値や論文の結論には触れない。
方向1:動的evalと敵対的テストセット
従来 eval set の根本的な弱点は、公開されれば攻略されることにある。サンプルが固定されているということは、モデルは(学習段階での偶発的な適応か eval 段階での意図的な適応にしては)次第にその特定のサンプルに適応していく。その結果、スコアは上がるのに能力は上がらない。動的 eval の考え方は、テストセットそのものを時間とともに動く的にする。毎回の eval で大きなサンプルプールからその場で抽出するか、ジェネレータが事前に設定した分布制約に従ってその場で新しいサンプルを生成する。
敵対的テストセットは、「モデルの弱みを見つける」こと自体を継続的な最適化目標として据える。専用の訓練済みモデル(あるいは探索プロセス)が、被評価モデルを失敗させる入力を探し続け、見つけた失敗サンプルは自動的に eval set に入る。従来の「人間が難しいサンプルを一批作って固定する」という eval と根本的に違うのは、eval set が持続的に進化する対抗過程になることである。
この2つの方向は「eval set への過学習」という問題を解決するが、新しいエンジニアリング上の課題を持ち込む。eval の再現性が悪くなる。毎回サンプルが異なれば、2回のスコア差の中に「サンプル難度の変動」という新しい方差の要因が混入し、それはサンプリング方差よりずっと大きい。対策は、ジェネレータの乱数シードを固定し、十分な数のサンプルによってサンプル難度の変動を薄めることである。代償は eval コストの大幅な上昇になる。
エンジニアリングチームへの実務上の助言はこうだ。動的 eval は探索と新しい弱点の発見に使い、固定 eval は回帰検出とバージョンの判断に使う。役割が異なるので、互いに置き換えられるものではない。固定 eval の価値はまさにサンプルが一定であることにあり、どの変化もシステムに帰属させることができる点にある。
方向2:エージェント(agentic)な場面での長期eval
従来の eval が暗黙に前提としているのは「入力1回、出力1回」で、インタラクション全体がいずれも数千 token のうちに終わることだ。評価対象が数十ステップを自律的に実行できるエージェントになると、この前提は全面的に破れる。新しい困難は少なくとも5層ある。
- 成功判定が複雑になる:タスクは「目標には到達したが遠回りした」場合、「まだ届いていないがより価値のあることをした」場合がある。二値判定では大量の情報が落ちるため、達成度の連続的な譜で採点する必要がある。
- 単歩の小さな誤りは自己修復されうる:単歩精度95%のモデルでも、20歩のタスクでの全体成功率は0.95の20乗よりずっと低い。多くの単歩エラーが後続のステップで修正されるからだ。つまり単歩精度は長期的性能に対してほとんど予測力を持たず、区間全体を測る必要がある。
- 変動要因が増える:ツールが返すタイミング、外部状態の変化、時間に依存する入力など、同じタスクの再実行結果を変えてしまうものが多数ある。
- 失敗をステップに帰属させる必要がある:区間全体が失敗したとき、「どのステップで、何ゆえに」を特定することが成否判定より重要になるが、そのためには完全な実行トレースの保存が必要になる。
- コストが第一級の問題になる:長期 eval は token コストとレイテンシコストを増幅する。上限を設けない eval を1回走らせると、オンラインサービスより高くつくことがある。
長期 eval の実務的な落としどころはこうだ。タスクにステップ数と予算の二重の上限を設け、上限に達したら失敗として記録する(際限なく走らせない)。トレースを第一級の市民として保存し解析する(最終結果だけを残さない)。失敗したステップの位置の分布で報告する(全体成功率ではなく)。前者は直ちにどこを直すべきかを指し示すからである。そして最低限の実行可能成功率をゲートとして設ける。長期タスクの成功率はもともと低いので、絶対値がゼロに近い成功率どうしを比較しても意味がなく、比較の焦点は同一タスク上のバージョン間の相対的な差にあるべきである。
方向3:verifiable task と自動採点
これは先に上げた「judge 自体が信頼できない」という問題に対する最も徹底した応答である。核心的な洞察はこうだ。タスクの正しさがプログラムによって決定論的に判定できるなら、eval はいかなるモデルによる採点も要らない。
verifiable task の典型的な形態としては、テストケースの集合を与えられ、すべての assertion を満たすコードを書く、与えられた目標状態に対して操作列を生成し、終状態をステートマシンで検証する、与えられた制約の集合に対してすべての制約を満たす解を解き、solver で検証する、与えられたソースデータから schema に適合する結果集合を作り在校照器で照合する、などがある。これらの共通点は検証が可能であり、しかも検証コストが生成コストよりはるかに安いことである。
verifiable eval の価値は3つの側面にある。第一に、測定精度がほぼノイズのない水準まで上がり、「2ポイント向上した」という判断が初めて信頼できるものになる。第二に、難度を自動調整できる。assertion を1本増やす、制約を少しきつめる、テストケースを1つ補う、それだけで難度が上がり、しかもこの調整に「この問題は難しいか」という人間の判断が不要になる。第三に、探索を自動的にサポートする。難度の異なる変種を大量に自動生成し、モデルの能力の切れ目を突き止められる。
制限も明確である。verifiable task は本質的に「正しさが判定できる」能力しかカバーできない。文章のスタイル、トーンの適切さ、有用性の度合い、一部の推論の妥当性まではこのように検証できない。したがって verifiable eval は従来の eval を置き換えるのではなく、従来の eval のうち本来は自動判定できたのに、弱い judge で採点していた部分を置き換えるのである。そしてこの部分は多くのチームの eval set で相当な割合を占めている。
もう1点注意すべきことがある。verifiable task は「刷られる」やすい。assertion 自体が緩く書かれていれば、モデルは assertion を通るが明らかに誤った解を生成するかもしれない(すべての assertion はチェックしたものの、本来チェックすべき分岐を1つ落とした、など)。対策は assertion 自体にも敵対的なレビューを与えることだ。専ら現在のテストを通る誤った解を生成しようとするモデルを1つ用意し、成功できるかを試す。この「メタテスト」のプロセスは assertion セットの設計段階で行うべきである。
方向4:evalと実分布の整合
よりエンジニアリング的だが同様に重要な方向として、eval の入力分布を直接実トラフィックに固定し、分布ドリフトを自動検出することがある。やり方は通常、線上の入力をクラスタリングあるいはテンプレート抽象によっていくつかのクラスタに分け、各クラスタの構成比の変化を定期的に集計し、あるクラスタの構成比が明確に上昇または下降したら業務分布が変化したということなので、既存の eval の重みを調整する必要がある、と判断する。
この方向の価値は、それまで解けていなかった問いに答えるところにある。あなたの eval set はいまも現在の業務を代表していますか。eval set の失効は汚染によるものではなく、大抵は時代に取り残されたことによる。事業の形が変わり、ユーザ集団が変わり、上流のデータ源が変わったにもかかわらず、eval set は半年前の分布にとどまったままになっている。定期的な再サンプリングと分布の整合は、eval の有効性を保つための必須のメンテナンスである。
一式のものを落地させる順序
ここまでの内容をすべて読んだうえで、いちばん腐り易い失敗は、全部を一度にやろうとすることだ。eval set の構築、6つの次元の摂動軸、judge の較正、継続パイプラインの構築を、それぞれまとめて片付ける方法だが、これはほぼ確実に失敗する。前提となる初期投資が、多くのチームが1回の状況把握のために払ってもよいと思う予算をはるかに超えているからだ。しかもこの途中に目に見える成果がないため、途中で打ち切られやすい。
順番を1つだけ選ぶとしたら、私は次の順序を勧める。これは「最短で意思決定の価値を生む」順に並べてある。
第一に、まず eval の信号の通路を作る。全量の記録、負の信号、安定した識別子。この作業はスコアを一切出さないが、後述のすべての作業のデータ基盤になる。線上の問題箇所の特定という痛点も同時に解決するので、優先度は最上位である。
第二に、まず構造化出力と指示の遵守という、決定論的に採分できる2つの次元からやる。採分は関数呼び出し1回で、コストはきわめて低く、結果にノイズがない。改善効果は通常すぐ見える。このステップによって、チームは「モデルが業務で実際に使えるのか」という問いに対して、印象ではなく具体的で再現可能な数字を持てるようになる。
第三に、線上の失敗サンプルから2〜3百条のチューニング集合を作り、帰属ラベルで分類する。このステップから本当に意思決定の価値が生まれる。「一番多い失敗はパラメータ抽出と長い往復での記憶の消失だ」と言えるようになる。「なんとなく不安定だ」ではなくなる。
第四に、決定論的に採分できる judge をすべて撤去し、決定論的な検査に置き換える。採分のノイズをシステムからしぶしぶ取り除く。このステップにより、既存のスコアの信頼度が明確に跳躍する。
第五に、本気でリスクがある次元を1つ測る。長い文脈、multi-turn の一貫性、道具呼び出しのいずれかを選ぶ。摂動で断点を探し、それを1本のアーキテクチャ規則に翻訳する。このステップは通常、「eval の結果によってアーキテクチャを変えた」という最初の瞬間であり、eval という行為が組織の支持を得るための鍵にもなる。
第六に、confidence interval と分散の分解を導入する。これ以降のすべてのスコアは区間つきで報告され、単一の数字だけで意思決定しなくなる。このステップは認知コストが高いが極めて必要なので、チームが eval への信頼を確立した後に回すとよい。
第七に、決定論的に判定できる部分を verifiable task に変換し、3層の継続パイプラインを組む。このステップが規模化である。前の6手を不到位のままパイプラインを組むと、誰も信頼せず、誰も見ないゲートになるだけだ。
有几个容易走错的岔路
最後に、実務で繰り返し現れる誤ったパターンをいくつかまとめる。技術的な細部よりも、こちらの方が整套 eval を無力化しやすい。
eval を1回限りのレポートとして扱う。eval の価値は継続的な対照にあり、ある1回の結論にはない。1回だけ走らせたレポートは、数字がきれいでも使い終われば古くなる。
綜合スコアを1つ求める。綜合スコアは分布を隠し、対応する処置も存在しない。次元ごとの通過率の表は、単一の数字より常に有用である。
弱い judge で、大量に判定可能なサンプルを採分する。測定精度が体系全体のボトルネックになる。しかも気付きにくいボトルネックである。エラーを出さず、「どの変更が効いたかうまく説明がつかない」という無力感だけを生む。
パイプラインの問題をモデルのせいにする。検索の未召回、コンテキストの切り詰め、上流データの汚れ、これらはすべて「モデルの回答が間違っていた」として現れる。帰属のこの輪を飛ばすと、壊れていないものを調整し続けることになる。
テスト集合をチューニングに使ってしまう。テスト集合のスコアがプロンプトの反復を誘導し始めた時点から、それはチューニング集合になる。その後、「新しい変更が本当により良いのか」というすべての判断が意味を失う。
モデルにできない仕事を、引き続きモデルに任せる。eval はある次元についてモデルにできないと告げた。正しい対処は、決定論的な検査を足すこと、状態を外へ出すこと、モデルを変えることであり、プロンプトの要求をより細かく書き込んで奇跡を期待することではない。
能力境界の探査とは、本質的に、「モデルは強い」という曖昧な判断を、位置とノイズと対応する処置を持つ具体的な結論の集合へ変換することである。決定性は提供しない。どの eval にもそれはできない。ただし、不確かな議論を、議論のできる技術的判断へ変えることができる。それ自体がすでに大きな前進である。
この体系が本当に効いているかどう判断するか
この仕組みは、形だけ整って中身が空の形式主義になりやすい。レポートは時間どおりに出るし、スコアの曲線もきれいだが、しかし製品判断は1度も変わっていない。
最初のサインは、選定の議論の中身が変わるかどうかである。議論が「どのモデルのほうが強いか」にとどまるなら、eval は十分な情報を与えていない。議論が「制約数の次元では、どのモデルにルーティングするか」になるなら、すでに意思決定を支えている。これは最も直接的で、かつ最も誠実な検査方法である。先月のモデル選定会議を1つ随手につまみ、eval の結論を引用した人がいるかどうかを確かめるだけでいい。
2つ目のサインは、アーキテクチャ変更が断点にさかのぼれるかどうかである。良い体系なら、モデル能力のために下したアーキテクチャ上の妥協がすべて記録として残る。ある次元で、どんな条件のもとで駄目だったかを測ったので、どの補償手段を取ることにしたのか、という記録である。状態の外部化層や検査層を誰かが廃止したいと思ったとき、当時の eval のデータを取り出して、廃止するとどの水準に戻るかを説明できる必要がある。このようなトレーサビリティを持たない補償層は、たいていある「コード整理」の波で誤って削除される。
3つ目のサインは、線上の問題の再発率である。これが最も硬い指標になる。同種の失敗が繰り返し 발견されるなら、還流の仕組みが動いていない。問題は見つかったのに回帰サンプルに沈殿していないので、次のバージョンでまた起きる。この比率が低下し続けていれば、ループは回っている。逆に、新しい問題の発見だけが速くなり、古い問題が一つも解決していないなら、eval set が収束するのではなく膨張している可能性が高い。
4つ目のサインは、eval そのものの信号対雑音比である。健全な体系では、「この変更でスコアが上がった」という発言はしばしば検証可能であるべきで、しばしばノイズであるべきではない。チームが eval 結果の変化を無意識に無視し始めたなら、ノイズ水準が信号水準を上回った。このとき採点方式を改めるか、繰り返し回数を増やすべきである。
最後に、投入と産出についての現実的な判断を1つだけ。能力境界の探査の効果は最初の月には現れない。その値は長期間かけて蓄積された対照データから来る。モデルアップグレードのたびに、プロンプト変更のたびに、アーキテクチャ調整のたびに、同じ物差しの下に目盛りとして残る。物差しそのものはあまり価値がない。価値するのは目盛りの個数のほうである。これも、前に繰り返し強調した点である。1回限りの eval をしてはいけない理由である。1回限りの eval は目盛り 0 個しか残さない。0 個では方向を定義できない。