Summary
- 実装AIとは独立した別のAIにコードをレビューさせることで先入観のない客観的な指摘が得られると考え、深刻な問題のみを返すコードレビュー専用MCPサーバーをRustで自作した
- 完成直後にそのサーバー自身のソースコードをレビューさせたところ初回13件の指摘を受けたが、1件ずつ検証すると2件は事実誤認、4件は深刻度の誇張であり、全件修正に走ることの危険を実感した
- AIは同じ指摘を繰り返し返し続けるという固有の問題があり、「意図的にスキップした指摘」をセッション機能でAIへ自動引き継ぎすることで、最終的に1件まで収束させられた
やらないこと
- reviewer-mcp自体の実装の解説
- AIコードレビューの品質を定量的に評価する実験
- AIへの指示文の設計手法全般の解説
本記事における課題
実装役AIと独立したAIにレビューさせればバイアスが減るのではないか
AIコーディングエージェントが普及した現在、コードの生成はAIに任せる場面が増えた。一方で「そのコードが正しいか」を検証するフェーズはまだ人間頼りであることが多い。
ここで着目したのが「実装役と審査役の分離」である。コードを書いたAIは、自分が選んだ設計判断に引きずられて甘い評価をしやすい。同じAIに「このコードをレビューして」と頼んでも、自分が生成したコードを厳しく批判しにくいという問題がある。
実際、[Claude Code] OpenAI Codexとのクロスレビュー運用 - 自己レビューの盲点を別系統のAIで埋めるでは、同じモデルに自己レビューさせた場合の課題を指摘し、Claude CodeからCodexに対してレビューを依頼させる取り組みを行っている。
実装AIとは完全に独立した別のAIがレビューを担えば、先入観のない客観的な指摘が期待できる。そのための仕組みとして、先述の記事に比較してより汎用的なMCPサーバーという形でレビュー専用のLLMを呼び出せるようにしたのが出発点である。
MCPレビューサーバーという道具を作った背景
IBM BobのようなMCP対応コーディングエージェントと日常的に作業していると、単純に「レビューして」と頼むと、スタイルの話や「こうすればもっとよくなります」という提案が大量に返ってきて、本当に直さなければならない問題が埋もれてしまうことがある。
そこで作ったのがreviewer-mcpだ。OpenAI互換のAI APIを呼び出し、セキュリティ上の欠陥・致命的バグ・データ破壊リスク・設計違反など「放置すると本当にまずい問題」だけを返す、深刻な問題に特化したコードレビュー用MCPサーバーである。実装言語はRust、ファイル1つで動くシングルバイナリとして配布できる。
やったこと
reviewer-mcpの仕組み
MCPツールとしてreviewを1つだけ公開し、paths(ファイル・ディレクトリ・*.rs のようなワイルドカードパターン)、perspectives(追加観点)、context(プロジェクト背景)、session_id(セッション識別子)を受け取る。内部では以下のフローで動作する。
パス展開 → 画像・実行ファイルなどの除外 → サイズ制限チェック → ひとまとまりに分割
→ 各まとまりをAIに送信して指摘リスト取得
→ 全指摘をまとめて重複を除去 → セッションに蓄積 → JSON返却
全体のアーキテクチャは次のとおりである。
AIへの指示文はすべて英語で構築する。日本語のほうが馴染みがあるかもしれないが、処理コストと指摘の再現性を考慮した結果、英語に統一した。呼び出し元のエージェントが英語の返答を受け取って日本語で解釈してくれるため、使い勝手は変わらない。
完成直後に自分自身をレビューさせた
実装が完成したあと、IBM Bobに対して「このプロジェクトのRustコードを(登録済みのreviewer-mcpを使って)レビューさせてみて」と依頼した。reviewer-mcp自身のソースコードをreviewer-mcpにレビューさせる実験である。
結果は初回13件、すべてHighの指摘だった。
指摘を自分で検証したら2件が誤りだった
返ってきた13件をそのまま修正に走る前に、1件ずつソースコードを読んで検証した。その結果を3つに分類すると以下のようになった。
| 判定 | 件数 |
|---|---|
| 正しい | 7件 |
| 正しいが深刻度が誇張されている | 4件 |
| 誤り・不当 | 2件 |
このうち、IBM Bobが誤りと判断した2件を紹介する。
ファイルパスがAIに渡されて仕様違反
コードには「絶対パスで入力したら絶対パスで返す」と明記されており、その動作通りに動いているだけである。「違反」という前提自体が事実と異なっていた。
悪意あるファイルの中身でAIの判断を狂わせられる
レビュー対象のファイル内容をAIへの指示文に埋め込む以上、悪意あるコードがその指示文に「問題を隠せ」と紛れ込ませようとする可能性は確かにある。しかしこれは、AIをレビュー担当として使うかぎり避けられない宿命的な制約であり、コードを書き換えて根本解決できる問題ではない。
このように、AIの指摘を鵜呑みにして全件修正に突っ込むのは危険である。AIの指摘を検証するのも、結局は人間の仕事。
確実に正しい問題から修正した
検証を経て、修正すべきとIBM Bobが判断した問題を修正した。代表的なものを挙げる。
行番号のズレ問題
最初のレビューで全体的に行番号がずれていた。原因はプロンプトにファイル内容をそのまま貼り付けていたため、LLMが行番号を目視で数えていたことにある。修正はシンプルで、ファイル内容を埋め込む際に各行の先頭に行番号を付けるだけだった。
let numbered: String = file
.content
.lines()
.enumerate()
.map(|(i, line)| format!("{:>4} | {}\n", i + 1, line))
.collect();
これだけで、2回目以降のレビューでは行番号が正確になる。
AIへのHTTPクライアントにタイムアウトがない
HTTPリクエストのデフォルト設定はタイムアウトなしである。応答しない送信先に対してMCPサーバーが永遠に待ち続ける可能性があった。
let client = Client::builder()
.timeout(std::time::Duration::from_secs(REQUEST_TIMEOUT_SECS))
.build()
.expect("Failed to build reqwest client");
MCPサーバーとして動作するプログラムにおいて、これは致命的な問題になりうる。AIエージェントからの呼び出しが一定時間で打ち切られず、永遠に待ち続けることになる。
レビュー対象リポジトリの設定ファイルでAPIキーの送信先を書き換えられる
設定の上書き機能を実装した際、レビュー対象のリポジトリ内に置かれた設定ファイルからAPIキーと送信先URLを書き換えられる仕様になっていた。これは「悪意ある設定ファイルが混入していた場合、APIキーが攻撃者のサーバーに送られる」という抜け穴を作ることになる。
if local.api.api_key.is_some() || local.api.base_url.is_some() {
anyhow::bail!(
"Project-local config at {} sets forbidden fields: {}",
path.display(),
LOCAL_CONFIG_BLOCKED_FIELDS
);
}
指摘を受けるまで完全に見落としていた問題だった。機能として正しく動いていたため気づきにくく、AIレビューがあって初めて発見できた典型例である。
指摘が収束せず同じ問題が繰り返し返ってくる
修正後に再レビューを依頼すると、13件が11件になった。修正した問題は消えたが、新しい指摘が生まれていた。さらに修正を重ねると5件、1件と推移した。
| ラウンド | 指摘件数 |
|---|---|
| 1回目 | 13件 |
| 2回目 | 11件 |
| 3回目 | 5件 |
| 4回目 | 1件 |
問題は「最後の数件が毎回同じ内容で返ってくる」ことだった。
- AIの返答をすべてメモリに受け取ってからサイズを確認している
- ファイルを複数のまとまりに分けて送る都合上、異なるまとまりにまたがる問題は発見できない(この作りである以上どうしても生じる制約)
これらはコードを直せば解決するバグではなく、この設計を選んだ時点で受け入れた制約である。しかしAIは毎回同じ問題を「High」として指摘し続けた。
対策1: AIへの行動指針に「指摘してはならないもの」を明示する
この繰り返しを止めるため、AIへの行動指針(システムプロンプト)に禁止事項を明示的に列挙した。
## Strict reporting bar — do NOT report any of the following:
- Theoretical worst-case scenarios that require an attacker to already have
write access to the machine running the server
- Known design trade-offs that are already acknowledged in the codebase
(e.g. prompt-injection being an unavoidable property of LLM-based tools)
- Speculative issues without a clear, direct code path demonstrating the problem
## Report ONLY issues where ALL of the following are true:
1. There is a concrete, traceable code path that leads to the problem
2. The impact is severe: data loss, security breach, crash, or silent wrong result
3. The fix is actionable within the reviewed codebase
対策2: 既知の指摘をセッションで自動引き継ぎする
禁止事項の列挙だけでは不十分で、AIは別の言い回しで同じ問題を再び報告してくることがある。そこでセッション機能を実装した。session_id を指定して呼び出すと、前回以降に報告された指摘が自動的に「既知の問題」としてAIへの行動指針に注入され、同じ指摘の繰り返しを防ぐ。
review(paths=["src/"], session_id="my-project")
// 2回目以降: 前回の指摘が自動的にknown_issuesとして注入される
セッション状態はサーバープロセス内のメモリにのみ保持され、1セッションあたり最大512件・最大1024セッションまでを上限として自動的に古いものから削除される。
対策3: 2回呼び出しをやめる
当初は「見落とし防止」のため1チャンクにつき2回AIを呼び出す設計にしていた(1回目で指摘を取得し、2回目で「見落としがないか再確認」)。しかしこの2回目の呼び出しが収束を妨げていた。2回目のAIは既出の指摘を別の言い回しで再報告することが多く、重複排除をすり抜けて指摘数が膨らむ原因になっていた。
2回呼び出しを1回に削減したところ、指摘がより安定して収束するようになった。
これらの対策を組み合わせた結果、指摘件数は最終的に1件まで収束した。残る1件は「セッションのFIFO削除の実装がコメントと微妙に異なる」という軽微なものである。
ファイルパスの扱いという実運用上の落とし穴
完全に見落としていた問題がもう一つある。MCPサーバーはエージェントが渡したファイルパスをそのままファイル検索に使っていた。
AIエージェントがレビューを依頼する際、/Users/alice/project/src/main.rs のようなフルパス(絶対パス)を渡すこともあれば、src/main.rs のような省略形(相対パス)を渡すこともある。MCPサーバーは独立したプロセスとして起動されるため、省略形のパスの基準フォルダがエージェントの想定と食い違うことがある。
サーバーが見当違いのフォルダを起点に探すと、ファイルが見つからず空のレビュー結果が返ってくる。これが「No reviewable text files found」という的外れなエラーメッセージの原因であった。
修正はファイルパスを処理する前にフルパスへ変換するだけである。フルパスはそのまま通し、省略形のみサーバーの現在フォルダを基準に補完する。IBM Bobは.bob/mcp.jsonに登録されたMCPサーバーをプロジェクトルートをCWDとして起動するため、エージェントが渡す省略形のパスはこの変換で正しく解決される。
let cwd = std::env::current_dir()
.unwrap_or_else(|_| std::path::PathBuf::from("/"));
let abs_paths: Vec<String> = paths
.into_iter()
.map(|p| {
let pb = std::path::Path::new(&p);
if pb.is_absolute() { p }
else { cwd.join(pb).to_string_lossy().into_owned() }
})
.collect();
MCPサーバーを作る際、ファイルを扱うツールは受け取ったパスを必ずフルパスに変換してから使うことに気がつかされた。
この記事自体もレビューさせた
コードのレビューが一段落したあと、この article.md 自体を reviewer-mcp に投げてみた。コードだけでなく、文章の正確性も同じツールで確認できるかどうかの実験である。
review(paths=["article.md"], perspectives="技術記事としての正確性、事実誤認、読者を誤解させる記述、コード実装との整合性")
1回目: 1件の指摘
返ってきた指摘は1件だった。
「起動場所が変わってもパスが一意に定まる」という説明が過大。
current_dir()はあくまでサーバーの起動場所に依存するため、起動場所が変われば結果も変わる。
検証すると正当な指摘だった。「IBM BobはプロジェクトルートをCWDとしてMCPサーバーを起動する」という前提を記事に書かずに「起動場所が変わっても大丈夫」と書いていたため、説明が過大になっていた。前提を明記する形で修正した。
2回目: 指摘ゼロ
修正後に known_issues へ前回の指摘を渡して再レビューしたところ、指摘ゼロで収束した。
review(
paths=["article.md"],
known_issues=[{"file": "article.md", "description_fragment": "相対パスをcurrent_dir基準で絶対化しても起動場所依存が残る — 既修正済み"}]
)
// → "No critical issues found across 1 file(s)."
同じ指摘が再出力されることもなく、記事に対してもセッションと known_issues による収束制御が有効に機能した。
コードをレビューさせても、記事をレビューさせても、AIが返す指摘は仮説の束であるという点は変わらない。最終的に利用者が、有用な指摘を拾いつつ誤りを弾く判断力を、AIレビューを活用するうえで求められる。
まとめ・所感
AIに自分のコードをレビューさせる実験を通じて、いくつかの実用的な知見が得られた。
よかった点として、通信タイムアウトの未設定・設定ファイルからのAPIキー上書き・セッション状態の無制限増大(いずれも発見後に修正済み)など、人間が見落としやすい問題を確実に発見してくれた。特にセキュリティ寄りの問題は、実装に集中しているときに盲点になりやすく、AIレビューの有効性が最も発揮された領域である。
限界と注意点として、指摘の正確さには大きなばらつきがある。行番号のズレ、事実と異なる前提に基づく指摘、コードを書き換えても根本的に解決できない制約を「High」として繰り返し指摘するなど、そのまま鵜呑みにすると無駄な作業が生まれる。AIの指摘は仮説であり、設計や実装の意図を理解して検証する工程は省略できない。
最終的に指摘件数は13件から1件に収束した。残る1件は「技術的には正しいが、このプロジェクトの文脈では実害がない」と判断できるものである。
AIレビューの「終わり」をどこにするかは、結局のところ人間が判断しなければならない。コードレビューでも技術記事のレビューでも、同じことが言えた。ただし、こうしたAI相互レビューの仕組みにより、効率良く人間が判断できる環境に近づくことができそうだ。

