0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AIが書いたコードを毎回"目視"レビューするのは、もう限界。評価(Eval)を仕組みにして"任せて確かめる"開発へ

0
Posted at

はじめに:AIの出力、"合ってる"ってどうやって判断してます?

最近、コードのかなりの部分をAIに書いてもらってる人、増えてきましたよね。

Claude CodeにしてもCursorにしても、関数1個どころか、機能まるごと「いい感じに書いといて」で返ってくる時代です。生産量はびっくりするくらい増えました。

でも、ここでちょっと正直に考えてみたいんです。

そのAIの出力、"合ってる"ってどうやって判断してます?

たぶん多くの現場の答えは、こうじゃないでしょうか。「目で見て、なんとなく大丈夫そうだったらマージ」。

これ、責めてるわけじゃないんです。僕も最初はそうでした。むしろ、AIが書いた量が少ないうちは、目視で十分まわります。

ただ、AIの生産量が増えていくと、ある日ふと気づくんですよね。自分が、AIの出力をひたすら採点し続けるだけの人になってる、という事実に。

実装はAIがやってくれるようになったのに、レビューだけは全部こっちに残ってる。しかも量は増える一方。これって、めっちゃもったいなくないですか。

今日はこの「毎回ぜんぶ目視レビュー問題」を、評価(Eval)を仕組みにするという考え方で抜ける話をします。抽象論で終わらせず、コピペで動く最小ハーネスとプロンプト例まで持って帰れるように書きました。


なぜ「毎回ぜんぶ目視レビュー」は破綻するのか

まず、なぜ目視レビューが限界を迎えるのか。ここを腹落ちさせないと、仕組みの話に進めません。理由は大きく4つあります。

1. スケールしない。
AIの出力は、人間の数十倍の速さで増えます。でも人間の目は1日8時間しかありません。出力が10倍になっても、目は10倍速くなりません。ここで需要と供給が壊れる。

2. 属人化する。
「あの人がレビューすれば大丈夫」は、その人がボトルネックになるということでもあります。レビュー基準がその人の頭の中にしかないと、チームに広がりません。

3. 静かな回帰(リグレッション)に気づけない。
ここが一番こわいところです。AIの出力品質は、自分のコードを変えてなくても勝手に変わることがあります。モデルがアップデートされた、プロンプトをちょっといじった、RAGに入れる文書を差し替えた。こういう変化で、昨日まで通っていたケースが今日は崩れる。目視だと、これに気づくのは「本番でバグが出てから」になりがちです。

4. 再現できない。
「前にこういう壊れ方したよね」を、人間の記憶だけで管理するのは無理があります。同じ失敗を、何度でも踏み直すことになる。

ちょっと具体的な事故を想像してみましょう。

あるチームが、AIに「ユーザー入力をJSONに整形する」処理を任せていた。半年間、目視レビューで問題なく回っていた。ある日、使っているモデルが新バージョンに切り替わった。すると、ごくまれに null を文字列の "null" として返すようになった。誰もコードを変えていない。レビューもすり抜けた。気づいたのは、3週間後、決済データの集計がズレた本番障害として、だった。

これ、AIが悪いわけじゃないんです。「出力が合っているかを、毎回・自動で・同じ基準で確かめる仕組み」が無かっただけ。情報を生成することと、それが正しいと保証することは、全然別の仕事なんですよね。

そして大事なのはここです。AI時代にエンジニアの仕事がなくなるんじゃなくて、重心が「実装する人」から「評価を設計する人」へ移る。電卓ができても数学者の仕事は消えなかった。計算じゃなく「何を計算すべきか」が仕事になっただけ。それと同じ流れだと思うんです。


結論:レビューを「評価(Eval)」という仕組みに変える

先に結論を置きます。

やることは、毎回の目視レビューを、一度作れば自動で回り続ける"評価(Eval)システム"に置き換えることです。

ここで言う評価(Eval)とは、おおげさなものじゃありません。ざっくり言うと、こういう部品の組み合わせです。

  • ゴールデンセット … 「この入力には、こう答えてほしい(あるいは、こういう基準を満たしてほしい)」を集めた、お手本の問題集。回帰テストの土台。
  • 決定論チェック … 形式・スキーマ・lint・既存テストなど、プログラムで白黒つけられる速くて安い検査。
  • LLM-as-judge … 「読みやすさ」「意図に合ってるか」みたいに、機械的には測りにくい品質を、別のAIに採点させる仕組み。
  • CIゲート … 評価スコアが基準を下回ったら、マージやデプロイを止める関所。
  • 人間サンプリング監査 … 全部は見ない。5〜10%だけ人間が見て、採点の精度そのものを保つ。

