欠陥をあらかじめ7件仕込んだ PR に、Claude Code の /code-review を low と high で3回ずつかけました。
low が拾ったのは4件、high は6件です。
件数だけ見れば high の勝ちに見えます。
ただ深刻度で並べ直すと、致命的・重大の欠陥は3回とも low が全部拾っていました。
high が上乗せしたのは、中程度が1件と軽微が1件だけです。
そして所要時間は、low が十数秒に対して high は数分でした。
件数だけでは分からなかったこと
前回、/code-review の effort を実測したときは「トークンと時間に余裕があるなら high」と書きました。
ただ、あのときは別の PR に各1回ずつ流して件数と時間を眺めただけです。
high の10件が low の1件より多いのは確かでも、増えた分が何なのかは見ていませんでした。
重い欠陥が増えているのか、細かい指摘が増えているだけなのか。
レビューは見つけたものより見逃したもののほうが痛いので、そこをはっきりさせたい。
そう思って、正解が分かっている PR を用意し、同じ条件で測り直しました。
検証の条件と題材の PR
題材は検証用サンドボックス cc-lab の PR #1 です。
タグ付け・ページネーション・タスク共有を追加する diff に、あらかじめ7件の欠陥を仕込んであります。
内訳は致命的1件・重大2件・中程度3件・軽微1件。
たとえば他人のタスクが読めてしまう認可チェック漏れ(IDOR)や、ページネーションの off-by-one といった、レビューで拾えてほしい種類のものです。
npm test は12件すべて green なので、テストを回しても落ちません。
この PR と7件の内訳は、敵対レビューの検出率を測ったときと同じものです。仕込んだ欠陥の一覧はそちらに載せています。
effort は /code-review low のようにコマンドの引数で渡すレベルで、軽いほうから low / medium / high / xhigh / max が用意されています。
今回回したのは low と high の2つです。
なお欠陥の深刻度も同じように低い・高いで表すため、そのまま書くと effort のレベルと紛らわしくなります。
そこでこの記事では、欠陥の深刻度を「致命的 / 重大 / 中程度 / 軽微」、effort のレベルを low / high と書き分けます。
実行条件は次のとおりです。
| 項目 | 値 |
|---|---|
| Claude Code | v2.1.223(モデルは Opus 5・auto mode on) |
| 題材 | cc-lab PR #1(7 files changed, +421/−4) |
| 回数 | low×3 / high×3 |
| 実行環境 | クリーンな clone 上で、各 run とも新規セッション |
3回ずつ回したのは、1回の結果がたまたまなのかを切り分けるためです。
また、前の run のコンテキストが次に効かないよう、1回ごとにセッションを立て直しています。
集計は全 run を終えてから、指摘をファイルと内容の要旨で正規化してまとめました。
同じ指摘かどうか迷ったものは、別物とみなす側に倒しています。
仕込んだ欠陥との突合も全 run 完了後で、レビュー中は正解リストを読ませていません。
なお v2.1.223 で /review は /code-review のエイリアスになりました。
以前は別物として使い分ける必要がありましたが、いまはどちらを打っても同じレビューが走ります。
この記事の数字は、この PR ひとつに対する low 3回・high 3回の実測です。
別の diff や別の規模で同じ比率になるかは確かめていません。
深刻度別に見た検出結果
仕込んだ7件を深刻度別に並べると、こうなりました。
| 欠陥の深刻度 | 仕込んだ数 | low で検出 | high で検出 |
|---|---|---|---|
| 致命的 | 1 | 1 | 1 |
| 重大 | 2 | 2 | 2 |
| 中程度 | 3 | 1 | 2 |
| 軽微 | 1 | 0 | 1 |
| 合計 | 7 | 4 | 6 |
急いで直すべき致命的・重大の3件は、low が3回とも全部拾っています。
effort を上げて増えたのは、中程度と軽微の1件ずつでした。
個別の内訳です。○が3回とも検出、×が一度も検出されなかったものを表します。
| 仕込んだ欠陥(深刻度) | low で検出 | high で検出 |
|---|---|---|
| 認可チェック漏れ・IDOR(致命的) | ○ | ○ |
| Authorization ヘッダのログ出力(重大) | ○ | ○ |
| off-by-one・ページネーション(重大) | ○ | ○ |
| 例外の握り潰し(中程度) | ○ | ○ |
| limit の未クランプ(中程度) | × | ○ |
| N+1 クエリ(軽微) | × | ○ |
| 競合状態・共有の重複挿入(中程度) | × | × |

