Digital Reactor
AIエージェント統計・確率

エージェント出力の品質を確率で保証する:Conformal Predictionで人手に回す割合を決める

エージェント出力の品質を確率で保証する:Conformal Predictionで人手に回す割合を決める

はじめに

エージェントの評価結果として「合格率92%」という数字が出てきたとき、そこから次に決められることはほとんどありません。残りの8%がどこにいるのか分からないので、出力を人の確認なしに通してよいかどうかは、結局その場の勘で決めることになります。運用で要るのは、目の前の1件を自動で通してよいかどうかの判断で、全体の平均点はそこに直接は使えません。

そこで、出力1件ごとに品質の下限を付け、下限が目標に届かないものだけ人に回す、という設計を考えます。品質の目標水準と、その目標を破ってよい割合を決めると、人手に回る割合が自動的に決まります。今回の合成データでは、自動で通した分の不良を10%まで許容するなら人手に戻るのは1割強で済みました。ここから許容を半分にすると人手はおよそ3倍になり、さらに半分にすると全体の7割近くが人手に戻ります。品質の要求水準を1段上げる交渉は、そのまま何人ぶんの工数を足すかの交渉になります。

対象読者:

  • LLMエージェントを業務に載せていて、どこまで自動で通すかを決めかねている方
  • 「合格率」以外の品質の言い方を探している、評価・品質保証の担当者
  • 人手レビューの人員をどこまで減らせるかを見積もりたい事業側の責任者

記事のポイント:

  • 出力ごとに品質の下限を付ける片側のConformal Predictionを実装し、被覆率(coverage)が名目どおり出ることを確かめます
  • 「自動で通した分の不良率をα以下にする」を直接に較正すると、同じ保証でも人手が3分の1で済むことを示します
  • 入力分布が変わると保証が崩れることを実験で示し、再較正とオンライン補正の使い分けを整理します

合格率という報告からは、何を自動で通すかが決まらない

合格率が扱いにくいのは、合否の線をどこに引いたかで数字が動いてしまうからです。必要項目が10個あるタスクで、9個抜ければ合格とするか10個そろって合格とするかで、合格率は数十ポイント変わります。しかもそれは全体の平均であって、いま処理している1件が合格する確率ではありません。合格率92%と報告されても、その1件を自動で通したときに何が起きるかは分かりません。

判断に使える形にするには、出力ごとの品質と、その品質の不確かさを分けて扱う必要があります。今回は、文書から必要項目を抜き出すタスクを想定します。品質スコア yy は、必要項目のうち正しく抜けた割合で、0から1の値を取ります。品質目標は τ=0.8\tau = 0.8 とし、y<τy < \tau の出力を不良と呼びます。合成データの設定では、全件を無条件で自動承認すると15.3%が不良でした。

品質スコアは、人が採点して初めて分かる量です。実行時に手元にあるのは手がかりだけなので、それを集めて品質を予測します。入力文書の長さ、必要項目数、検索でヒットした参照の数、ツール呼び出しの再試行回数、モデルの自己申告信頼度を特徴量にして、勾配ブースティングで品質スコアを予測する回帰モデル f(x)f(x) を作ります。学習6,000件で、テスト30,000件に対する平均絶対誤差は0.069、実品質との相関は0.72でした。自己申告信頼度だけを使った場合の相関は0.57なので、モデル自身の申告を鵜呑みにするより、周辺の観測量を足したほうが予測は当たります。

def generate(n, rng, shift=0.0):
    length = rng.gamma(3.0, 1.0, n)                      # 入力文書の長さ
    n_items = rng.integers(5, 21, n)                     # 必要項目数
    n_refs = np.minimum(rng.poisson(3.0, n), 8)          # 参照のヒット数
    retries = rng.poisson(0.4, n)                        # 再試行回数
    difficulty = (0.35 * (length - 3.0) + 0.06 * (n_items - 12)
                  - 0.18 * n_refs + 0.50 * retries + 0.54
                  + shift + rng.normal(0, 0.40, n))
    p = 1.0 / (1.0 + np.exp(-(2.9 - 1.2 * difficulty)))  # 1項目あたりの成功確率
    conf = np.clip(p + rng.normal(0, 0.12, n), 0.0, 1.0)  # 自己申告信頼度
    y = rng.binomial(n_items, p) / n_items               # 品質スコア
    return np.column_stack([length, n_items, n_refs, retries, conf]), y

出力ごとに品質の下限を付ける