ポイントは役割分担です。人間が設計するのは「合否の基準(ルーブリック)」。採点という単純作業をスケールさせるのはAI。そして、その採点が信用できるかを一部だけ人間が監査する。

「全部を目で見る」から「基準を設計して、採点を任せて、一部だけ確かめる」へ。これが今日の主題です。


評価ピラミッド4階層 — 実務の地図

いきなりコードに行く前に、全体の地図を見せます。評価は4階層のピラミッドで考えると整理しやすいです。下にいくほど「速くて安い」、上にいくほど「賢いけど高い」。

            ┌───────────────────────────┐
   高い・遅い │ L3: 人間サンプリング監査     │  ← 5〜10%だけ。判定の精度を守る
            ├───────────────────────────┤
            │ L2: LLM-as-judge (意味)     │  ← 読みやすさ・意図適合などを採点
            ├───────────────────────────┤
            │ L1: ゴールデンセット回帰     │  ← 過去の成功/失敗を固定して再発防止
            ├───────────────────────────┤
   安い・速い │ L0: 決定論チェック          │  ← format/schema/lint/test。まずここ
            └───────────────────────────┘

順番がめちゃくちゃ大事です。いきなりLLM-as-judgeから作らないこと

まずL0の決定論チェックで、機械的に弾けるものは全部弾く。「JSONとして壊れてる」「必須フィールドが無い」「lintが落ちる」みたいなのは、AIに採点させるまでもありません。速いし、安いし、ブレません。ここで9割の事故は止まります。

その上で、L0では測れない「意味の質」だけをL2のjudgeに回す。そしてjudge自体が信用できるかを、L3の人間監査で定期的に確かめる。

この順番を守るだけで、評価のコストは一気に下がります。怠惰なエンジニアほど、まず一番安い層を厚くするんです。


手を動かす:最小Evalハーネスを作る

ここからは実際に動くものを作ります。題材は「自然文の住所を、決まったJSON構造に整形するAI処理」とします。汎用的なダミー題材です。

Step 1. ゴールデンセットを持つ

まず、お手本の問題集をファイルで持ちます。最初は25〜50件で十分。ハッピーケースだけじゃなく、過去にやらかしたケースと、意地悪なエッジケースを必ず入れるのがコツです。

[
  {
    "id": "addr-001",
    "input": "東京都千代田区1-2-3 サンプルビル4F",
    "must_include": ["千代田区"],
    "expect_schema": ["prefecture", "city", "rest"],
    "note": "標準ケース"
  },
  {
    "id": "addr-002",
    "input": "住所不明",
    "must_include": [],
    "expect_schema": ["prefecture", "city", "rest"],
    "note": "回帰: nullを文字列nullで返した事故の再発防止"
  },
  {
    "id": "addr-003",
    "input": "Tokyo, Chiyoda 1-2-3",
    "must_include": [],
    "expect_schema": ["prefecture", "city", "rest"],
    "note": "エッジ: 英語表記でも壊れないか"
  }
]

ここで大事なのは、addr-002 のような前に壊れたケースを、必ず1件ずつ資産として残していくこと。これが回帰テストの本体です。本番でヒヤッとするたびに1行足す。これだけで、明日の自分が同じ事故を踏まなくて済みます。

Step 2. 決定論チェック(L0)を書く

次に、機械で白黒つく検査。AIを呼ぶ前に、まずこれで弾きます。

import json

def deterministic_checks(raw_output: str, case: dict) -> list[str]:
    """機械的に判定できる失敗を返す。空リストなら合格。"""
    failures = []

    # 1) そもそもJSONとして妥当か
    try:
        data = json.loads(raw_output)
    except json.JSONDecodeError:
        return ["JSONとしてパースできない"]

    # 2) 必須キーが揃っているか
    for key in case["expect_schema"]:
        if key not in data:
            failures.append(f"必須キー欠落: {key}")

    # 3) 含まれているべき文字列があるか
    for needle in case["must_include"]:
        if needle not in raw_output:
            failures.append(f"必須語が含まれない: {needle}")

    # 4) "null"という文字列で誤魔化していないか(過去事故の番人)
    if data.get("prefecture") == "null":
        failures.append('prefectureが文字列"null"になっている')

    return failures

地味ですよね。でもこの地味な関数が、L2のjudgeを呼ぶコストの大半を節約してくれます。判定が速くてブレない検査を、できるだけ下の層に積む。これが評価設計の基本姿勢です。

