Claude Codeでループエンジニアリング応用編| AIの採点者、1人だと見逃す。3人に投票させたら「出荷事故」も「完璧主義の沼」も防げた話
前回のキモは「書く人(本線AI)と採点する人(検証AI)を分ける」だった。今日はその続き。採点する人を1人から3人に増やして、多数決で合否を決めてみたら、思ってた以上に面白い結果になったので書いておく。先に結論だけ言うと、採点者を増やすと"バグの見逃し"だけじゃなく"完璧主義で永遠に終わらない事故"まで防げた。両方効くとは思ってなかった。
前回を読んでなくても分かるように書くけど、気になったら先に読んでもらえると入りやすいです。
なぜ1人だと危ういか
前回のループはこうだった。本線が実装して、検証AIが1人で採点して、FAILなら直して、PASSまで回す。これで十分うまくいく。ただ、採点者が1人だと弱点がある。その1人がサボると、バグがそのまま通る。
人間のレビューでも同じだよね。忙しい先輩が「テスト通ってる?ならOK」で済ませて、後で本番が燃える。あれのAI版が普通に起きる。
じゃあ採点者を増やせばいいのでは、というのが今日の話。
今日のワナ:テストは緑なのにバグがある
お題は「配列の平均を出す `average()`」。よくあるやつ。
export function average(numbers) {
let sum = 0;
for (const n of numbers) sum += n;
return sum / numbers.length;
}一見よさそう。実際、用意したテストは全部グリーンになる。
tests 3 / pass 3 / fail 0でも、空配列を渡すと壊れる。
average([]) = NaN ← 0/0 になるポイントは、テストが空配列のケースを書いていないこと。だからテストは緑のまま。テストだけ見て「OK」と言う採点者は、このバグに気づけない。現場でありがちなやつを、わざと再現した。
3人の採点者に、性格を変えて一斉採点させる
同じ実装を、3人の検証AIに同時に見せた。ただ3人コピーしても意味が薄いので、わざと性格(観点)を変えた。
Aさん:スピード重視。テストを走らせて、緑なら基本OK。細かいことは追わない。
Bさん:丁寧。テストに無いエッジケースも自分で考える。
Cさん:意地悪。実装を壊しにいく。変な入力を自分で試す。
3人を並列で走らせた結果がこれ。
| 採点者 | 観点 | 判定 |
| A | スピード重視 | PASS |
| B | 丁寧 | FAIL |
| C | 意地悪 | FAIL |
| 多数決 | | FAIL(2:1) |
Aは案の定「テスト緑だからPASS」で見逃した。Bは「空配列でNaN、しかも配列以外を渡すとTypeErrorで落ちる」とコードを読んで見抜いた。Cは実際に `average([])` を実行して `NaN` を目視で確認してFAIL。
もしAさん1人に任せていたら、このバグはそのまま出荷されていた。BとCが救出してくれた。これが採点者を増やす一番わかりやすい効果。
直して、もう一回投票
指摘を全部反映した。空配列は0を返す、配列以外はエラーにする、を足しただけ。
export function average(numbers) {
if (!Array.isArray(numbers)) {
throw new TypeError("average() には配列を渡してください");
}
if (numbers.length === 0) return 0;
let sum = 0;
for (const n of numbers) sum += n;
return sum / numbers.length;
}ついでに、3人が見つけた穴をテストにも昇格させた(空配列→0、非配列→エラー)。テストは5件に増えて、全部グリーン。
で、同じ3人に再投票させた。ここで予想外のことが起きる。
| 採点者 | 1周目 | 2周目 |
| A(スピード) | PASS(見逃し) | PASS |
| B(丁寧) | FAIL | PASS |
| C(意地悪) | FAIL | FAIL |
| 多数決 | FAIL | PASS(2:1) |
AとBはPASS。ところがCがまだ粘った。「`average([1,'a',3])` もNaNになる」「`average([true, false])` は0.5を返す、真偽値が数値扱いされてる」と、もっと深い穴を自分で実行して見つけてきた。意地悪担当、仕事しすぎ。
でも多数決はPASS(2:1)。ここで実装は完成、ループ終了。
学びは2つあった
1つ目。多数決はバグの見逃しを防ぐ。 これは想定どおり。1周目でAが見逃したバグを、2人が捕まえた。
2つ目。多数決は"完璧主義で終われない事故"も防ぐ。 これは想定してなかった。2周目のCみたいに、厳しい採点者は入力の粒度を細かくすればいくらでもダメ出しできる。文字列混入、真偽値、BigInt、無限大…付き合ってたら永遠に終わらない。採点者が1人でそれがCだったら、ループは沼る。多数決なら、1人が粘っても他の2人がPASSなら前に進める。
つまり、サボり過ぎる採点者にも、厳しすぎる採点者にも、投票が効く。振り切れた1人を、残り2人が中和してくれる感じ。
で、「どこまで直せば合格」なのか
ここが今回いちばん大事だと思った。Cの指摘(要素に文字列が混ざったらNaN)は、間違ってはいない。実際にそうなる。でも今回のBは「要素の型チェックは今回のスコープ外」と自分で線を引いてPASSにした。この判断、正しいかどうかは場合による。社内ツールなら「配列に数値以外入れるお前が悪い」で十分。決済まわりならCの言うとおり全部弾くべき。
要するに、どこで合格にするかは、最後は人間が完了条件で決める。AIを何人並べても、ここだけは丸投げできない。採点者を増やすと精度は上がるけど、合格ラインそのものは人間が引く。前回「完了条件を機械が判定できる形にしろ」と書いたけど、今回はその一歩先。「完了条件をどこまで広げるか」も自分で決める話だった。
Claude Codeでの組み方
やってることは単純で、前回作った検証サブエージェント(`.claude/agents/verifier.md`)を、観点だけ変えて3人ぶん同時に呼ぶだけ。呼び出しは並列でいい。3人の判定を集めて、PASSが2つ以上なら合格、とするだけ。
コツは、3人に別々の性格を与えること。同じプロンプトを3回投げても、だいたい同じ答えが返るので投票の意味が薄い。「速い人」「丁寧な人」「意地悪な人」みたいにバラすと、拾える穴が増える。人間のレビューチームと同じで、全員同じタイプだと意味がない。
奇数にするのも地味に大事。2人だと1対1で決まらない。3人か5人。
つまずいたところ
| 失敗 | 原因 | 対策 |
| 投票がいつも全員一致で意味がない | 3人とも同じプロンプト | 性格・観点をわざとバラす |
| 意地悪担当が永遠にFAILを出す | 合格ラインが無限に広い | 完了条件で「どこまで見るか」を先に決める |
| 票が割れて決まらない | 採点者が偶数 | 3人か5人(奇数)にする |
| テスト緑なのにバグ | テストがエッジケース未カバー | 見つけた穴はテストに昇格させる |
最後のやつは今回もやった。3人が見つけた空配列と非配列のケースを、テストに足しておいた。次から同じバグは1周目で自動的に落ちる。採点で見つけた穴をテストに戻すと、レビューが1回ごとに賢くなる。これは地味だけど効く。
まとめ
採点する人を1人から3人にして、多数決にした。分かったのはこの2つ。
見逃しが減る。そして、1人の厳しすぎる採点者に足を引っ張られなくなる。振り切れた意見を、票数が中和してくれる。
ただし、どこで合格にするかは人間が決める。ここだけはAIに渡せない。採点者を増やすのは精度を上げる話で、合格ラインを引くのは別の仕事。
次の検証ネタのリクエストがあれば、ぜひコメントからリクエストしてください。