f(x)f(x) は点予測なので、そのまま閾値と比べても外れたときの保証がありません。ここで欲しいのは上下の区間ではなく下限だけです。品質が予想より高いぶんには困らず、予想より低いことだけが問題になるからです。そこで片側のSplit Conformal Predictionを使います。

学習に使っていない較正データで、非適合スコア(nonconformity score)を過大予測の量 si=f(xi)yis_i = f(x_i) - y_i と定義します。その (1α)(1-\alpha) 分位点 qq を取り、品質下限を L(x)=f(x)qL(x) = f(x) - q とします。テスト点と較正点が交換可能(exchangeable)なら、有限標本で次が成り立ちます。

P(yn+1f(xn+1)q)1α,q=(n+1)(1α)/n 分位点P\bigl(y_{n+1} \ge f(x_{n+1}) - q\bigr) \ge 1 - \alpha, \qquad q = \left\lceil (n+1)(1-\alpha) \right\rceil / n \text{ 分位点}

分位点の取り方と保証の導出はConformal Predictionの入門記事で扱ったので、ここでは実装だけ載せます。

def conformal_slack(f_cal, y_cal, alpha):
    """過大予測 s = f - y の (1-alpha) 分位点。品質下限は L(x) = f(x) - q。"""
    s = f_cal - y_cal
    n = len(s)
    lvl = min(np.ceil((n + 1) * (1 - alpha)) / n, 1.0)
    return float(np.quantile(s, lvl, method="higher"))

較正8,000件で作った下限が、テスト30,000件でどれだけ当たるかを確かめます。

名目 1α1-\alpha実測被覆率すべり幅 qq
0.800.79980.062
0.900.90130.117
0.950.94780.165
0.990.98750.273

名目どおりの被覆率が出ています。予測器の性能や品質スコアの分布に何も仮定を置いていないので、この一致自体は設計どおりです。実務でこの表を作る意味は、保証が出たことの確認よりも、すべり幅 qq を見ることにあります。90%の下限を出すには予測値から0.117引く必要があり、95%なら0.165引きます。予測器の精度がそのまま、下限を引き下げる幅として目に見えます。

下限が手に入れば、選択的予測(selective prediction)の形に持ち込めます。L(x)τL(x) \ge \tau の出力だけ自動で承認し、届かないものは人に回します。α=0.10\alpha = 0.10 とすると閾値は f(x)0.917f(x) \ge 0.917 となり、人手に回るのは42.6%でした。通した分の実測不良率は3.1%です。目標の10%は満たしているものの、実際には3分の1以下まで下がっていて、そのぶん人手へ回しすぎています。

下振れするのは、qq が全件を混ぜた分位点だからです。難しい入力では過大予測が大きく、簡単な入力では小さいのに、すべての入力から同じ幅を引いています。自動承認されるのは簡単な入力ばかりなので、そこに難しい入力ぶんの余裕まで載る形になります。この保証はあくまで全体(marginal)に対するもので、承認した部分集合に対するものではありません。

通した分の不良率を直接に較正する

運用として約束したいのは「自動で通した出力のうち、品質目標を下回るものが α\alpha 以下」という条件つきの量です。それなら、その量を直接に較正できます。

閾値 tt の候補をグリッドに並べ、較正データのうち f(x)tf(x) \ge t となったものの不良率に、二項分布のClopper-Pearson上側信頼限界を取ります。その上限が α\alpha 以下になる閾値のうち、最も緩いものを選びます。候補をすべて検定するのでBonferroniで多重性を補正します。

def calibrate_accept_threshold(f_cal, y_cal, alpha, delta=0.05, n_grid=40):
    """受理分の不良率 P(y < TAU | 受理) <= alpha を確率 1-delta で満たす閾値。"""
    bad_cal = (y_cal < TAU)
    grid = np.quantile(f_cal, np.linspace(0.02, 0.98, n_grid))
    conf = 1.0 - delta / n_grid
    best = np.inf
    for t in grid:
        acc = f_cal >= t
        n_acc, k = int(acc.sum()), int(bad_cal[acc].sum())
        if cp_upper(k, n_acc, conf) <= alpha:   # 上側信頼限界
            best = min(best, float(t))
    return best

保証の型が変わっている点は押さえておきます。conformalの 1α1-\alpha は較正データを引き直すことも含めた平均としての確率で、こちらは「較正データの引きが悪くなければ、確率 1δ1-\delta で不良率が α\alpha 以下」という形です。運用の約束としては後者のほうが宣言しやすく、監査でも説明が通ります。代わりに、δ\delta ぶんだけ上限を厳しめに取ることになります。

