DIVX テックブログ

catch-img

AIコードレビューの深さを左右するのは「プロンプト」か「エージェント数」か

こんにちは。株式会社DIVXの堀次です。

「AIによるコードレビューは便利ですが、その出力が浅く感じることも多いのではないでしょうか。」

指摘は一通り出てくる。でも「それを直すべき優先度は?」「実際にどのような攻撃が行われる可能性があるのか、そのシナリオがイメージできない」「どれくらいの規模で影響が出るのか不明」といった情報が薄い。

こうした「浅さ」を改善しようとしたとき、真っ先に思い浮かぶのは「エージェントを増やす」というアプローチです。ただ、ふと疑問が浮かびました。「これって、AIへの依頼方法の問題なのか、それともエージェントの数の問題なのか?」
今回はこの問いを検証するために、3パターンの比較実験を行いました。


こんな方に読んでほしい

  • AIレビューを利用しているが、出力が広く浅いと感じている
  • プロンプトを工夫するだけで改善できるのか知りたい
  • マルチエージェントの使いどころを実際の検証で確認したい

実験の設計

意図的にバグを追加したTypeScriptのユーザー管理サービス(7ファイル・約470行)を準備し、3パターンでレビューを実施しました。サンプルコードは GitHub リポジトリ に公開しています。

パターンA:単一エージェント・汎用指示(「シニアエンジニアとしてレビューして」)
パターンB:単一エージェント・3観点指示(セキュリティ・パフォーマンス・コード品質を明示)
パターンC:3エージェント並列(観点ごとに専門エージェントが担当)

コードに追加したバグは2種類です。

単一観点のバグ(セキュリティ・パフォーマンス・コード品質に分離したもの)

  • SQLインジェクション、XSS、平文パスワード保存、安全でないトークン生成、N+1クエリなど

複数観点をまたぐバグ(クロスカッティングバグ)
意図的に追加した、複数の観点が絡み合う問題です。

// ① キャッシュの無効化なし(セキュリティ × パフォーマンス)
// アカウントを停止したユーザーがキャッシュから取得され続ける + メモリリーク

// sample/cache.ts
const userCache = new Map<string, any>();
export function cacheSet(key: string, value: any): void {
  userCache.set(key, value); // TTL・上限・個別invalidateなし
}

// sample/userService.ts
export async function findUser(username: string) {
  if (cacheHas(username)) return cacheGet(username);
  // ...
  cacheSet(username, result.rows[0]);
}
export async function updateUserRole(userId: number, newRole: string) {
  await paramQuery("UPDATE users SET role = $1 WHERE id = $2", [newRole, userId]);
  // cacheClearAll() / 個別invalidate を呼んでいない
}

// ② エラーメッセージに内部情報を含む(セキュリティ × コード品質)
// DB構造が漏洩 + エラーハンドリング設計の問題

// sample/userService.ts
} catch (error: any) {
  throw new Error(`Database error: ${error.message}`);
}
// sample/db.ts でも同種のエラー漏洩あり(rawQueryのcatch内)

// ③ 非定数時間のパスワード比較 + 平文保存(セキュリティ × コード品質)
// タイミング攻撃が可能 + そもそもハッシュ化していない

// sample/auth.ts
export function hashPassword(password: string): string {
  return password; // ハッシュ化していない(DB上は平文で保存される)
}
export function verifyPasswordPlain(stored: string, input: string): boolean {
  return stored === input; // 短絡比較で時間差リーク
}

これらのクロスカッティングバグを追加した理由は、単一エージェントと専門エージェントのどちらが「複数観点をまたぐ問題」を効果的に扱えるかを確認するためです。

3パターンとも Claude Code のヘッドレスモード(claude -p)で実行しました。各エージェントは Read ツール等を使ってサンプルコードを自律的に読み取りながらレビューします。パターンCはこのclaude -pを3並列で起動しています。

各実行は独立したプロセスとして起動するため、パターン間での状態共有はありません。実験前に Claude Code の自動メモリディレクトリが空であることも確認しており、過去の会話履歴によるコンテキスト混入も発生していません。