Step 3. LLM-as-judge(L2)を呼ぶ

L0を通った出力だけ、意味の質をAIに採点させます。judgeを呼ぶコードは、驚くほど薄くて大丈夫です。

def judge(input_text: str, model_output: str, llm) -> dict:
    """別のLLMに採点させ、JSONの判定を受け取る。llmは任意のクライアント。"""
    prompt = JUDGE_PROMPT.format(
        input_text=input_text,
        model_output=model_output,
    )
    raw = llm.complete(prompt, temperature=0)  # 採点はブレを抑えるため温度0
    verdict = json.loads(raw)
    return verdict  # {"pass": bool, "score": int, "reason": str}

判定の温度は0に寄せます。採点者がコロコロ気分で点を変えたら、回帰テストになりませんからね。

肝心なのは、薄いコードの中身じゃなくて、採点基準(ルーブリック)を書いたプロンプトです。ここが今回の山場なので、章を分けます。

Step 4. CIゲートで関所を作る

最後に、これをCIに組み込みます。スコアが基準を割ったら、マージを止める。pytestでもいいし、GitHub Actionsならこんな形です。

name: ai-eval-gate
on: [pull_request]

jobs:
  eval:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run eval suite
        run: python run_eval.py --golden golden.json --min-pass-rate 0.95
      # run_eval.py 内で合格率が 0.95 を下回ったら exit 1 にしておく
      # → CIが赤くなり、マージがブロックされる

ここまで来ると、「AIの出力が静かに劣化したら、人間が気づく前にCIが赤くなる」という状態になります。本番障害として顕在化する前に、関所で止まる。3週間後に決済データのズレで気づく、みたいな未来が消えるわけです。


LLM-as-judgeを"信頼できる採点者"にする

さて、ここが心配性な人ほど気にしてほしいところです。

LLM-as-judgeは便利ですが、judge自身も間違えます。しかも、わりとクセのある間違え方をする。だから「AIに採点させてるから安心」と丸投げするのは危険です。信頼できる採点者に育てる手順がいります。

まず、ちゃんとしたルーブリックを書く

採点プロンプトは、曖昧な5段階評価じゃなく、観点ごとに白黒つく形で書きます。出力はJSON固定にして、機械で集計できるようにします。

あなたはコード出力の厳格なレビュアーです。
以下の「入力」と「モデル出力」を、次の3観点で評価してください。
甘い点はつけないこと。少しでも疑わしいものは fail にすること。

観点:
1. 正確性: 入力の情報を取りこぼさず、捏造もしていないか
2. スキーマ適合: 指定キー(prefecture, city, rest)に正しく割り当てているか
3. 安全性: 不明な値を、それと分かる形(null)で表現できているか

入力:
{input_text}

モデル出力:
{model_output}

次のJSONだけを返すこと。前置きや説明は禁止:
{"pass": true/false, "score": 0-10, "reason": "30字以内で根拠"}

「甘い点はつけるな」「疑わしきは fail」と明示するのがポイントです。judgeは放っておくと優しすぎる先生になりがちなので、最初から厳しめに躾けます。

バイアスを知っておく

judgeには、知られたクセがあります。代表的なのはこの3つです。

  • 位置バイアス … 2つの出力を比べさせると、先に出したほうを高く評価しやすい。
  • 冗長性バイアス … 長くて説明が多い出力を、中身に関係なく高く評価しやすい。
  • 自己優遇バイアス … 自分(同じモデル)が書いた文章を、好意的に採点しやすい。

対策はシンプルで、判定の根拠(reason)を必ず書かせること。そして比較させるときは順番を入れ替えて2回聞く。これだけでだいぶマシになります。

jury(多数決)で安定させる

一番効くのが、1人の採点者に頼らず、複数のjudgeに採点させて多数決を取るやり方です。jury-of-judges と呼ばれます。

このタスクを、3人の独立したレビュアーとして評価してください。
それぞれ別々に pass/fail を判定し、最後に多数決の結論を出してください。

- レビュアーA(正確性重視): {観点1だけを厳しく見る}
- レビュアーB(安全性重視): {観点3だけを厳しく見る}
- レビュアーC(全体最適): {3観点をバランスで見る}

出力:
{"verdicts": ["pass"/"fail", ...], "final": "pass"/"fail", "reason": "..."}

観点をわざと分けるのがミソです。同じ視点の3人より、違う視点の3人のほうが、見落としを潰せます。1人なら見逃した安全性の穴を、安全性担当のBが拾う、みたいに。