α=0.10\alpha = 0.10 で較正すると閾値は f(x)0.805f(x) \ge 0.805 になり、人手に回るのは11.8%、通した分の実測不良率は8.5%でした。同じ約束を、下限で判定したときのおよそ3分の1の人手で果たせています。

予測した品質スコアと実際の品質スコアの散布図。conformalの品質下限が品質目標0.8を超える閾値は0.965、受理集合の不良率を直接較正した閾値は0.895で、後者のほうが大幅に多くの出力を自動承認できる。

保証を1段強めると、人手は何倍になるか

許容する不良率 α\alpha を動かして、人手に回る割合と実測の不良率を並べます。

左: 許容する不良率を下げるほど人手に回る割合が増え、1件ごとの下限で判定する方式は同じ許容水準でも常に人手が多い。右: どちらの方式も実測不良率は目標を下回るが、下限で判定する方式は大きく下回りすぎている。

どちらの方式も約束は守っています。違うのは目標をどれだけ下回るかで、下限で判定する方式は実測が目標の3分の1程度に落ち、その代償として人手が2倍から3倍に膨らみます。α=0.03\alpha = 0.03 まで下げると必要な閾値が1.0を超えるので、全件を人手に回すほかありません。予測値の最大が1.0である以上、f(x)q0.8f(x) - q \ge 0.8 を満たす入力が無くなるからです。宣言できる保証水準に天井があるので、この方式は運用の要求に追随しにくくなります。

直接較正のほうを、人手の工数に換算します。月2万件を処理し、人が1件確認するのに7分かかる想定です。

許容する不良率 α\alpha人手に回る割合実測不良率人時/月常勤換算
15%4.0%12.3%930.6人
10%11.8%8.5%2751.7人
5%31.4%4.3%7334.6人
3%54.2%2.3%1,2647.9人
2%68.4%1.6%1,59510.0人

15%から10%への引き締めは0.6人から1.7人で、まだ現場の裁量で吸収できます。10%から5%にすると4.6人になり、5%から2%ではさらに倍になって10人です。曲線が凸なので、品質目標を上げるほど1ポイントあたりの追加人員が増えていきます。

この表があると、品質の議論を人員の議論として進められます。「不良を5%以下に抑えたい」という要求に対して、「では常勤4.6人ぶんのレビュー体制が要ります」と返せます。逆に「レビューは2人までしか出せない」という制約からは、宣言できる保証はおよそ9%だと逆算できます。人が1件見る手間と、不良を1件通したときの損失を金額に直せるなら、どこで止めるかは期待損失を最小にする問題として解けます。

曲線の位置を決めているのは予測器 f(x)f(x) の識別力なので、レビュー人員を減らす手立ては人員交渉だけではありません。f(x)f(x) の当たりが良くなれば曲線全体が下に動きます。エージェント本体の改善と、品質を予測する側の改善は別の作業で、後者のほうが安く済むことがよくあります。

較正データの件数が、宣言できる保証の上限を決める

較正データを部分抽出して、件数を変えたときに何が宣言できるかを見ます。各セルは30回の抽出のうち閾値が見つかった割合と、そのときテストで人手に回った割合の平均です。

較正件数α\alpha=10%α\alpha=5%α\alpha=3%α\alpha=2%
500100% / 23.2%27% / 60.5%0% / 不可0% / 不可
1,000100% / 17.0%80% / 49.3%10% / 65.2%0% / 不可
2,000100% / 13.9%100% / 40.3%43% / 65.6%3% / 68.7%
4,000100% / 11.9%100% / 34.3%100% / 62.1%10% / 69.1%
8,000100% / 11.8%100% / 31.4%100% / 54.2%100% / 68.4%

500件では α=3%\alpha = 3\% を1度も宣言できませんでした。データが足りないと、不良率が本当に低い閾値を選んでも、上側信頼限界がそこまで下がらないためです。α=10%\alpha = 10\% なら500件でも宣言できますが、人手は23.2%で、8,000件のときの倍かかります。較正データが少ないぶんの不確かさを、余分な人手で埋めている形になります。

較正データは人が採点したラベルつきの出力なので、増やすには実際の工数がかかります。厳しい保証を出したいという要求が来たときに最初にぶつかるのは、たいていモデルの性能ではなくラベルの枚数です。何件あれば何が言えるかの見積もりは、評価データセットの件数を検出力から決める話と同じ計算に乗ります。

入力の顔ぶれが変わると保証は崩れる

