対象読者: AIにコードや文章のレビューをさせているが、返ってきた指摘の捌き方が定まっていない人
異なるベンダーのAIを2〜3体並べて同じ対象をレビューさせ、返ってきた指摘を突き合わせてから採否を決める、という運用を続けています。レビュー1回ぶんの成果物は「依頼プロンプト」「各レビュアーの生の指摘」「採否記録」の3種類です。生の指摘はレビュアーの数だけ出るので、実ファイルは prompt 1本・review 2〜3本・intake 1本で4〜5本になります。この3種類をどう作り、どう回しているかを書きます。
指摘の的中率をこの記録から集計した結果は、AIレビュアーの指摘はどれだけ当たるのかに、指摘の中身の分布はAIに書かせたコードのレビュー指摘95件を全部分類したにまとめてあります。本記事は、その数字を生んでいる工程のほうです。
3つのフェーズ
flowchart LR
A["フェーズA<br/>プロンプト生成<br/>review-prompt_<topic>_<date>.md"] --> B["実行<br/>レビュアーを並列起動<br/>review_<topic>_<model>_<date>.md"]
B --> C["フェーズB: intake<br/>再照合・突き合わせ・採否<br/>intake_<topic>_<date>.md"]
C --> D[修正の反映]
3つのうち、AとBは対象を理解しているエージェント(ふだんその記事やコードを書いているセッション)の仕事です。実行だけは、対象を何も知らない外部のエージェントに任せます。知らないほうが、書き手の思い込みを共有しないぶん、読み手として機能します。
ファイル名は3種類に固定してあります。
| フェーズ | ファイル名 |
|---|---|
| A | review-prompt_<topic>_<YYYYMMDD>.md |
| 実行 | review_<topic>_<vendor-modelID>_<YYYYMMDD>.md |
| B | intake_<topic>_<YYYYMMDD>.md |
<vendor-modelID> には、レビュアー自身が名乗った識別子を入れさせます(codex-gpt-5、claude-opus-4-8-1m など)。codex や claude のようなベンダー名だけに潰すと、あとから「どのモデルの指摘が当たっていたか」を数えられなくなります。
ただし、この識別子は自己申告です。ランナーは起動した CLI がどのバージョンで動いたかを知らないので、モデルに名乗らせるしかありません。実際に、自分を codex-gpt-5 と名乗った出力が GPT-5.5 だったことがあります。記録をモデル別の集計に使うなら、CLI 側の設定と突き合わせて実際のバージョンを確かめる工程が要ります。
フェーズA:依頼プロンプトに何を書くか
「見ないもの」を先に宣言する
プロンプトで最初に書くのは、レビュー対象ではなく対象外です。
## このレビューで見るもの / 見ないもの
**見ないもの(機械検証済み・再検証不要)**:各記事の「再現→修正」の動作。
固定した Node イメージのコンテナ内で「エラー再現 → 修正適用 → シグネチャ消滅」を
検証済み(`verification/report.json` の `verified: true` が根拠)。ここは疑わなくてよい。
**見るもの(機械が保証できない領域=ここに集中)**:
- (a) 未検証のバージョン境界・事実主張。一次情報(公式 changelog / API ドキュメント)で裏取りする
- (b) 編集規約への準拠
- (c) 助言の安全性・誤解を招く表現
これを書かないと、3体が揃って「手順が本当に動くか確認しましょう」と書いてきます。テストや検証スクリプトで機械が保証している範囲は、人手のレビューでもAIのレビューでも再確認する価値がありません。CIが緑である前提を明記して、ロジックと不変条件に指摘を寄せる。コード差分のレビューなら「lintと型チェックは緑前提」と書きます。
レビュアーができないことを書く
レビュアーはファイルを読んで推論する第2の読み手であって、コードを実行して正しさを検証するツールではありません。ここを曖昧にすると、実行しないと分からないことを実行したかのように書いてきます。
そこでプロンプトに、コード実行もコンテナ起動もできない前提を明記し、環境や再現性への懸念は「提供ファイル内に根拠がある場合だけ指摘にし、根拠がなければ実行して確かめるべきコマンドの提案にとどめる」と指示しています。リリース状況や最新バージョンのようなモデルの記憶に依存する話題も、ファイル内に根拠がある場合だけ指摘にする、と条件を付けます。
出力フォーマットを固定する
自由記述で返させると、intake で突き合わせられません。項目を揃えます。
- 深刻度(High / Medium / Low / Nit)順に並べる
- 各項目は
[深刻度] タイトル→file:line→ 何が問題か → 具体的な失敗シナリオ(入力→誤結果) → 推奨修正 - 確証ありと要確認を区別する
- 末尾に「必ず直す High の要約」と「追加すべきテスト」
失敗シナリオを必須項目にしているのは、書けない指摘を落とすためです。「ここは壊れやすい」で止まる指摘は、入力と誤結果の形に落とせません。
なお、レビュアー自身が申告する確証レベルは、採否の根拠には使えません。根拠のある主張にも、示した出典では裏が取れない主張にも、同じ「確証あり」が付きます。この欄は「裏取りキューに入れる合図」として読みます。
実行:対話CLIを並べて起動する
依頼プロンプトが1本のファイルとして存在するので、あとは各レビュアーの CLI にそれを読ませるだけです。このブログのリポジトリでは、ランナー1本で Windows Terminal を分割して並列起動しています。
pwsh tools/peer-review/run.ps1 # 3-way(codex + claude + agy)
pwsh tools/peer-review/run.ps1 -Two # agy を外して 2-way
pwsh tools/peer-review/run.ps1 -DryRun # 起動せず、生成されるコマンドだけ表示
最新の review-prompt_*.md を拾って topic と日付を解析し、レビュアーの数だけペインを開き、各ペインに「このプロンプトを読んでレビューし、結果をこのファイル名で書け」という最初の一言を渡します。
ヘッドレス実行をやめた理由
各CLIには非対話モード(-p や exec)があります。以前はそれで組んでいて、保守できなくなりました。provider 側のCLI仕様が変わるたびに壊れるからです(非対話経路で stdin がハングする、といった形で出ます)。
いまは対話モードのまま起動し、最初の一言だけ自動で流し込む形にしています。人間が見ている画面と同じランタイムを通るので、仕様変更の影響を受けにくい。承認ダイアログは自動承認オプションで消します。
# まず安全側から。書き込みをリポジトリ内に絞る(codex の例)
codex -s workspace-write -a never -C <repo> "<seed>"
ランナーが承認ダイアログを消すために実際に渡しているのは、これより広い権限です。codex は -s danger-full-access -a never、claude は --permission-mode auto、agy は --dangerously-skip-permissions で、3つとも自動承認かつサンドボックスなしで起動します。手元では全コミット済みのクローンで回しているので成立していますが、これをそのまま自分の環境へ貼るのは勧めません。
危険なのは「git で戻せる」の外側です。
- サンドボックスなしなので、書き換え先はリポジトリ内に限りません。 リポジトリ外のファイルを消されても git では戻せない(ネットワークまで到達するかは各 CLI のドキュメントで確認してください)。
- git で戻せるのは、コミット済みで追跡されているファイルだけです。 未コミットの変更と未追跡ファイルは
git restoreでは戻りません。「全コミット済み」を満たさない作業ツリーで走らせると、この前提が崩れます。 .envや鍵も読み取り範囲に入ります。 自動承認なので、秘匿情報を読む動作も止まりません。「プロンプトでレビューに限定する」は強制力のある制御ではなく、プロンプトが不完全でもエージェントは止まらない。
読者が試すなら、codex は上の workspace-write を出発点にし、claude と agy も含めて、信頼できない対象は使い捨てのクローンか専用アカウントで回すのが安全です。フルアクセスは、隔離した環境で必要だと判断したときだけ明示的に選ぶ。
詰まりどころ
npm の shim が引数を壊す。 PATH 上の codex が npm の .cmd shim だと、引数が cmd.exe の %* を経由します。複数行・空白・非ASCIIを含むプロンプトが1つの壊れた値に潰れる。ランナー側で実体の .exe を解決してから起動しています。
レビュアーが1体になったら止める。 CLIが PATH になければ警告してスキップしますが、残り2体を切ったら中止します。1体だけでは異ベンダーの突き合わせという完了条件を満たしません。単独のレビューにも拾える指摘はありますが、この工程が価値を置いているのは突き合わせのほうです。
フェーズB:intake で採否を決める
返ってきた指摘は、そのままでは修正計画になりません。
- 現行ファイルで再照合する。
file:line付きの指摘でも、参照先の取り違えは起きます。指摘された行に、指摘された記述が本当にあるかを見る - 一致と単独に仕分ける。 複数モデルが独立に同じ箇所を挙げた指摘は、裏取りを先に回す。単独指摘は、裏取りを通すまで採用しない
- 一次情報と実機で裏を取る。 バージョン番号は公式 changelog、挙動はコンテナで確かめる
- 採用・棄却(誤検知)・要確認を記録する。 判断の理由も書く
- severity を付け直す。 レビュアーの High をそのまま作業順に使わない
5番目は、実際に食い違います。同じ箇所に片方が High、もう片方が Low を付ける。深刻度の差は、記事なら「単体で見るか、記事群の整合で見るか」、コードなら「その関数の中で見るか、呼び出し側まで含めて見るか」の差になります。どちらの視野を採るかは、レビュアーではなく持ち主が決めることです。
誤検知の判定を省略しない
intake には「棄却=誤検知」の判定を必ず書きます。採用した指摘だけ記録して修正に進むほうが速いのですが、それをやると、レビュアーがどれだけ当たるのかを後から測れなくなります。
同じ理由で、prompt / review / intake の3点セットはすべてコミットします。モデル識別子も、棄却の理由も残す。レビューの記録は、レビューの副産物であると同時に、レビュアー自身を評価する一次データです。
回してみて分かったこと
未検証リストは、作るだけでは消えません。 各記事のサマリーに「裏取りが必要な項目」の欄を用意していました。欄はあるのに、確認した結果を本文へ反映する工程がない。あるバッチで High が5本の記事に集中したとき、その論点はほぼ全部この欄に列挙されていました。書いてあって、閉じていなかった。
対応として、各対象に「主張」「一次情報のURL」「確認日」を並べたファイルを置き、リストを閉じるところまでを工程に入れました。欄を増やすのではなく、欄を消し込む担当を工程に足す。
指摘はリードであって、結論ではありません。 レビュアーは実行しない読み手なので、findings は「確認すべきリード」として扱います。実行時の正しさは、公開前チェックや実機検証の側で担保する。この線を引いておくと、レビュアーの推測(「これは Windows のバグでは」)を検証もせずに本文へ入れずに済みます。
プロンプトの「見ないもの」は、毎回書き直します。 対象が変われば、機械が保証している範囲も変わる。前回のプロンプトをコピーして topic だけ差し替えると、検証していない範囲を「検証済み」と宣言してしまいます。保存してある過去のプロンプトは、踏襲する雛形として読み、宣言部分だけは対象ごとに書き直しています。