最後に、人間がキャリブレーションする

そして、これを忘れちゃいけません。judgeが正しく採点できているかを、人間が確かめる工程です。

やり方は地味です。人間が30〜50件、自分の手で pass/fail のラベルを付ける。さらに「なぜそう判定したか」を一言メモする。そのうえで、judgeの判定とどれだけ一致するかを見ます。

# キャリブレーション用の聞き方(人間ラベルと突き合わせる)
次の出力を pass/fail で判定し、判断理由を1文で書いてください。
そのあと、人間のラベル「{human_label}」と、あなたの判定が
一致したか不一致かを答えてください。
不一致だった場合、ルーブリックのどの表現が曖昧だったかを指摘してください。

ここで不一致が多ければ、judgeが悪いんじゃなくて、たいていルーブリック(基準)の書き方が曖昧なんです。人間の判断基準を、もう一段だけ言語化してプロンプトに足す。これを何回か回すと、judgeの判定が人間の感覚にそろってきます。

この「人間が基準を磨き、AIが採点を回す」往復こそが、評価を仕組みにするってことの中身です。


人間が設計し、AIが採点し、人間が監査する

ここまでの役割分担を、1枚に整理しておきます。AI時代の開発って、結局この線引きをどこに引くかの勝負だと思うんですよね。

工程 誰がやる 中身
合否基準(ルーブリック)の設計 人間 何を正解とするか。ここが一番の知的作業
ゴールデンセットの選定 人間 どのケースを資産として残すか。事故を1件ずつ追加
決定論チェックの実装 人間→以後自動 一度書けば、あとは機械が回す
大量の出力の採点 AI スケールする単純作業はAIに渡す
採点の監査(5〜10%) 人間 judgeが暴走してないかを定期チェック
基準の更新 人間 不一致やすり抜けを見て、基準を磨き直す

見てのとおり、人間の仕事は消えてません。むしろ難しくて面白いところ(基準の設計と監査)に集中できるようになる。採点という消耗戦から解放されて、設計に頭を使えるようになるんです。

これが、僕がよく言う「実装者から、設計・評価・文脈設計者へ」の正体です。役割が奪われるんじゃなくて、一段上に引き上げられる感覚に近いかなと。


明日から始める3ステップ

「全部やるのは大変そう」と思った人へ。一気にやらなくて大丈夫です。小さく始めるのが正解です。

  1. ゴールデンセットを1ファイル、5件だけ作る。
    いま一番AIに任せてる処理について、「これは絶対こう返ってほしい」を5件、JSONで書く。次にヒヤッとしたら1件足す。これだけで回帰テストの種ができます。

  2. 決定論チェックを1個だけ書く。
    JSONが壊れてないか、必須キーがあるか。一番安い検査を1個、CIに入れる。judgeはまだいりません。これだけで事故の体感は減ります。

  3. judgeのルーブリックを1枚書いて、10件だけ人間と突き合わせる。
    厳しめのルーブリックを書いて、人間の判定と一致するか10件で確かめる。ズレたら基準の言葉を直す。judgeを"育てる"感覚をつかめば、あとは広げるだけです。

3つとも、たぶん1〜2時間で着手できます。完璧じゃなくていい。65点でいい。今日5件のゴールデンセットを書いておけば、明日の自分が回帰で救われます。


おわりに:採点を毎回手でやるのは、もう卒業

AIにコードを書かせる時代、本当のボトルネックは「生成」じゃなくて「それが合ってるとどう保証するか」に移りました。

そして、その保証を毎回・人間が・目視でやるのは、もう量的に無理がある。これは気合いの問題じゃなくて、設計の問題なんですよね。

だから、レビューを「仕組み」に変える。
基準は人間が設計し、採点はAIに任せ、その採点を人間が一部だけ監査する。一度組めば、AIの出力が静かに劣化しても、本番に出る前に関所が止めてくれます。

これって、めちゃくちゃ怠惰な発想なんです。でも、いい怠惰です。汗をかくんじゃなくて、冷や汗をかく前に仕組みで止める。手を動かし続けるんじゃなくて、頭とAIで一回設計しておく。

毎回の採点に消耗してる今日の自分から、ちょっとだけ卒業して、評価を設計する側にまわってみる。

今日ゴールデンセットを5件書いた自分に、明日の自分はたぶん「あざっす」って言ってくれます。そのくらい軽い一歩から、始めてみませんか。

最後まで読んでくれて、ありがとうございました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?