結論
AIにコードレビューをやらせてみたところ、本物の問題はちゃんと見つけてくれました。
ただ、その指摘をそのまま鵜呑みにするのは、まだ危ないな、というのが正直な感想です。
今回は、さくらのAI EngineでGit差分をレビューする小さなCLIを作り、
正解が分かっている差分をいくつか用意して試してみました。
ついでに、モデルを4つ変えて同じ検証もやってみています。
結果だけ先に言うと、今回の構成では gpt-oss-120b が一番安定していました。
コード向けをうたっているモデルでも見逃しは普通に起きますし、
日本語モデルはコンテキスト長の上限に引っかかって途中で止まってしまいました。
一番効いたのは、指摘の根拠になる変更行を必ず引用させることと、
その結果をもう一段別のAIに検証させることです。
今回程度の小さな検証では、これだけで誤検知がかなり減りました。
実装と検証用の差分は GitHub に置いています。
作ったもの
作ったのは、Gitの差分をAIにレビューしてもらうだけの小さなCLIです。
名前は AI Code Review Arena にしました。
やっていることはシンプルで、1つの差分を4つの役割のAIに別々に見てもらいます。
- バグを探す
- セキュリティを見る
- 必要なテストを考える
- コードの読みやすさを見る
この4つの役割は、rolesの定義としてコードにベタ書きで固定しています。
const roles = [
['バグ検出', '例外、条件漏れ、境界値、データ不整合を重点的に確認してください。'],
['セキュリティ', '認証・認可、入力検証、情報漏えい、インジェクションを重点的に確認してください。'],
['テスト設計', '追加すべき自動テストを、再現手順と期待結果つきで提案してください。'],
['保守性', '可読性、重複、命名、将来の変更容易性を重点的に確認してください。'],
];
最後に、別のAIが4人分の結果をまとめる役をやります。
コードの差分
↓
4人のAIが別の観点でレビュー
↓
まとめ役のAIが重複を整理
↓
最終レビュー
1回のレビューで、個別レビュー4回とまとめ1回、合計5リクエストを使う計算です。
今回はベースのモデルとして gpt-oss-120b を使いました。
さくらのAI EngineはOpenAI互換のChat Completions APIを提供しています。
何を調べたか
いきなり実際のPRで試すのはさすがに怖かったので、まずは答えが分かっている小さな差分で様子を見ることにしました。
確認したかったのは次の2点です。
- 本物の問題をちゃんと見つけられるか
- 問題のない変更に、余計なことを言い出さないか
そのために、正解が分かっている差分を4パターン用意しています。実際に使った差分も公開しているので、興味があれば見てみてください。
| 差分 | 正解 |
|---|---|
| URLからIDを取る場所を変え、404処理にreturnを書かない | 2件のバグ |
| 管理者かどうかの条件を逆にする | 一般ユーザーが削除できる重大な問題 |
| SQLを安全な書き方から文字列連結に変える | SQLインジェクション |
| 変数名だけを変える | 問題なし |
実在するサービスのコードではなく、AIのレビュー精度を比べるためだけに用意した小さな差分です。
たとえば1つ目は、ユーザーを取得するAPIの差分です。
app.get('/users/:id', async (req, res) => {
- const user = await db.users.findById(req.params.id);
+ const user = await db.users.findById(req.query.id);
+ if (!user) {
+ res.status(404).json({ error: 'not found' });
+ }
+ await auditLog.write(`user=${user.email}`);
res.json(user);
});
この差分でAIに気づいてほしかったのは、次の2点です。
- URLは
/users/:idなのに、IDの取得元がreq.query.idにすり替わっている - ユーザーが見つからないときに404を返したあとも処理が続いてしまい、
user.emailの参照で落ちる
最初のAIレビューで困ったこと
最初は本当に雑で、AIに「この差分をレビューしてください」とだけ頼んでいました。
これが、思ったより厄介でした。本物のバグはちゃんと見つけてくれるのですが、
差分に書かれていないことまで平気で断定してくる時があります。
認証の仕組みもDBの種類も分からないはずなのに、「認可がない」「NoSQLインジェクションになる」と言い切ってくるときがあります。
AIが持っている一般的な知識が、今回のコードにそのまま当てはまるとは限らないわけです。
差分だけを見せられても、周りの前提が分からない以上、想像で補うしかありません。
改善したこと
そこで、AIへの頼み方を変えることにしました。
今回の変更で悪くなったことだけを指摘してください。
指摘には、根拠となる変更行と、変更前より何が悪くなったかを必ず書いてください。
こうすると、たとえば次のような指摘なら、後から人間が裏を取りやすくなります。
変更行: + const user = await db.users.findById(req.query.id);
問題: ルートは /users/:id なのにクエリからIDを取るため、
今までの /users/123 という呼び出しでIDを取得できなくなる。
逆に、変更行を示せない「認証を追加した方がよい」みたいな一般論は、最終結果からは外すようにしています。
実装としては、まず差分の追加行・削除行だけを取り出し、それを指摘の引用元としてAIに渡すだけの簡単な仕組みです。根拠行を必須にするプロンプトの詳細はこちらにあります。
function changedLines(diff) {
return diff
.split('\n')
.filter((line) => /^(?:\+|-)\s?/.test(line) && !/^(?:\+\+\+|---)/.test(line));
}
3つの方法を比べた結果
ここまでの改善が本当に効いているのか気になったので、次の3パターンを、
4つの差分それぞれで3回ずつ実行して比べてみました。
- 通常版:4役のAIとまとめ役に、普通の差分レビューを頼むだけ
- 根拠行必須版:同じ構成だが、指摘ごとに「変更行」と「変更前より悪くなった点」を必ず書かせる
- 検証役あり:根拠行必須版の結果を、もう1人別のAIに差分と照らし合わせて確認させる
検証役ありでは、2人目のAIに次のようなルールで確認させています。
変更行だけから説明できる指摘だけを残してください。
単なる変数名の変更は、外から見えるAPIが変わった証拠にはなりません。
同じ原因の指摘は1つにまとめてください。
各方式12回のレビューで、通常版・根拠行必須版は1回5リクエスト、検証役ありは1回6リクエスト。
全部合わせると192リクエストになりました。実行手順と採点基準は検証記録にまとめてあります。
実行自体は、リポジトリに入れているスクリプトに任せました。
# 通常版と根拠行必須版を比較する
RUNS=3 bash scripts/run-ab-evaluation.sh
# 検証役ありを3回ずつ試す
RUNS=3 bash scripts/run-verified-evaluation.sh
| 結果 | 通常版 | 根拠行必須版 | 検証役あり |
|---|---|---|---|
| 用意した問題を見つけた回数 | 12/12 | 12/12 | 12/12 |
| 問題のない名前変更で「指摘なし」と返した回数 | 0/3 | 1/3 | 3/3 |
本物の問題については、3方式とも全部見つけてくれました。ここは素直にすごいと思います。
一方で通常版は、問題のない名前変更に対しても、認証だ例外処理だ設計方針だと、
関係のない話をどんどん出してきます。
根拠行必須版にすると出力自体は短くなり、変更に近い話へ寄ってはくれるのですが、
それでも3回中2回は余計な指摘が残ってしまいました。
検証役を挟んだバージョンでは、安全な名前変更に対する余計な指摘は3回とも出ませんでした。
404後のreturn漏れとnull参照みたいに、根っこが同じ話も1つにまとまってくれています。
もちろん4ケースを3回ずつ試しただけの話なので、「これでAIレビューはもう安全」とまでは言えません。
ただ、最初のAIの出力をそのまま採用せず、もう一段確認を挟む価値は十分ありそうだ、というのが今回の手応えです。
出力を1つ比べてみる
問題のない名前変更の差分は、変数名を status から health に変えただけの、ごくシンプルなものです。
- const status = await healthService.status();
- return res.status(200).json(status);
+ const health = await healthService.status();
+ return res.status(200).json(health);
通常版だと、この差分に対して「例外処理がない」「認証がない」「返すデータを絞るべき」といった指摘が出てきます。どれも一般論としては確かにそのとおりなのですが、この名前変更が原因で新しく発生した問題ではありません。
対して、検証役ありの最終結果はこうでした。
確定した指摘なし
最初のAIが広く拾ってきた候補を、2人目のAIが「今回の変更が根拠になっているか」だけで絞り込む、というイメージです。
この確認用プロンプトも公開しています。
処理の流れとしては、まとめ役AIの結果を作ったあと、verified方式のときだけ検証役AIに渡す、というだけです。
const draft = await chat([{ role: 'user', content: editorPrompt(reviews) }]);
const final = REVIEW_MODE === 'verified'
? await chat([{ role: 'user', content: verifierPrompt(diff, draft) }])
: draft;
さくらのAI Engineでモデルを比べる
レビューの方式が固まったところで、検証役ありの方式に固定して、モデルを4つ変えて比べてみました。
基準モデル・コード向け・汎用・日本語系と違うものを選んでいます。
| モデル | 比較する理由 |
|---|---|
| gpt-oss-120b | これまでの検証で使ってきた基準モデル |
| preview/Kimi-K2.7-Code | コード向けモデルがレビューでも有利になるか見たかった |
| preview/Qwen3.6-35B-A3B | 別系統の汎用モデルとの比較用 |
| llm-jp-3.1-8x13b-instruct4 | 日本語の指示・レビュー文をどこまで扱えるか試したかった |
各モデルで4ケースを3回ずつ実行しています。
期待した問題は1つ目のケースに2件、残り2ケースに各1件で、合計12件です。
| モデル | 期待した問題の検出 | 安全な名前変更で「指摘なし」 | 今回分かったこと |
|---|---|---|---|
| gpt-oss-120b | 12/12 | 3/3 | 今回の構成ではもっとも安定 |
| preview/Kimi-K2.7-Code | 9/12 | 3/3 | 404後のreturn漏れは拾ったが、ID取得元の変更は3回とも見逃した |
| preview/Qwen3.6-35B-A3B | 10/12 | 3/3 | ID取得元の変更は3回中1回しか検出できず |
| llm-jp-3.1-8x13b-instruct4 | 採点できず | 採点できず | まとめ役への入力が7,732トークンとなり、4,096トークンの上限を超えて停止 |
ここで面白かったのは、コード向けをうたっているモデルだからといって、レビューでも強いとは限らないということです。少なくとも今回固定したプロンプトと差分では、素直にgpt-oss-120bが一番安定していました。
llm-jp-3.1-8x13b-instruct4が途中で止まったのも、単に「性能が低いから」という話ではなさそうです。個別レビューの段階で同じような指摘を何度も繰り返してしまい、その結果まとめ役に渡す文章がどんどん長くなっていました。今回のような多段構成のレビューでは、モデルの回答の質だけでなく、コンテキスト長も地味に効いてくる選定条件になる、というのは1つの発見でした。
モデル比較に使ったスクリプトはこちらです。
分かったこと
今回の検証を通して分かったのは、だいたい次のようなことです。
- AIは、パラメータの取り違え、認可条件の反転、SQLインジェクションのような分かりやすい問題は、ちゃんと見つけてくれる
- ただし、差分だけでは分からない前提まで勝手に想像してしまう
- 変更行を根拠として示させ、別のAIでもう一度確認させると、少なくとも今回程度の小さなテストでは誤検知をかなり減らせた
- モデル名だけでレビューの得意・不得意は判断できず、実際に同じケースで試してみる必要がある
- 多段のAIレビューでは、回答の質だけでなくコンテキスト長も意外と重要になる
- とはいえ、実際のコードを直す前には、最終的に人が判断する必要がある
まとめ
今回試した範囲では、AIコードレビューは、人が見落としがちな変更を拾ってくれる補助としては、そこそこ使えそうな手応えでした。
ただ「AIがそう言っているから直す」は、まだ危険だと思います。変更行、なぜ問題になるのか、修正案が本当に正しいのかは、結局のところ人が確認するしかありません。
今回はあくまで小さな4ケースだけの検証です。実際のプロジェクトの差分や、もう少し複雑な変更でも同じ傾向が出るかは、まだ分かっていないのでより深い検証が必要だと感じています。