1回目の low で返ってきた4件。3回とも同じ顔ぶれだった
仕込んだ7件に対する検出範囲は、effort ごとに固定されていました。
low の4件も high の6件も、3回のあいだで一度も入れ替わっていません。
揺れていたのはこの外側で、そちらは後半で扱います。
これは公式ドキュメントの説明とも合います。
At
lowandmedium, the review reports only the findings it's most confident in, so you see fewer false positives;highthroughmaxcast a wider net and may include findings the review is less sure about.
low と medium は確信度の高いものだけを返し、high から max は網を広げて確信の薄いものまで含める。
effort は精度のつまみではなく、どこまで拾いに行くかのつまみというわけです。
その分、拾ってきたものの性格も違いました。
low の4件はいずれも仕込んだ欠陥で、外れがありません。
一方 high の10件は、仕込んだ欠陥が6件、仕込んでいない実在のバグが1件(後述する外部キーの件)、残る3枠が回ごとに入れ替わる部分です。
この3枠が妥当な指摘だったかまでは突合していないので、そこは分かりません。
6回とも拾われなかった1件
表の最下段、競合状態だけはどちらも×でした。
共有の重複挿入を防ぐ処理を、既存レコードの有無を確認してから挿入する check-then-act で書いてあります。
確認と挿入のあいだに別のリクエストが割り込むと重複が通る形で、しかもトランザクションも UNIQUE 制約もありません。
これが low・high 合わせて6回のあいだ、一度も指摘されませんでした。
つまり、high なら網羅されるというわけではありません。
しかも今回試した範囲では、effort を上げても回数を重ねても出てきませんでした。
このクラスの問題は、UNIQUE 制約やトランザクションのようにコード側で構造的に塞いでおくほうが確実です。
low と high の所要時間
low と high では、かかる時間の桁が違いました。
| effort | 1回目 | 2回目 | 3回目 |
|---|---|---|---|
| low | 13秒 | 15秒 | 15秒 |
| high | 約6分 | 約8分 | 約9分 |
low は3回とも15秒以内で、high は分単位。
数十倍の開きです。
ただ、この差は待ち時間だけの問題ではありません。
low なら diff を見ながら気軽に回せますが、high は回すと決めてから席を立つ時間です。
3回ずつ回して見えた性質の違い
まず low は、3回とも完全に同じ4件でした。
変わったのは報告された行番号だけで、IDOR の指摘位置が :230 / :246 / :250 とずれました。
同じ問題を指しているのに、どの行を代表として出すかが揺れている格好です。
一方の high は毎回ぴったり10件でしたが、中身は違います。
3回で出た指摘をクラスタにまとめると全部で13種類あり、そのうち3回すべてに登場したのは7種類でした。
つまり残りの6種類が、回ごとに3枠を奪い合っています。
そもそも high は、候補を洗い出す finder と、それを反証しにかかる検証エージェントの2段構えで走ります。
完了報告にその内訳が出るのですが、候補の段階では数がそろっていません。
| run | 候補 | 検証エージェント |
|---|---|---|
| 1回目 | 42件 | 30体 |
| 2回目 | 37件 | 31体 |
| 3回目 | 36件 | 23体 |
入り口も検証の規模も違うのに、出口だけが毎回10件です。
ワークフローの自己説明には a ranked, capped findings report という表現がありました。
ここからは推測です。
候補数が動いても報告数が動かないなら、ランク上位10件で切られていると読むのが自然でしょう。
そう考えると、入れ替わる3枠はカットライン際の指摘が回ごとに順位を変えている、という説明がつきます。
公式ドキュメントでこの上限が明言されているのは確認できていないので、断定はしません。
ただ、もし本当に上限があるなら、high を回しても報告に載らないまま沈む指摘が残ります。
high を回せば網羅、とはならない理由がここにもあるわけです。
もうひとつ、説明そのものが食い違った例もありました。
仕込んだ7件の外側で high が挙げてきた指摘に、外部キーと DELETE の問題があります。
1回目と3回目は「外部キーが有効なので DELETE が 500 になる」と報告していて、実機で確認したところ、こちらが正しい。
ところが2回目だけは「PRAGMA foreign_keys が未設定なので外部キーは無効、孤児行が残る」という逆の機序を報告してきました。
指摘の位置はどちらも src/db.ts:54 で同じです。
場所が正しくても、添えられた説明まで正しいとは限りません。
low と high の使い分け
私の運用はこう変えました。
- 日常の diff は low。十数秒で終わるので、書いた流れでそのまま回して直す
- 大きな変更・アーキテクチャの設計が絡むところ・影響が長く残るところは high
- high の指摘は、場所は信じても説明までは鵜呑みにしない
なお effort を省略すると前回打ったレベルが再利用されるので、切り替えるときは /code-review high のように明示して打ちます。
低いレベルのまま大事な PR に入ってしまうのが、この仕様でいちばん起きやすい事故です。
今回回したのは low と high だけです。
medium は日常の落としどころとして手ごろだろうと思っていますし、high の上には xhigh と max もありますが、どちらも実測していないので見込みの話になります。
前記事では「余裕があるなら high」と書きましたが、そこから一段変えたことになります。
重い欠陥が low で取れているなら、日常の diff に毎回数分を払う理由は薄いと考えました。
判断材料をもうひとつ挙げるなら、そのコードをどれだけ設計してから書いたかです。
これは検証から出た数字ではなく運用上の実感ですが、事前に設計を詰めてあるほど low で足りますし、考えずに書き散らしたコードほど high に時間をかける価値があります。
ただし、これを「low だけでいい」と読まないでください。
今回 low が致命的・重大を取りこぼさなかったのは、7件を仕込んだこの PR での話です。
落とすと痛い変更を前にして数分をケチる理由はないので、そこは素直に effort を上げたほうがいい。
そして忘れたくないのが、競合状態の1件です。
low でも high でも、6回とも出てきませんでした。
どちらを選ぶかという話の手前に、レビューに任せきりにできない領域があります。
Xをフォローいただけると嬉しいです!
AI駆動開発(特に Claude Code)のノウハウや Tips をよく発信しています!

