前回の記事では、コミットのレビューをOpusからSonnetに切り替えて、費用を62%減らしました。ただ、その代わりにバグをどれだけ取りこぼすのかは未確認のままでした。正解データがなければ答えは出ません。そこで、自分のリポジトリの履歴から、AIコードレビュー用の正解データを作りました。18個のコミットに30件のバグが入っていて、どれも後から手作業で直したものです。同じレビュースクリプトを2つのモードで走らせました。
Sonnetは30件中18件を$11.68で見つけました。Opusは17件を$26.03で見つけました。
要点:
- このデータセットでは、Opusのほうが多く見つけたわけではありません。30件で1件の差に意味はありませんが、2倍払う根拠もまだありません。
- 2つのモデルが見落とした箇所は違いました。合わせると21件、単独では17〜18件です。
- 両方が見落とした9件のうち2件は、
tscで検出できます。無料で、数秒です。 - 残りの見落としには共通点があります。バグが見えるのは、差分にないファイルと合わせて読んだときか、ライブラリの実行時の挙動を知っているときだけです。
- 自動検索で見つかった候補の半分は、バグではありませんでした。手作業のラベル付けがないと、正解データは信用できません。
答えが分かっているバグの集め方#
AIレビューのベンダーは、自社で性能を測っています。DeepSourceはこれをはっきり書いています。各社が自分のデータセットでベンチマークを走らせ、自分で勝っているのです。私が知りたかったのは、自分のスクリプトと自分のコードでの結果でした。
材料はgitにあります。メッセージにfixを含むコミットは、以前に誰かが書いた行を直しています。修正前の状態でその行にgit blameをかけると、バグが入ったコミットが分かります。この手法はSZZと呼ばれ、2005年の論文の著者Śliwerski、Zimmermann、Zellerの頭文字です。
git show -U0 --format= "$FIX" -- "$FILE" | grep '^@@'
git blame --porcelain -L 41,41 "$FIX^" -- "$FILE" | head -1Pythonのスクリプトを7つのリポジトリに走らせました。Next.jsのサイトが2つ、Prismaのデータベース付きカタログ、Webの電卓、TypeScript製のオープンソースwiki、Swift製のiOSアプリとApple Watchアプリです。「バグを含むコミット→修正」のペアが125組見つかりました。
ここからは手作業です。データやテキストの修正を外し、コードの修正だけの37件を候補に残しました。候補ごとにSonnetのエージェントを1つ立て、修正と元のコミットを読ませて、3つの問いに答えさせました。新しい要件ではなく、本物の欠陥か。そのコミットに入っているか。実機で動かさなくても、注意深いレビュアーなら気づけたか。バグが複数あるケースは自分で再確認しました。
ラベル付けした37件のうち、残ったのは18件です。この絞り込みは、思った以上に役に立ちました。
- 「修正」の一部は新しい要件の実装でした。たとえば、デバイス間のテーマ同期です。
- 一部は、バグが別のコミットで入っていました。SZZが指したのは、同じ行をたまたま触っただけの隣のコミットでした。
- 一部は、コミット時点にはなかったデータで、後から欠陥が表に出たものでした。
もう1点、これを外すと数字に意味がなくなります。レビュアーに全履歴付きの作業コピーを渡すと、git log --allで将来の修正がバグの説明つきで見えてしまいます。そこで各ケースを、2コミットだけの独立したリポジトリにしました。バグが入る前の状態と、バグを含むコミットそのものです。未来の情報は入っていません。
検証方法#
レビューには、毎日使っているchange-reviewをそのまま使いました。引数を3つ足しています。リポジトリの場所、レビュー対象のファイル、状態を書き込まないこと、です。モードは2つあります。
| モード | 下調べ | 指摘の探索と検証 |
|---|---|---|
| 通常 | Haiku | Sonnet |
thorough | Haiku | Opus |
レビューの後は、別の審査役エージェントが、指摘と正解データを突き合わせました。原因と症状が同じなら一致とし、行番号のずれは許容しています。正解データと一致しなかった指摘は、審査役がコードで確かめて、本物の欠陥かどうかを判定しました。
AIコードレビューの結果#
| Sonnet | Opus | |
|---|---|---|
| 30件中の検出数 | 18 | 17 |
| high(14件中) | 9 | 9 |
| medium(10件中) | 7 | 6 |
| low(6件中) | 2 | 2 |
| 正解の指摘が1件以上あったケース | 18件中10 | 18件中11 |
| 正解外の指摘数と、審査役が本物と認めた数 | 77 / 57 | 75 / 57 |
| APIの料金表によるレビュー費用 | $11.68 | $26.03 |
| 1ケースあたりの費用 | $0.65 | $1.45 |
費用は前回の記事と同じく、エージェントのログにあるmessage.usageから計算しました。審査役の費用は$1.63と$1.70、正解データのラベル付けは$3.94です。実験全体で約$45かかりました。
重大なバグ14件のうち、両モデルとも9件を見つけましたが、同じ9件ではありません。Opusが見つけたのは、無料トライアルを使用済みの購読者にもトライアルをうたうペイウォールです。これには、StoreKitでは導入オファーの利用資格がオファー本体とは別に判定される、という知識が要ります。Sonnetが見つけたのは、wikiの初期設定です。新しく立てたサーバーに最初に来た訪問者なら誰でも、設定を終えて管理者になれる状態でした。Sonnetだけが見つけたバグは4件、Opusだけが見つけたバグは3件です。
異なるモデルで2回走らせると、合わせて21件が見つかり、どちらか単独より多くなります。費用は$37.71で、Opus単独より高くつきます。Sonnetを2回走らせるほうが安く、およそ$23のはずですが、この実行はしていません。注意点もあります。各モードは1回しか走らせていません。差の一部は、同じモデルの実行ごとのばらつきかもしれません。これはまだ測っていません。
誰も見つけられなかったバグ#
両モデルが見落としたバグは9件です。3つのグループに分かれます。
型エラー。 電卓では、必須の引数weightなしで関数を呼んでいます。別のファイルでは、どこにも宣言されていないIntlMessages型を使っています。どちらのコミットでもビルドが落ちていました。ところがレビュアーは、同じファイルの別の問題を6〜10件ずつ見つけながら、この2件だけは見落としました。tsc --noEmitなら、トークンを1つも使わずに見つかります。順序はこうです。先にコンパイラ、テスト、リンター。モデルはその後です。
別のファイルとの接点にあるバグ。 3件ありました。
- CSPから
'unsafe-eval'を外したところ、クライアントではnew Function()でMDXを実行していたため、本番で記事が表示されなくなった。 serialize()がdevモードで、後で描画に使うJSXランタイムとは別のランタイム向けにMDXをコンパイルしていた。- サイトマップがDockerイメージのビルド中にプリレンダリングされ、データベースがないために、空のリストを6時間返し続けた。
どの場合も、差分には1つのファイルしかなく、そのファイル単体では問題なさそうに見えます。バグに気づくには、その設定を使うコンポーネントを開くか、コードがどこで実行されるかを知っている必要があります。レビュアーは周辺のコードを読んでかまいませんが、差分を絞られた状態では読みません。前回書いたのと同じ死角です。ファイルを1つずつ順に見ていく方式では、ファイル間のつながりは見えません。
細かいもの。 lowレベルで議論の余地があるものが3件。もう1件はslug === "test"の例外で、意味が説明されているのは同じコミットのメッセージだけです。
1回の評価にかかる費用#
2回の実行を合わせて、約6,400万トークンを消費しました。その82〜85%がキャッシュの読み取りで、通常の入力より20分の1の単価です。仕組みは前回の記事と同じです。各エージェントは約4.9万トークンのコンテキストで起動し、リクエストのたびにそれを読み直します。18ケースで、1モードあたり約130エージェントでした。
この4.9万トークンのかなりの部分は、タスクと関係がありません。評価を起動したプロジェクトのCLAUDE.mdにある共通ルールと、プロジェクトのメモリです。エージェントは他人のリポジトリをレビューしているのに、私のサイトのルールを持ち歩いていました。そのせいで、あるレビューは古いコミットから、私自身のルールが禁じているアシスタントの署名を見つけてきました。
今後変えること#
- 通常モードはSonnetのままにします。 レビューでOpusを選ぶ根拠になるデータはありません。
thoroughは、リリース前に通常モードの後で走らせる、2回目の独立した実行として残します。 - レビューの前に
tscとビルドを走らせます。 見落とし9件のうち2件は、無料で防げます。 - 変更されたコードを使っているファイルをレビュアーに渡します。 いまは差分しか渡していません。呼び出し元の一覧は、モデルを起動する前にgrepで取れます。
- 正解データをスクリプトの隣に置きます。 レビューのプロンプトを直したら、同じ18ケースで走らせて、良くなったか悪くなったかを確かめられます。
検証できていないこと#
- 実行ごとのばらつき。 各モードは1回しか走らせていません。「実行1回の見落としが違った」ではなく「モデルの見落としが違う」と言うには、モードごとに少なくとも3回の実行が要ります。
- 正解データのラベル付けはSonnetです。 バグが複数あるケースは確認しましたが、全部ではありません。ラベル付け役がコミット内のバグを見落とせば、それは正解データに入らず、レビュアーの指摘は「正解外」に回ります。
- 審査役もSonnetで、努力レベルは低めです。 正解外の指摘に対する評価(77件中57件が本物)は、手作業では確かめていません。
- 30件は少なすぎます。 このデータセットでは、モード間で3〜4件に満たない差はノイズです。
自分のレビュースクリプトがあるなら、まず一晩で試してみてください。履歴へのSZZ、手作業のラベル付け、独立したケース。これがなければ、「新しいモデルのほうがレビューが良い」は意見であって、測定ではありません。workflowスクリプト自体の仕組みは、Claude Code workflowsの記事で解説しました。トークンの消費がどこから来るかは、Claude Codeのトークン消費の記事で書いています。



