TL;DR
- AI コードレビューの再現性は、モデルの賢さではなく、モデルの外側に置いた決定的な構造で決まります。決定的な構造とは、対象選定・粒度・位置決め・検証層の 4 つです。
- この見立てには、ちょうど一次ソースになる実装があります。Alibaba の Open Code Review(CLI は
ocr)です。README は汎用エージェントの失敗をincomplete coverage/position drift/unstable qualityの 3 つとして名指しし、その原因を 「a purely language-driven architecture lacks hard constraints on the review process」 と書いています(公式値/ 固定 SHAa694be56/ 2026-09-16 取得)。 - README の言葉だけでは信じるに足りないので、同じ SHA のツリーを数えました。Go ファイルは 339 本、対して LLM へ渡すプロンプトテンプレートは 12 ファイルです(
社内データ/ 筆者による計測)。対象選定・グルーピング・位置再決定はinternal/agent/selection.go・internal/agent/grouping.go・internal/diff/relocation.goというコードのファイル名として存在します。自然言語の指示ではありません。 - そして本題はここからです。構造は「入れた」だけでは効きません。 壊した入力を通す検証をしない限り、効いていないまま緑になります。
- 裏づけは自分のリポジトリにあります。失敗台帳の直近区間 L-0069〜L-0080(実在 10 行) は、10 行とも「ガードや規約が存在していたのに効かなかった」型でした(
社内データ/ca3b9182f7e1ff9ed0b0310e2f0d9cdb29b61a64/ 2026-09-16 実測)。内訳は 充足不能な契約 3・未実行 3・失効根拠 2・転記ドリフト 1・部分測定 1。「構造が無かった」行は 1 行もありません。 - だから本シリーズの結論は「良いツールを入れよう」ではありません。構造を入れたあと、その構造を壊す入力を自分で作って通すところまでが一組、です。
- この記事は**シリーズ 1 本目(起点)**です。個別ツールの採否は勧めません。扱うのは設計の考え方だけです。
はじめに:同じ diff を 2 回レビューさせると、違うものが返ってくる
AI にコードレビューをやらせたことがある人なら、たぶん一度は次の状況に遭遇しています。
朝に走らせたときは 12 件指摘してきたのに、昼にもう一度同じ diff を投げたら 5 件しか返ってこない。しかも前回の 12 件に含まれていた本命が、今回は入っていない。プロンプトは変えていない。モデルも変えていない。
このとき最初に出る仮説は「モデルの調子が悪い」です。そして大抵、次の一手はプロンプトを厚くすることになります。「すべてのファイルを漏れなく見よ」「行番号を正確に」と書き足す。
この記事の主張は、その一手が効きにくい理由についてです。
レビューの再現性が崩れるとき、壊れているのはモデルではなく、モデルを囲む構造である。
言い換えると、「漏れなく見よ」はお願いであって制約ではない、ということです。お願いは確率的に守られます。制約は決定的に守られます。両者を混ぜたまま「AI レビューの品質」を語ると、改善の打ち手が全部プロンプトに寄ってしまいます。
この記事で扱う範囲を先に宣言しておきます。
| 扱うこと | 扱わないこと |
|---|---|
| レビューの再現性を決める 4 つの構造(対象選定・粒度・位置決め・検証層) | 特定ツールの採用推奨 |
| その構造が実装としてどう現れるか(一次ソースの読み方) | CLI のフラグ仕様・設定リファレンス |
| 構造を入れたあとに何が残るか(自リポジトリの失敗記録) | AI レビュー結果の精度比較実験 |
なお、AI レビューを運用フローのどこに置くかはAIレビューのワークフロー設計で、レビューの観点をスキルとして版管理する話はRiver Reviewer のスキルレジストリで扱っています。ここで書くのは、その手前にある「そもそも何が再現性を決めているのか」です。
1. 再現性を壊す 3 つの症状と、その共通の原因
まず症状を言語化します。ここは自分の経験則ではなく、一次ソースの言葉を借ります。
Open Code Review の README には、汎用エージェント(README は Claude Code を名指ししています)でコードレビューをやったときに起きることが 3 つ挙げられています。以下は原文です(README / 2026-09-16 取得)。
- Incomplete coverage — On larger changesets, agents tend to "cut corners," selectively reviewing only some files and missing others.
- Position drift — Reported issues frequently don't match the actual code location, with line numbers or file references drifting off target.
- Unstable quality — Natural-language-driven Skills are hard to debug, and review quality fluctuates significantly with minor prompt variations.
これは Alibaba 自身が公開している見立てであって、第三者の検証結果ではありません。そこは割り引いて読む必要があります。ただし症状の記述としては、実感と一致する人が多いはずです。日本語にすると「全部見てくれない」「指摘の場所がずれる」「昨日と今日で結果が違う」です。
そして README は原因を 1 文で書いています。
The root cause: a purely language-driven architecture lacks hard constraints on the review process.
hard constraints(硬い制約)という語が、この記事の中心です。
ここで大事なのは、3 つの症状が別々の問題に見えて、同じ場所に原因があることです。整理するとこうなります。
| 症状 | 実際に欠けているもの | それは誰の仕事か |
|---|---|---|
| 全部見てくれない | 対象選定が決まっていない(どのファイルを見るかをモデルが決めている) | 決定的な処理 |
| 指摘の場所がずれる | 位置決めが言語出力に委ねられている(行番号をモデルが言い当てている) | 決定的な処理 |
| 昨日と今日で違う | 粒度が不定(1 回のレビュー単位に何が入るかが揺れる) | 決定的な処理 |
3 つとも、本来モデルにやらせる必要がない仕事をモデルにやらせているという形をしています。ファイル一覧を作るのも、行番号を diff にマップし直すのも、変更を束ねるのも、コードで書けば毎回同じ答えが出る処理です。毎回同じ答えが出る処理を確率的な機構に投げたら、答えは毎回ぶれます。当たり前のことなのですが、自然言語のプロンプトを書いていると境界が見えなくなります。
2. 「構造に置く」とは具体的にどういう形をしているか
では構造に置くとは何か。抽象論で終わらせないために、実装を読みます。
Open Code Review は Go 製の CLI で、Apache-2.0、リポジトリ作成は 2026-05-18、star は約 2.9 万(公式値 / GitHub API / 2026-09-16 取得)。star 数を概数で書いているのは意図的で、この値は執筆中にも動いたからです(企画時 29,248 → 執筆時 29,461 → 独立レビュー時 29,472。同じ日の中の変化です)。配布は npm で、npm install -g @alibaba-group/open-code-review を実行すると ocr コマンドが入ります。前提は Git >= 2.41 です。最新リリースは v1.12.3(2026-09-16T00:34:22Z)。
ただしバージョンは今日も動いています。 日次で push されているので、CLI のフラグ仕様に依存した話はすぐ古くなります。以降で見るのはファイル構成とその意味だけにします。
2-1. まずツリーを数える
README の主張が本当に実装に現れているかは、ファイル名を数えれば見当がつきます。以下はすべて固定 SHA a694be568d9b9a935b2ba11a867d5a91d7ffd833 のツリーに対する実測です(2026-09-16)。
# 固定 SHA のツリーを取得して数える(gh CLI)
gh api "repos/alibaba/open-code-review/git/trees/a694be568d9b9a935b2ba11a867d5a91d7ffd833?recursive=1" \
--jq '.tree[].path' > tree.txt
grep -cE '\.go$' tree.txt # 339
grep -E '\.go$' tree.txt | grep -vcE '_test\.go$' # 124
grep -cE '_test\.go$' tree.txt # 215
grep -cE '^internal/config/template/prompts/.*\.md$' tree.txt # 12
| 数えたもの | 件数 |
|---|---|
| Go ファイル(合計) | 339 |
| うち非テスト | 124 |
うちテスト(_test.go) | 215 |
LLM へ渡すプロンプトテンプレート(.md) | 12 |
読み方は 2 つあります。
- プロンプトは 12 ファイルしかない。 残りはコードです。「レビューの振る舞いをプロンプトで作る」という構成ではありません。
- テストファイルのほうが非テストより多い(215 対 124)。決定的な層は、決定的であることをテストで押さえられます。これは後述の第 4 節と直結します。
2-2. 4 つの構造が、4 つのファイル名として存在する
README の「Core Design: Deterministic Engineering × Agent Hybrid」節は、決定的に扱うべき対象を列挙しています。要旨は次の 4 つです(原文は Precise file selection / Smart file bundling / Fine-grained rule matching / External positioning and reflection modules)。
重要なのは、これらが散文の理念ではなくパッケージとファイルとして実在していることです。同じ SHA のツリーから拾うと、対応がつきます。
ただし表を出す前に 2 点断っておきます。この対応づけは筆者の読み取りであり、README がパスを名指ししているわけではありません。 また README の 4 項目と、この記事が掲げる 4 構造(対象選定・粒度・位置決め・検証層)は同じものではありません。README の Fine-grained rule matching(ルールのマッチング)はこの記事の射程外なので表から外し、代わりに記事側の 4 つ目にあたる「検証層」の行を置いています。
| この記事の 4 構造 | README の対応する語(原文) | 実在するパス(固定 SHA) | 何を決めているか |
|---|---|---|---|
| 対象選定 | Precise file selection | internal/agent/selection.go | どのファイルをレビュー対象にするか/何を除外するか |
| 粒度 | Smart file bundling | internal/agent/grouping.go | 1 単位のレビューに何をまとめるか |
| 位置決め | External positioning ... modules | internal/diff/relocation.go | 指摘を diff 上のどこに置き直すか |
| 検証層 | (README に対応語なし) | internal/agent/coverage_test.go | 対象の取りこぼしが起きないことをテストで押さえる |
internal/diff/ には relocation.go とその隣に relocation_test.go、さらに relocate_across_files_test.go があります。位置決めは「モデルに正確な行番号を出させる」問題ではなく、「出てきた指摘を diff にマップし直す」問題として扱われているということです。後者ならテストが書けます。前者は書けません。
この差が、たぶんこの記事で一番持ち帰る価値のある区別です。
再現性を上げたいなら、モデルの出力の正しさを祈る形から、モデルの出力を決定的な処理で受け直す形へ、問題の置き場所を変える。
2-3. 「決定的 = LLM を使わない」ではない
ここで誤解しやすい点を先に潰しておきます。
同じツリーのプロンプト 12 ファイルの中には、grouping_task_system.md と re_location_task_system.md が含まれています。つまりグルーピングにも位置再決定にも LLM 側のタスクが存在します。
だから「決定的な構造」は「LLM を排除すること」ではありません。正確にはこうです。
- モデルが提案する
- コードが受け取り方・束ね方・置き直し方を決める
- テストがその受け取り方が壊れていないことを押さえる
README 自身も「combine deterministic engineering with an agent, each handling what it does best」と書いており、置き換えではなく分業として説明しています。エージェント側に残しているのは dynamic decisions and dynamic context retrieval、つまり動的な判断と文脈の取りに行き方です。
2-4. 数値の扱いについて一言
README には自社ベンチマーク(AACR-Bench)に基づく比較が載っています。Claude Code と同一モデルで比較して Precision と F1 が高く、トークンは約 1/9、ただし Recall は低い、という趣旨です。
これは Alibaba 自身が公開しているデータセットと公開値です(AACR-Bench)。第三者による再現結果ではないので、この記事では性能の優劣の根拠としては使いません。使うのは 1 点だけで、「Recall が低いのは precision over noise という意図的なトレードオフだ」と README 自身が明記していることです。つまりこの設計は「全部拾う」ことを目標にしていない。構造の話をするときに、その構造が何を最適化しているかは押さえておく必要があります。
3. 構造を入れただけでは効かない — 自分のリポジトリの記録から
さて、ここまでは「構造に置け」という話でした。ここからが本題です。
構造は、入れた時点では効いていません。 効いているかどうかは別の行為で確かめる必要があります。そしてこれは精神論ではなく、うちのリポジトリに記録が残っています。
このブログのリポジトリには spec/article_failure_ledger.md という失敗台帳があります。AI(と筆者)が書いたガードやルールがすり抜けた事象を、1 件 1 行で escape_mode(なぜすり抜けたか)付きで分類したものです。commit ca3b918 時点で 72 行あります(同じ数え方は直後のコマンドで再現できます)。
直近の区間 L-0069〜L-0080 を見ます。この採番区間に実在するのは 10 行です(L-0073 / L-0074 は並列作業で予約したまま未使用)。分類の内訳はこうでした。
| escape_mode | 意味 | 件数 |
|---|---|---|
| E5 | 充足不能な契約 — 構造的に満たせない要求を契約に書いた | 3 |
| E8 | 未実行 — ガードは正しく存在し発火可能だが、手順として実行されなかった | 3 |
| E7 | 失効根拠 — 対象が変わった後の古い検証結果を、現在の判断根拠に使った | 2 |
| E6 | 転記ドリフト — 同じ契約が複数ファイルに写され、片方だけ更新された | 1 |
| E4 | 部分測定の全体化 — 一部だけ測って全体の結論として報告した | 1 |
| 合計 | 10 |
# 台帳の総行数と、直近区間の escape_mode 内訳(ref: ca3b918)
grep -cE '^\| L-[0-9]{4} \|' spec/article_failure_ledger.md
# uniq はロケール照合で別の文字列を同一視することがあるので LC_ALL=C を付ける
awk -F'|' '/^\| L-00(69|7[0-9]|80) \|/{print $5}' spec/article_failure_ledger.md | LC_ALL=C sort | LC_ALL=C uniq -c
この表の読み方です。10 行とも「構造が無かった」話ではありません。 構造はあったのです。契約は書かれ、ガードは実装され、CI に載っていました。それでもすり抜けた。E4(部分測定の全体化)に分類した 1 件も、内実は「ID 衝突ガードは存在したが、検出窓が並行 open な PR の head を含んでいなかったので両方を緑にした」というもので、構造の不在ではなく構造の効かなさです。
とくに効いている 3 分類を具体化します。
E5(充足不能な契約)が 3 件。 これは「満たすことが構造的に不可能な要求」を契約に書いてしまう型です。典型は、.gitignore 配下の成果物を証拠として要求するルールでした。書き手は正直に従えないので、回避が常態化します。ガードは緑のままです。契約を書くときは「これは充足可能か」を先に確かめる必要がある、という教訓がここから来ています。
E8(未実行)が 3 件。 ガードは正しい。発火もする。ただ手順として走らせなかった。さらに厄介な変種として、同じ穴を塞ぐ対策が同一リポジトリに merge 済みで存在したのに、同じ穴を抱えた別のガードへ 5 日間持ち込まれなかったという記録(L-0069)があります。対策の「横展開」は、放っておくと起きません。
E7(失効根拠)が 2 件。 これがいちばん見つけにくい型です。ある時点で測って「確認済み」と書いた値が、後続の変更で偽になる。書いた本人にも読んだ人にも、失効した瞬間が見えません。直近の実例では、前の記事に載せた行数が、後からマージされた 2 本の PR と合成された結果、公開時点で既に偽になっていました。
この最後の型への対策として、うちでは gate.md の実測値行に 40 桁 commit SHA の併記を強制するガードを入れました(scripts/check_measured_value_ref.py、2026-09-16 merge)。面白いのはこのガードが数値の正しさを検証しないことです。保証するのは「後から失効を検出できる形になっていること」だけ。正しさを毎回検算するのはコストが見合わないので、失効可能性を可視化するところで止めているわけです。
規模感を添えておきます。commit ca3b918 時点で、そこまでに merge された PR のうち 2026-09-15〜16 の 2 日間に入ったものは 12 本で、その大半がこの手の「ガードを直すガード」でした。
この「commit で母集合を固定する」書き方自体が、いま説明した E7 への対策です。初版はこの文を「この 2 日間に merge された PR」という時間で動くスコープで書いていたため、この記事を含むシリーズ 3 本が同じ 2 日間に後から merge された時点で偽になりました——E7 を解説している段落の直後で、筆者自身が E7 を踏んだわけです。数値だけ直しても次の merge でまた失効するので、母集合の側を ref で固定しました(内訳・再測手順・欠番の根拠は gate.md の追補節に記録してあります)。
ここが前節のガードの守備範囲の外でもあります。check_measured_value_ref.py はこの失効した行に対して緑のままでした——ref が指すのは測定時点であって、主張のスコープではないからです。
構造を入れる作業と、構造が効いていることを確かめる作業は、別の作業です。 前者だけやると、緑のダッシュボードと素通りする穴が同時に手に入ります。
関連して、ガードが発火しない典型パターンはガードが発火しないパターンに、ガードを直す PR が別の穴を開ける現象はガード修正PRが穴を再生産するにまとめてあります。
4. 効いているかどうかは、壊した入力を通して初めて分かる
ではどう確かめるか。うちで定型化したのは 1 つだけです。spec/guard_self_check.md の R-1、二面検証です。原文はこうです。
対象ガードごとに、以下の両方を検証すること。
- 正常な入力では通る(exit 0 /
PASS)- 本来検出すべき違反を含む入力では落ちる(exit 非 0 /
BLOCKED)
1 だけをやるのが、いちばんよくある失敗です。「テストを書いた、緑になった、よし」。しかしそれは検査が何も検出していなくても同じ結果です。検査対象が 0 件でも緑、検査が到達不能な分岐に書かれていても緑、判定結果が呼び出し側に伝播していなくても緑。台帳の E1〜E3 はまさにその 3 型です。
AI コードレビューに翻訳すると、こうなります。
| 構造 | 1. 正常系の確認 | 2. 壊した入力の確認(こちらが本体) |
|---|---|---|
| 対象選定 | 変更 5 ファイルで 5 ファイル分の指摘が返る | 意図的に除外設定を壊し、対象外になるはずのファイルが混ざったら落ちるか |
| 粒度 | 通常サイズの diff でレビューが完走する | 1,000 ファイルの changeset を投げて、黙って一部だけ見る挙動に退行しないか |
| 位置決め | 指摘の行番号が diff と一致する | **行がずれる diff(前後に同一行が並ぶ等)**を作り、位置が外れたことを検出できるか |
| 検証層 | CI が緑になる | 既知の欠陥を仕込んだ PR を通し、赤になるか |
右の列をやらない限り、左の列は「動いている証拠」になりません。ここは AI かどうかとは無関係で、テスト設計の古典的な話です。
念のため、この記事が根拠から言えることと、言えないことを分けておきます。台帳 10 行が示すのは「正常系の確認だけでは、検査に検出能力があることを示せない」という限定された事実です。そこから「壊した入力を通す検証をしない限り、効いていないまま緑になる」と一般化するのは、台帳全体(第 3 節で ref 固定した 72 行)の分類のうち E1〜E3(発火不能・判定の非伝播・空振り合格)がいずれも左の列では検出できない型であることに基づく推論です。10 行を数えただけで証明された命題ではありません。それでも実務の指針としては、この形で運用する価値があると判断しています。ただし AI レビューでは出力が毎回違うので、左の列だけを見て「今日は通った」を積み上げる誘惑が強い。だから明文化しておく価値があります。
もうひとつ、この 2 日間で学んだ実務的な追加があります。検算は「別の関数に書いたか」ではなく「別の材料から導いたか」で決まる、というものです。同じ正規表現・同じ入力から 2 回計算しても、両側が同時に傾くので検算になりません。独立した測定をどう組むかは独立測定によるクロスチェックに分けて書いてあります。
まとめ
ここまでが起点です。主張をもう一度置きます。
AI コードレビューの再現性は、モデルの賢さではなく、モデルの外側に置いた決定的な構造で決まる。そしてその構造は「入れた」だけでは効かず、壊した入力を通す二面検証をしない限り、効いていないまま緑になる。
このシリーズで扱うこと
この記事で意図的に踏み込まなかった論点が 2 つあります。どちらも 1 本分の重さがあるので、後続に分けました。2 本とも公開済みです。
- 2 本目: AIレビューで絞ったツール集合は閉じているか。 「エージェントに渡すツールを絞れば安定する」は広く言われますが、絞ったあとに何が残るのかはあまり語られません。Open Code Review の組み込みツール定義(
internal/tool/definitions.go)には、組み込みの一覧と並んで、動的に発見されたツールを表す仕組みと、組み込み名との衝突だけを禁止する予約語判定があります。絞った集合の外側に口が開いているという構造をどう考えるかを扱います。 - 3 本目: AIレビューの導入単位は段取りか本体か。 Open Code Review には、自分で LLM を持たずに「対象選定とルール解決だけをやって、レビュー本体は呼び出し側のエージェントにやらせる」モードがあります。責務をどこで切るかの話で、Agents APIで外注できる責務の境界と隣接します。
そして、この記事が扱わないと決めたことも書いておきます。
- 「Open Code Review を採用すべきか」には答えません。筆者はこのツールを本番運用していません。読んだのは固定 SHA のソースと README だけです。
- 権限設計・被害範囲の話は扱いません。そちらはAIエージェントの権限は影響範囲で設計するにあります。
明日からやれること
抽象論で終わらせないための最小の一手を 3 つ置きます。
- いま使っている AI レビューについて、「対象選定を誰がやっているか」を 1 行で書く。 モデルが決めているなら、そこが再現性の穴です。
- 壊した入力を 1 つだけ作って通す。 既知の欠陥を 1 つ仕込んだ PR を出し、赤になるか見る。緑なら、その検査は今日まで何も検出していなかった可能性があります。
- すり抜けを 1 行で記録する場所を作る。 うちは
escape_mode付きの台帳にしました。分類しないと、同じ型を何度も踏みます。数十行たまってから分かったことがいくつもあります。
FAQ
Q. プロンプトを工夫しても再現性は上がりませんか?
上がる範囲はあります。ただし上限があります。プロンプトで改善できるのは「モデルが判断すべきこと」の質で、「毎回同じ答えが出るべき処理」の安定性ではありません。対象選定や行番号のマッピングは後者なので、プロンプトを厚くするほど改善幅が飽和します。どちらの問題を解いているのかを先に切り分けてください。
Q. 決定的な構造を自分で全部実装する必要がありますか?
必要はありません。この記事の主張は実装方法ではなく、どこに境界を引くかです。既存のツールを使うにせよ自前で書くにせよ、「対象選定・粒度・位置決め・検証層がどこに実装されているか」を指させることが条件です。指させないなら、それは構造ではなくお願いです。
Q. Open Code Review を導入すべきですか?
この記事はその判断をしません。筆者は本番運用しておらず、読んだのは固定 SHA のソースと README だけです。参照した理由は、設計思想が README に明文化されていて、かつ対応する実装をファイル名で確認できるからです。採否は各自の環境で判断してください。
Q. 二面検証はどのくらいの頻度でやるものですか?
うちでは「ガードを追加・変更した PR ごと」に必須としています。定期実行ではありません。理由は、構造を入れた直後が「その構造に検出能力があるかを一度も確かめていない」状態だからです。台帳の直近 10 行(L-0069〜L-0080 の実在 10 行)は、10 行とも「既存の構造が効かなくなった/最初から効いていなかった」型でした(第 4 節の表。社内データ / ca3b9182f7e1ff9ed0b0310e2f0d9cdb29b61a64 / 2026-09-16 実測)。構造が無かった行は 1 行もありません。
Q. 失敗台帳は何を書けば機能しますか?
最低限、観測日 / すり抜けた理由の分類(escape_mode)/ 証拠の所在の 3 つです。分類が無いと集計できず、集計できないと「同じ型が閾値を超えた」という判定ができません。逆に言うと、分類さえあれば対策の優先順位は自動的に決まります。
References
- Open Code Review リポジトリ(Alibaba / Apache-2.0): https://github.com/alibaba/open-code-review
- README(固定 SHA
a694be56、本記事の引用元): https://github.com/alibaba/open-code-review/blob/a694be568d9b9a935b2ba11a867d5a91d7ffd833/README.md - 組み込みツール定義
internal/tool/definitions.go(同 SHA): https://github.com/alibaba/open-code-review/blob/a694be568d9b9a935b2ba11a867d5a91d7ffd833/internal/tool/definitions.go - 位置決めの実装
internal/diff/(同 SHA): https://github.com/alibaba/open-code-review/tree/a694be568d9b9a935b2ba11a867d5a91d7ffd833/internal/diff - AACR-Bench データセット(Alibaba 公開): https://huggingface.co/datasets/Alibaba-Aone/aacr-bench
- npm パッケージ
@alibaba-group/open-code-review: https://www.npmjs.com/package/@alibaba-group/open-code-review - Git 公式サイト(README が前提として挙げる Git >= 2.41 の出所): https://git-scm.com/
- Open Code Review 公式ドキュメント(README がリンクしている一次情報): https://open-codereview.ai/docs