ここまでの保証はすべて交換可能性(exchangeability)に乗っています。較正時の入力とこれから来る入力が同じ母集団から来ている、という仮定です。エージェントの運用では、この仮定は次のような場面で崩れます。新しい業種や新しい書式の文書が入ってくれば入力分布が変わりますし、基盤モデルを差し替えたりプロンプトを書き換えたりすれば f(x)f(x) の意味自体が変わります。ツールの仕様変更で参照ヒット数の分布が動くこともあります。

5週目から入力が段階的に難しくなる状況を作り、3つの運用を比べます。較正したきりで放置する場合、毎週500件にラベルを付けて直近4週分で閾値を引き直す場合、毎週300件だけ通した出力を監査して閾値を上下させる場合です。3つ目は適応的conformal(adaptive conformal inference)と同じ更新則を閾値に当てたもので、監査で測った不良率が目標を超えていれば閾値を上げ、下回っていれば下げます。

左: 5週目以降、放置すると受理分の不良率が10%の目標を超えて18%付近まで上がる。再較正とオンライン補正はいずれも目標付近に戻る。右: 目標を保った2つの方式では人手に回る割合が12%から40%前後まで増える。

放置した場合、7週目以降の不良率は平均16.8%で、目標の10%を大きく超えました。人手に回る割合は14.4%のまま動きません。保証は破れているのに、運用のダッシュボード上で見えるのは「人手に回る件数が少し増えた」程度で、品質が落ちたことは何も表示されません。ラベルを取り続けないかぎり、破れたこと自体が観測できません。

毎週の再較正では不良率は8.2%に収まりましたが、人手は41.2%まで上がりました。オンライン補正は不良率10.5%、人手30.7%です。再較正のほうが人手が多いのは、直近4週分の2,000件から毎回上側信頼限界を取り直すので、標本が少ないぶんの上乗せを毎回払うためです。オンライン補正は有限標本の保証を持たず、長期の平均を目標に寄せるだけなので、その上乗せが要りません。

どちらを選ぶかは、ラベルをどれだけ供給できるかで決まります。採点の体制があるなら再較正が確実で、監査しか回せないならオンライン補正で日々の追随をしつつ、四半期に一度は較正データを取り直して引き直す形になります。信用スコアリングの運用でも、モニタリング指標は毎月見るものの、スコアカードの再構築は年単位で回します。頻度の違う2つのループを併走させる考え方は、そのままここでも使えます。

この設計を持ち込む前に確かめること

まず確かめるのは、品質を1本の連続量として書けるかどうかです。必要項目のリストに対する充足率のように数えられる指標ならそのまま乗りますが、文章の自然さのような主観評価しかない場合は、採点基準を数えられる形に落とすところから始まります。合否の二値しか取れないなら、品質下限の話は消えて、通した分の不良率を直接較正する部分だけが残ります。それでも運用上は十分に使えます。

棄却された分の受け皿も先に決めておきます。人手に回る割合が11.8%から31.4%に増えるとき、レビュー担当が本当にその件数をさばけるのかは別の問題で、さばけなければ待ち行列が伸びて納期が崩れます。処理能力に上限があるなら、最適化の変数は α\alpha ではなく「1日に人が見られる件数のなかで、どの出力を優先して見るか」に変わります。

保証の意味も取り違えないようにします。ここで担保しているのは通した集団の不良率であって、1件ごとの安全ではありません。1件の誤りが取り返しのつかない結果を生む出力に対しては、割合の保証は答えになりません。そうした出力は α\alpha の設計から外し、全件確認か、そもそも自動化しない判断を別建てで置きます。

まとめ

エージェントの品質を合否の二値で語るのをやめると、品質目標と人員が同じ軸に乗ります。今回の設定では、通した出力の不良を10%まで許すなら人手は11.8%、5%なら31.4%、2%なら68.4%で、月2万件の処理なら常勤1.7人から10.0人までの幅になりました。この対応表があれば、「品質をもう一段上げたい」という要求に対して、必要な人員と、代わりに諦める自動化率を数字で返せます。

前提はいくつもあります。品質を採点したラベルが数千件は要りますし、入力分布が動けば保証は静かに崩れて、ラベルを取り続けないかぎり崩れたことにも気づけません。それでも、合格率92%という報告よりは、意思決定に使える形になっています。手元にエージェントの実行ログと採点済みの出力があるなら、まず品質スコアを予測する回帰を1本作り、この記事の閾値較正を通して、いまの運用が何%の不良率を宣言できる状態にあるかを出してみてください。その数字が、自動化をどこまで広げられるかの出発点になります。

関連記事

← 技術ブログ一覧へ