なお、本記事で示す実行時間や発見数はいずれも1試行の観察値です。Claude Codeは同じプロンプトでも文脈やサンプリングのゆらぎによって出力が変動するため、絶対値そのものではなくパターン間の傾向や差分の方向性を読み取っていただく目的で記載しています。


結果

実行時間

パターン

方式

時間

A

単一・汎用指示

約71秒

B

単一・3観点指示

約222秒

C

3エージェント並列

約127秒(最長エージェント基準)

パターンBは3観点を1エージェントで丁寧に検証するため、最も時間がかかりました。パターンCは3つのエージェントが同時並行で処理するため、合計作業量はBの約3倍ながらも実時間はBを大きく下回りました。並列実行の効果が表れています。

ただし、ここで短縮されているのは実時間だけである点には注意が必要です。パターンCは3つのエージェントを同時に走らせているため、消費するトークンや計算量の総量は並列数ぶん(今回はBの約3倍)増えます。実時間は短縮されるが、消費リソースは並列数ぶん増えるというトレードオフの上に成り立った数字です。

発見した問題数

パターン

合計

A:単一・汎用

約24件

10

6

8

B:単一・3観点

26件

14

9

3

C:3エージェント並列

約34件(重複除外後 約28件)

約14

約12

約2

パターンCは3エージェントの合計が約34件ですが、N+1クエリ(3エージェントが言及)、キャッシュ問題(2エージェント)、any/==の型問題(2エージェント)など重複報告があるため、純発見数は約28件です。「高」深刻度の比率で見ると A:42%(10/24)、B:54%(14/26)、C:50%(14/28)となり、純発見数が最多のCよりもBのほうが「高」の割合は高いという結果でした。件数だけ見るとCが最多ですが、今回の検証で特に注目したのは内容の差でした。


パターンAの出力(実際の結果より抜粋)

セキュリティ・パフォーマンス・コード品質の3カテゴリに分けて報告されました。「高」深刻度の問題(セキュリティ)の一部を抜粋します。

- 平文パスワード保存(auth.ts:17-20)
- SQL インジェクション(userService.ts:14-16, 84-87 / db.ts:18-26)
- XSS(userService.ts:54-61, routes.ts:83-90)
- 権限昇格(routes.ts:32-37 — req.body.role 素通し)
- トークンの予測可能性(auth.ts:11-13)
- タイミング攻撃(auth.ts:25-27, 31-40 / userService.ts:34)
- エラーメッセージで内部情報を漏洩(db.ts:24, userService.ts:21, routes.ts:27/47)
- ユーザー存在判別(auth.ts:60-66 のメッセージ分岐)
- 認可漏れ(routes.ts:62-71 — トークンだけで全ユーザー返却)
- セッション無期限(types.ts:27-31, auth.ts:44-50)

各問題に対しては修正コード例も提示されました。ただし問題同士の関係性や、ユーザー規模ごとの定量的な影響には触れていませんでした


パターンBの検証:プロンプトを専門化すると何が変わるか

汎用指示から3観点を明示した指示に変えると、同じ単一エージェントでも出力の質が大きく変わりました。

パターンAは「タイミング攻撃が可能」と「平文パスワード保存」を別々の問題として報告しました。

パターンBの実際の出力はこうでした(※本記事中のCVSSスコアはAIによる推定値です。詳細は本セクション末尾の補足を参照)。

#7 ユーザー名列挙 + タイミング攻撃(Medium, CVSS 5.3)
該当箇所:

  • sample/auth.ts:54-67 login が「ユーザーが存在しません / パスワードが違います」を出し分け
  • sample/auth.ts:31-40 verifyTokenUnsafe が長さ違いで早期 return → 長さリーク
  • sample/userService.ts:34 === 比較

攻撃シナリオ:メールアドレス辞書を /login に流してエラーメッセージ差で有効ユーザーを列挙。続いて Credential Stuffing で侵入。レート制限なし(routes.ts 全体)なので 1万件/分も容易。

