対象読者: AIにコードを書かせていて、レビューでどの失敗パターンを重点的に見るべきか決めたい人
コードをAIに書かせ、外部のAIにレビューさせる運用を、性質の違う4つのプロジェクトで続けてきました。溜まったレビュー記録のうち、コードに対する指摘は95件。誤検知として棄却した1件を除く94件を、失敗パターンで全部分類しました。採否記録が揃っている対象で指摘の的中率を測った結果はAIレビュアーの指摘はどれだけ当たるのかに、レビューの回し方は2エージェント相互レビューの実運用に書きました。本記事が扱うのは、指摘の中身の分布です。
分類した記録
対象は4プロジェクト。いずれもコードの大部分をAIエージェントに書かせています。
| 対象 | 言語 | レビューの体制 | 指摘数 |
|---|---|---|---|
| 歩数・位置情報から1日のタイムラインを組み立てるAndroidアプリ | Kotlin | 異ベンダー2モデル | 34 |
| 画面キャプチャをOCRで読み取るWindowsのGUIツール | Python | 領域別の並列レビュー+別モデルのクロスレビュー(時期の違う2回) | 34 |
| 動画から字幕を生成・編集するデスクトップツール | Python | 単一モデル | 10 |
| 技術記事を静的サイトとしてビルドする実装 | TypeScript | 異ベンダー2モデル+採否記録 | 17 |
| 計 | 95 |
件数は概算で、数え方もプロジェクトで揃っていません。重複を統合した指摘グループ単位で数えられているのは、サイト実装の17件だけ。AndroidアプリとGUIツールの34件ずつはレビュー記録の行をそのまま数えたもので、同じ穴への複数指摘が残っています(後述の削除機能がその例)。字幕ツールの10件は解決済みリストの1項目を1件と数えたもので、逆に1項目へ複数の症状が畳まれている。軸ごとの件数は、この粒度差の影響を受けます。
もう1つ先に断っておくと、これは「AIコードにあった欠陥」の集計ではなく「AIレビュアーが指摘した内容」の分類です。GUIツールとサイト実装の指摘は採否まで判定してあり、大半は修正済み(一部は見送り・未対応のまま残っています)。字幕ツールの10件は解決済みという記録が残っているだけで、採否の判定記録はありません。Androidアプリの34件は採否未判定のまま。冒頭に挙げた精度の記事が的中率を測ったのは、この4つのうちサイト実装が属する1プロジェクトの採否記録で、ここで数える95件の大半はその測定の外にあります。だからここで数えるのは、何が指摘されるかだけです。
前処理:コードへの指摘だけを残す
レビュー記録の全体は160件あり、そのまま分類すると軸が立ちません。技術記事の本文への指摘(バージョン境界の事実誤りなど)36件と、サイトのデザイン仕様への指摘(コントラスト、状態定義など)29件が混ざっていたからです。レビュアーが直させようとしている対象物が違えば、失敗の型も違う。この65件を除いた95件がコードへの指摘で、うち1件(GUIツールへの指摘)はレビュアー側の誤検知(挙動が変わらない変更の提案)として棄却済みのため、分類の母数は94件です。
コードと文章を分けるこの前処理は下ごしらえのつもりでしたが、結果的に1つの発見につながった。過大申告の節で戻ります。
仮説の6軸と、畳んだ結果
分類の前に、これまで印象に残っていた失敗から6つの軸を仮説として立てました。fail-open/サイレント劣化、要件未実装、リソース寿命、エッジケース黙殺、検証済みの過大申告、非代表データ過信の6つです。レビュー記録に付いていた生のラベルは60種類を超えていたので、まずこの6軸へ畳み、入らないものはデータの側から軸を追加しました。
| 軸 | 指摘数 | 由来 | 代表的な指摘 |
|---|---|---|---|
| fail-open/サイレント劣化 | 13 | 仮説 | zip のペアずれ、失敗ファイルの再処理漏れ、API戻り値の無視 |
| 性能・メインスレッド | 8 | 追加 | メインスレッドI/O、毎回のフルリロード、O(n²) |
| 保守性 | 8 | 追加 | dead code、関数の重複、誤解を招く書き方 |
| エッジケース黙殺 | 7 | 仮説 | データが空の日、DST、NaN、スペース入りタグ |
| リソース寿命 | 7 | 仮説 | コールバック未解除、tempファイルのリーク、壊れたキャッシュの放置 |
| 実行環境・互換性 | 7 | 追加 | BOM、cp932、DLL同梱漏れ、権限の未宣言 |
| 例外境界・契約 | 6 | 追加 | 捕捉範囲が狭すぎる/広すぎる、型契約の違反 |
| テスト欠落 | 6 | 追加 | 境界値テストなし、異常系テストなし |
| UI状態の取り残し | 6 | 追加 | 完了表示が消えない、実行中状態がない |
| 並行性・非決定性 | 5 | 追加 | デッドロック、race、set順序依存 |
| データ保全・原子性 | 5 | 追加 | migrationなし、非アトミック書き込み |
| セキュリティ | 4 | 追加 | 埋め込みJSONへの注入、CSVインジェクション |
| 要件未実装・仕様ずれ | 3 | 仮説 | disabled UIのままのMVP要件 |
| 過大申告 | 2 | 仮説 | 「新着順」が実はslug順、検証フラグの無検証信頼 |
| 非代表データ過信 | 1 | 仮説 | 特定解像度前提の座標決め打ち |
| その他 | 6 | — | アクセシビリティ、時刻ソースの不一致など |
| 計 | 94 |
仮説の6軸に入ったのは33件、約3分の1でした。最多はfail-open/サイレント劣化で仮説どおり。エッジケース黙殺とリソース寿命が各7件で続き、過大申告と非代表データ過信は合わせて3件しか出ませんでした。この偏り方は、立てる前には予想していなかったものです。
最頻出は「黙って間違う」
単独最多は、fail-open/サイレント劣化の13件。中身はこういう指摘です。
- 2本のリストを
zipで束ねる処理で、片方に欠けが出ると以降の要素が黙ってずれたままペアを組み、余った分は捨てられる - 監視フォルダの「処理済みリスト」へファイル名を処理の前に登録していて、処理に失敗したファイルが二度と再処理されない
- 手動の「いますぐ同期」がWorkManagerの
ExistingWorkPolicy.KEEPで既存ジョブに吸収される。画面には再取得中と出ているのに、実際の同期が次の周期まで走らないことがある - Win32 APIの戻り値を確認せず、ウィンドウの保存位置と実際の位置がずれても検知できない
- 多言語対応で同じslugの記事を置くとコレクションのidが衝突し、警告ログだけ残して後勝ちになり、片方のページが消える
共通の形は「失敗を検知する材料はそこにあるのに、既定値・継続・無言スキップで先へ進む」です。例外を投げれば1行で済む場所が、投げない方向に書かれている。
エッジケース黙殺の7件も同族です。代表は、データが1件もない日でも欠測の枠から「就寝」「記録なし」のタイムラインを組み立てて表示するという指摘で、欠測が実データの顔をして出てきます。両方を合わせて94件中20件、指摘のおよそ5分の1が「黙って間違う」系でした。
この型は、正常系のテストが緑のまま残ります。異常が既定値や継続に化けるので、異常系の入力を明示的に流すテストがない限りアサーションに届きません。例外もエラーログも出ないぶん、気づく経路がレビューか実害かの二択になる。機械検証やテストで範囲を絞った後のレビューに何を指名するかと聞かれたら、このデータからは真っ先にこの軸を挙げます。
過大申告は、コードにはほぼ出ない
仮説6軸のうち、検証済みの過大申告は2件、非代表データ過信は1件で、ほぼ空振りでした。コード側に出た過大申告は、ソートの「新着順」ラベルが日付メタデータ欠落時のフォールバックで実際にはslugのアルファベット順だったものと、検証レポートのverifiedフラグを終了コードと突き合わせずに信頼していたものの2件。どちらも「看板が実装より広い」形をしています。非代表データ過信も、画面の読み取り領域が特定解像度・特定アスペクト比の決め打ちなのにコメントは任意対応と読める、という1件だけでした。
では前処理で除外した文章側に多いのかというと、率はほとんど変わりません。デザイン仕様への指摘29件のうち過大申告は2件で、「正しさは証明済み」類の文言が機械検証の実際の範囲(固定した環境での再現と修正の確認)を超えた約束としてHighで指摘され、書き直した次の版にも同じ言い回しが残りました。独立した事例としては実質1つです。技術記事の本文への指摘36件には、過大申告と分類された指摘がありません。示した出典では裏の取れない「確証あり」をレビュアー自身が申告してくる件は精度の記事に書いたとおりですが、あの2件は検証したら主張自体は正しく、問題は当否ではなく申告文から当否を判別できないことでした。
件数だけ見れば、過大申告はコードにも文章にも少ない失敗です。目を引いたのは数より出た場所のほうで、ソートのラベル、verifiedフラグ、「証明済み」の宣言、レビュアーの「確証あり」欄——どれも、成果物の品質を自称する層に出ています。標本が小さいので法則とは言いませんが、AIの書いたロジックと同じかそれ以上に、AIが自分の仕事について語る箇所を疑う。この分類が観点として足したのは、コードの軸よりむしろこれでした。
要件未実装は、動くデモをすり抜ける
MVP要件に入っているデータ削除機能が、設定画面では「近日対応」のdisabledボタンになっていて、データ層にも削除APIがない——そういう指摘がHighで挙がりました。プライバシー要件なのに、ユーザーが端末内の位置履歴を消せない状態です。同じ穴を、もう1体のレビュアーは「削除機能用のDAOが未実装」というLowとして挙げていました。実装の欠けとして見ればLow、要件表と突き合わせればHigh。同じ事実でも、参照する文書で重大度が変わります。
表の指摘数は3件ですが、実体は2つです。いま見た削除機能への重大度違いの2件と、仕様が求める判定条件と実装のずれが1件。数はどうであれ、この型は動くデモをすり抜けます。disabledボタンは触っても「未対応の印」にしか見えず、欠けているのはコードではなく要件との対応関係だからです。要件リストとコードを突き合わせる読み方をレビュアーに明示的に頼まない限り、出てこない指摘だと考えています。
動き続けるプログラムの、資源と状態
リソース寿命7件、並行性・非決定性5件、データ保全・原子性5件、UI状態の取り残し6件。合計23件のこの一群は、全件がAndroidアプリとPythonのデスクトップツール2本、つまり常駐して動き続けるプログラムから出ています。ただし、標本のうち常駐しないのはサイトのビルド実装1本だけで、そちらにはウィンドウもUI状態も終了処理も構造上ほぼ存在しません。測った分布というより、標本の構成の裏返しとして読んでください。
- tkinterの
afterで自分を再スケジュールし続けるループを、ウィンドウを閉じるときにキャンセルしていない。破棄済みのrootへ発火してTclErrorになる - 位置取得にタイムアウトを付けたが、打ち切ったのはコルーチン側だけで、位置サービスへのリクエストは裏で走り続ける
- OCRエンジンのキャッシュがネイティブ層の例外で不正な状態に陥った後も破棄されず、以後その機能だけ失敗し続ける。同型のworker 3つのうち2つはキャッシュを破棄していて、3つ目だけ抜けていた
- 処理中にウィンドウを閉じると、走っているQThreadの破棄でクラッシュする
- メモの自動保存をviewModelScopeで起動していて、戻る操作でスコープごとキャンセルされ、書いた内容が保存前に消えうる
確保する側のコードは書けています。抜けるのは、失敗経路と終了経路で畳む側です。1リクエストで終わるコードなら畳み忘れはプロセス終了が回収してくれますが、常駐アプリでは閉じる操作・タイムアウト・例外のたびに回収の機会が来て、そのどれかが漏れる。データ保全の5件(Roomのmigration戦略なし、設定ファイルの非アトミック書き込みなど)も、プロセスの外に残る状態の畳み方という意味で同じ側の話です。
残り半分は、昔からあるレビュー観点
性能・メインスレッド8件、保守性8件、テスト欠落6件、例外境界・契約6件。ここに並ぶのは、GUIのメインスレッドで毎秒ディスクI/Oを回す、スクリーンショット1枚ごとに全辞書をディスクから読み直す、TypeError しか捕捉していないため他の例外が生のまま上がる、境界値のテストがない、といった指摘です。実行環境・互換性の7件——CSVにBOMが付かずWindowsのExcelで文字化けする、コンソールのcp932で例外メッセージが化ける、配布ビルドにGPUのDLLが同梱されない、バックグラウンド読み取りの権限がmanifestに未宣言——は、「書いた環境の外」で初めて露見する型に収まります。
セキュリティを含めるとこの一群は39件、どの軸にも畳めなかった「その他」の6件を足せば45件で、母数の半分近くになります。並んでいるのは、コードレビューの観点表に昔から載っている項目です。前節の資源と状態の23件も、出どころは常駐型に偏っていても観点としては古典的なもの。書く前に印象から立てた6軸だけでは94件の3分の1しか拾えなかった、というのがこの分類のもう1つの答えでした。
この分布をレビューにどう返すか
レビュー依頼のプロンプトには「見るもの」を列挙する欄を設けてあるので(冒頭に挙げた回し方の記事で説明した形式)、そこへ書く観点をこの表から選べるようになりました。fail-open/サイレント劣化とエッジケース黙殺は、テストが緑のまま残る型なので最優先で指名する。要件未実装は、要件リストを渡して突き合わせを頼むという依頼の形に変える。過大申告はコードだけを対象にしたレビューでは取りこぼしやすいので、READMEや検証レポートといった「成果物についての主張」も対象に、別の1回として回すことにしています。