複数ファイル(auth.ts × userService.ts × routes.ts)にまたがる問題を一つの攻撃連鎖として記述しています。Aは個別箇所として並列に列挙していたのに対し、Bはエンドツーエンドの攻撃ストーリーで提示する点が大きな違いでした。

さらにパターンBは「キャッシュ起因の認可失効遅延(CVSS 7.5)」を高深刻度のセキュリティ問題として記述し、「攻撃者を検知して管理者が deactivateUser を実行 → しかしキャッシュには active=true の旧オブジェクトが残るため、攻撃者は引き続き API にアクセス可能」というインシデント対応の現場視点まで踏み込みました。

パターンBだけが発見した問題もありました。

  • 「今すぐ塞ぐべき4つ」の優先順位提示:SQLi / 権限昇格 / 認証欠落 / 平文パスワードを「単独で全権限喪失に直結する」と整理し、修正の着手順を明示
  • 攻撃連鎖の可視化:SQLi → 平文パスワード露出 → トークン奪取 → セッション乗っ取りという一連の流れを1ストーリーで記述
  • インシデント対応視点:「停止したのに止まっていない」現象など、運用時の問題への言及

プロンプトの専門化により、発見数の総量よりも「高」深刻度の比率が大きく上がりました(A:42% → B:54%)。 「AIレビューが浅い」と感じている場合、まず指示の与え方を見直すことで改善できる可能性があります。


パターンCの検証:エージェントを分業させると何が変わるか

パターンCのセキュリティ専門エージェントの出力を抜粋します。

1. 【高】SQLインジェクション(文字列補間)
該当箇所:

  • userService.ts:14-15 findUser
  • userService.ts:86 findUsersByDomain
  • db.ts:18-26 rawQuery 自体がパラメータ化なしの実行口
  • routes.ts:75-79routes.ts:20-22 から到達可能(認証・検証なし)

深刻度:高 / CVSS 9.8(AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)
攻撃シナリオ:

  • GET /users/'%20OR%201=1-- → 全ユーザー情報を取得
  • GET /users/by-domain?domain=x'%20UNION%20SELECT%20password,token,... → 認証情報を抜き出し
  • db.ts:24 がエラー詳細をそのまま返すため、エラーベース SQLi(型不一致・列名漏洩)で偵察を加速可能

CVSSスコアやCWE番号の推定値はAIによる算出であり、公式な評価基準に基づくものではないため参考程度に留めてください。

パフォーマンス専門エージェントの「致命的」相当の出力はこうでした。

致命的:N+1クエリ — userService.ts:39-50 getUsersWithPostCount
ユーザーごとに SELECT COUNT(*) FROM posts WHERE user_id = ... を発行。1万ユーザーなら 1 + 10,000 回のラウンドトリップ。

致命的:コネクションプール設定が全部デフォルト — db.ts:5-7
maxidleTimeoutMillisstatement_timeout 未設定。N+1と組み合わさると並行リクエスト時にプール枯渇でサービス停止。

致命的:インメモリキャッシュが無限増大 — cache.ts × userService.ts:findUser
ユニーク username 引数でアクセスされ続けるとプロセスメモリを食い尽くす(攻撃可能なメモリリーク)。

ドメイン固有の深掘りではCが上回る結果となった

各専門エージェントがパターンBでは見落とした問題を発見しました。

  • セキュリティ専門のみの発見:CSPヘッダ未設定、X-Content-Type-Options: nosniff 未付与、エラーベース SQLi の具体的な偵察手順
  • パフォーマンス専門のみの発見:「致命的」3点セット(N+1 × コネクションプール × 無制限キャッシュ)の連鎖シナリオ、負ヒット保護が壊れている問題(cacheHas + cacheGet の二段構造)
  • コード品質専門のみの発見:Clean Architecture 風のディレクトリ再構成提案、auth.ts から DB 操作を切り離す具体的な DI 化例

特にコード品質専門エージェントは、各モジュール間の依存関係まで踏み込んで分析しており、リファクタリング後の構造(http/routes → services → domain + infra (db, cache, hasher) → domain)を具体的に提示しました。これは1エージェントが3観点を兼ねるパターンBには現れにくい深さでした。

今回の試行ではクロスカッティングバグの関係性接続でBが上回った

興味深かったのはクロスカッティングバグへの対応の違いです。

キャッシュの無効化なし問題に対して、パターンCの各エージェントはそれぞれの観点から分析しました。

  • セキュリティ専門:「権限失効遅延(取り消されたはずの権限で操作可能)」
  • パフォーマンス専門:「メモリリーク・スループット劣化」
  • コード品質専門:「テスト時の状態リーク」

セキュリティ専門エージェントは自スコープ内で「権限・無効化問題」と「メモリDoS」を1問題内に統合する形で扱いました。一方、パフォーマンス専門エージェントは性能観点のみで止まり、権限失効リスクへの言及はありませんでした

パターンBはこれを「**#8 キャッシュ起因の認可失効遅延(High, CVSS 7.5)**」として整理し、「攻撃者を検知して deactivateUser を実行 → しかしキャッシュには active=true の旧オブジェクトが残るため、攻撃者は引き続き API にアクセス可能」というインシデント対応視点まで踏み込みました。同じバグを観点別に分割して扱うCと、攻撃ストーリー単位で統合するBの違いが、今回の結果に表れていたと感じています。


3パターンの使い分け

今回の実験から得られた各パターンの使い分けの目安を以下に示します。

パターンA(単一・汎用)が向く場面

  • 軽微な変更のざっくり確認
  • リファクタリングの方向性チェック
  • 社内ツールや影響範囲が限られた変更

パターンB(単一・3観点)が向く場面

  • 認証フローや決済処理など、セキュリティとパフォーマンスが絡み合うコード
  • 「このバグは複数の観点でどう影響するか」の全体像を把握したいとき
  • 問題の優先順位をステークホルダーに説明する材料が必要なとき

パターンC(3エージェント並列)が向く場面

  • セキュリティ・パフォーマンス・設計を個別に徹底的に掘り下げたいとき
  • 時間を短縮しながら広範囲をカバーしたいとき(実時間の短縮と引き換えに、消費リソースが並列数ぶん増えることを許容できる場合)
  • 新規機能など、各観点でのチェックリストを揃えたいとき

まとめ

「AIコードレビューの浅さ」を改善する方法として、3パターンを比較しました。

プロンプトの専門化(パターンB)は、今回の検証では比較的コストが低い改善手段でした。 エージェントを追加する前に、観点を明示する・CVSSスコアや定量試算を要求するといった指示の工夫だけで、発見数の総量だけでなく 「高」深刻度の比率が上がる (A:42% → B:54%)傾向が見られ、攻撃連鎖の記述や優先順位の提示など分析の質が向上しました。

エージェントの分業(パターンC)は、ドメイン固有の深掘りと並列化による時間短縮の面で効果が確認できました。 同じ3観点の検証にBは222秒、Cは127秒と並列処理の効果が表れています。ただし短縮されるのはあくまで実時間であり、消費するトークン・計算量の総量は並列数ぶん増えるため、速さとコストのトレードオフとして判断する必要があります。また今回の実験では、「高」深刻度の比率がC:50%(14/28)とB:54%(14/26)をわずかに下回り、「複数観点をまたぐ問題の関係性把握」においても単一エージェントが上回る場面がありました。どちらが常に優れているかではなく、目的によって使い分けるのが現実的ではないかと考えています。

今回最も印象に残ったのは、「どちらか一方が常に正解ではない」という結果が出たことそのものでした。 AIツールの使い方に対しても、コードと同じように「なぜそれを使うのか」を問い続けることが大切だと改めて感じました。


参考

お気軽にご相談ください


ご不明な点はお気軽に
お問い合わせください

サービス資料や
お役立ち資料はこちら

DIVXブログ

テックブログ タグ一覧

人気記事ランキング

関連記事