
AI生成コードが怖いのは、間違いが多いからではなく目立たないから
動いているコードを入れてよいか決める、12の観点と確認の順番
「動いてはいる。テストも通っている。でも、このまま入れていいのか分からない」
「差分を上から読んだけれど、どこも普通に見える」
「結局、全部を自分で書き直すくらい読まないと安心できない」
この迷い、よくわかります。
私も最初のうちは、AIが書いたコードを人のコードと同じ読み方でレビューしていました。
うまくいかない原因は、読む量が足りないことではありません。
疑う場所の見つけ方が、人のコード向けのままになっていることです。
先に結論
人のコードは、理解が浅い箇所ほど書き方が粗くなるので、粗さを手がかりに疑える
AIのコードは、分かっていない箇所も同じ滑らかさで書かれる。粗さを手がかりにできない
だから疑う場所は、見た目ではなくコードの外とつながる場所と失敗したときの経路で決める
頻出するのは次の5つの型です
存在しない参照
古い書き方
過剰な抽象
握りつぶした例外
実装をなぞるだけのテスト
全行を同じ深さで読む必要はありません。確かめる順番を決めれば、読む量はむしろ減ります
この記事は次のような方に向いています
AIが書いたコードを、そのまま入れてよいか毎回迷っている
レビューはしているのに、あとから不具合が見つかる
テストが通っていることを、どこまで信じてよいか分からない
チームのメンバーが出してくるAI生成のPRを、どう見ればいいか決めかねている
この記事でわかること
人のコードとAIのコードで、疑う場所が違う理由
頻出する5つの型と、その見つけ方
観点リスト12項目(疑う理由・例・確かめ方)
差分を受け取ってから判断するまでの確認手順
確認を省いてよい条件と、省いてはいけない場所
チームに渡す前の最終確認と、PR説明に書く3行
1. 人のコードは粗さで疑えるが、AIのコードは粗さが出ない
人が書いたコードをレビューするとき、私たちは無意識に「粗さ」を手がかりにしています。
変数名が急に雑になる。
コメントが途切れる。
似た処理なのに書き方が揃っていない。
こうした粗さは、たいてい書いた人の理解が浅かった場所に出ます。
だから粗い場所を重点的に読めば、問題のある箇所に当たりやすい。
長くレビューをしてきた人ほど、この嗅覚が育っています。
AIのコードには、この手がかりがありません。
よく知っている処理も、存在しない関数を呼んでいる処理も、同じ整った書き方で出てきます。
命名は揃っていて、コメントも丁寧で、エラー処理まで付いている。
間違いがあっても、見た目の上では周りと区別がつきません。
つまり、人のコード向けの嗅覚は、ここではほとんど働きません。
読む量を増やしても、どこを疑えばいいかが分からないまま時間だけが過ぎます。
代わりに使う手がかりは2つです。
外との接点: ライブラリ、API、設定、環境変数、パッケージ。コードの外に実物があり、実物と照合できる場所
失敗経路: 例外、空の値、タイムアウト、テスト。うまくいかなかったときにだけ通る場所
AIの間違いは、この2か所に集まります。
外との接点は、学習した知識と実物がずれやすい場所です。
失敗経路は、動かして確かめる機会が少ないので、間違いが残りやすい場所です。
2. 頻出する5つの型
2か所のうち、実際によく当たる型を5つ挙げます。
どれも「動いているのに間違っている」ので、手元で動かしただけでは気づけません。
型1: 存在しない参照
実在しない関数、オプション、設定キーを、もっともらしく呼んでいる型です。
いちばん厄介なのは、エラーにならずに黙って無視されるものです。
const res = await fetch(url, { timeout: 5000 });fetch には timeout というオプションがありません。
この行はエラーにならず、ただ無視されます。
ふだんは問題なく動き、外部のAPIが詰まった日にだけ、待ち続けて処理が止まります。
const res = await fetch(url, { signal: AbortSignal.timeout(5000) });タイムアウトを付けたいなら、こう書く必要があります。
型2: 古い書き方
学習した時点では正しかった書き方が、そのまま出てくる型です。
動くので気づきにくく、非推奨の警告が出るまで残ります。
from datetime import datetime
now = datetime.utcnow()Python 3.12 からは非推奨です。
タイムゾーンを持たない値が返るので、ほかの日時と比べたときにずれる原因にもなります。
型3: 過剰な抽象
1つしか実装がないのに、インターフェースやファクトリが用意されている型です。
「将来の拡張に備えて」の設計が、頼んでいないのに付いてきます。
間違いではありませんが、読む人の負担が増え、プロジェクトの作りからも浮きます。
型4: 握りつぶした例外
try で囲んで、失敗したら null や空配列を返す型です。
エラー処理があるように見えるので、レビューで安心してしまいます。
try {
return await fetchUser(id);
} catch (e) {
console.error(e);
return null;
}呼び出し側が null を想定していなければ、画面には何も出ず、ログにだけ痕跡が残ります。
障害が起きても、気づくのが遅れる書き方です。
型5: 実装をなぞるだけのテスト
実装と同じタイミングで、同じ前提から書かれたテストです。
実装が間違っていれば、テストも同じように間違えます。
テストが通っていることが、正しさの根拠になっていません。
expect(calcTotal(items)).toBe(
items.reduce((sum, i) => sum + i.price * i.qty, 0)
);期待値を、実装と同じ計算で作っています。
これでは、計算式そのものが間違っていても落ちません。
3. 差分を受け取ったら、まずこの5行を当てる
細かい観点に入る前に、持ち帰れる最小の形を置いておきます。
差分を開いたら、上から読み始める前にこの5つだけ確認します。
[ ] 新しく出てきた関数・オプション・設定キー・パッケージは、実在するか
[ ] 非推奨の書き方や、古いバージョン前提の書き方が混ざっていないか
[ ] 実装が1つしかない抽象(インターフェース・基底クラス)が増えていないか
[ ] catch の中で、値を返して処理を続けていないか
[ ] テストの期待値は、実装とは別の根拠から来ているかこの5行を当てるだけでも、差分のどこを重点的に読むかが決まります。
残りは、引っかかった箇所から読めば足ります。
4. 観点リスト12項目
ここからは、5つの型を含めた12の観点を、疑う理由と確かめ方まで書きます。
1章の2つの手がかりに沿って、4つのグループに分けています。
A. 外との接点(1〜3)
B. 失敗経路(4〜7)
C. 構造と範囲(8〜10)
D. 検証の根拠(11〜12)
全部が同じ重さではありません。
1・4・11 は事故の大きさに直結するので、ここだけは省かずに見てください。
A. 外との接点
観点1: 存在しない参照
疑う理由は、AIが「ありそうな名前」を作れてしまうことです。
よく使われるライブラリほど、似た名前のオプションが実在するので、区別がつきません。
特に注意するのは、渡しても黙って無視される場所です。
オブジェクトで渡すオプション(未知のキーを無視する作りが多い)
環境変数(未定義なら undefined になり、既定値で動いてしまう)
設定ファイルのキー(存在しないキーは読まれないだけ)
CSSのプロパティや、HTMLの属性
確かめ方は、実物と照合することです。
エディタの定義ジャンプで、宣言までたどれるか
型チェックを通したとき、オプションの型で弾かれるか
公式ドキュメントに、そのオプション名が載っているか
環境変数なら、.env.example や設定の定義に同じ名前があるか
型のある言語では、型チェックがかなりの部分を拾ってくれます。
ただしオプションの型が緩く定義されていると通ってしまうので、過信は禁物です。
「差分で初めて出てきた名前」を拾い出して、1つずつ定義ジャンプするのが確実です。
観点2: 古い書き方
AIは、学習した時点の知識で書きます。
非推奨になったAPIや、古いメジャーバージョンの書き方が、何事もなかったように出てきます。
見比べる相手は3つあります。
使っているライブラリのバージョンを確認し、その版のドキュメントと見比べる
実行時やビルド時の非推奨警告を、差分の前後で比べる
プロジェクト内の既存コードが同じ処理をどう書いているかを見る
3つ目がいちばん手軽です。
既存コードと書き方が違ったら、どちらが新しいのかを確かめるきっかけになります。
観点3: 新しく増えた依存
依存の追加には、2種類の危うさがあります。
1つは、頼んでいないのに依存が増えること。
もう1つは、パッケージ名そのものが実在しない、あるいは別物であることです。
AIが出しがちな実在しないパッケージ名を先回りして登録し、悪意のあるコードを仕込む攻撃も報告されています。
名前がもっともらしいほど、インストールしてしまいやすい。
依存の差分は、コードの差分より先に見ます。
増えたパッケージは、公式のリポジトリと公開元を確認する
ダウンロード数や最終更新日が極端に少ない・古いものは、使う前に止まる
そもそもその依存が必要か。既存の依存や標準機能で足りないか
B. 失敗経路
観点4: 握りつぶした例外
AIのコードで、いちばん見逃しやすい型です。
エラー処理が「ある」ので、レビューでは安心材料に見えてしまいます。
疑う理由は、AIが処理を最後まで通すことを優先しやすいからです。
失敗したときに止めるより、既定値を返して先へ進める書き方を選びがちです。
見つけ方は、差分の中の catch except rescue と、?? デフォルト値 || [] のような既定値の補完を全部拾うことです。
拾ったら、1つずつこう問います。
この失敗は、ここで吸収してよいものか
吸収した結果、呼び出し側は失敗に気づけるか
ログだけ残して、誰がそのログを見るのか吸収してよいのは、失敗しても利用者に影響がなく、あとから気づける仕組みがある場合だけです。
それ以外は、例外をそのまま上に投げるか、失敗を表す値を明示的に返すよう直してもらいます。
私も一度、外部APIの失敗時に空配列を返すコードを通してしまったことがあります。
一覧が空で表示されるだけなので、利用者からは「データがない」ようにしか見えません。
障害だと分かったのは、数日後に件数の集計が合わなくなってからでした。
観点5: 空・境界・想定外の入力
依頼文に書かれた例は、たいてい正しく処理できます。
その外側は、誰も確かめていません。
確かめる入力は、だいたい決まっています。
空文字、空配列、null / undefined
0件、1件、上限ちょうど、上限+1
全角、絵文字、前後の空白
日付の月末、うるう日、タイムゾーンの境目
全部を試す必要はありません。
その処理にとって「いちばんありそうな例外」を1つだけ選び、自分で入力して動かします。
それだけで、境界をまったく考えていないコードかどうかは分かります。
観点6: 非同期と順序
非同期の間違いは、テストでは出にくい種類の間違いです。
手元では処理が速いので、順番が崩れても結果が揃ってしまいます。
よく出るのは次の書き方です。
items.forEach(async (item) => {
await save(item);
});
console.log("保存完了");forEach は await を待たないので、保存が終わる前に「保存完了」が出ます。
for...of で書くか、Promise.all でまとめて待つ必要があります。
差分の中で async が付いた関数は、呼び出し元に await があるかを1つずつ見ます。
ループの中の async と、戻り値を使っていない非同期の呼び出しは、特に疑ってください。
観点7: 入力と秘密情報の扱い
動かすことを優先すると、安全側の書き方が省かれることがあります。
全部を網羅するのはセキュリティレビューの仕事ですが、最低限これだけは見ます。
外から来た値を、SQLやシェルのコマンドに文字列で埋め込んでいないか
APIキーやパスワードが、コードや設定ファイルに直接書かれていないか
ログに、トークン・個人情報・リクエスト全体を出していないか
権限の確認が、画面側だけで終わっていないか
どれか1つでも当てはまったら、その差分は観点8以降より先に直してもらいます。
C. 構造と範囲
観点8: 過剰な抽象
AIは「一般的に良いとされる設計」を持ち込みがちです。
一般論としては正しくても、実装が1つしかない抽象は、読む人の手間を増やすだけになります。
見分け方は単純です。
差分で新しく増えた次のものについて、実装や利用箇所がいくつあるかを数えます。
インターフェース
基底クラス
ファクトリ
設定による切り替え
1つなら、抽象を外して直接書いてもらう候補です。
観点9: 既存の仕組みの作り直し
AIは、プロジェクトの中を全部見てから書いているわけではありません。
日付の整形、APIの呼び出し、バリデーションといった処理を、既存のユーティリティがあるのに新しく書いてきます。
新しく増えた関数の名前や処理内容で、プロジェクト内を検索してみてください。
似たものが見つかったら、そちらを使うよう直してもらいます。
二重に持つと、片方だけ直されて挙動がずれる原因になります。
観点10: 頼んでいない変更
AIは親切心で、周辺も直してきます。
整形、リネーム、関係ない関数のリファクタリングが、依頼した変更に混ざります。
差分のファイル一覧を開き、依頼の範囲から外れたファイルがないかを見ます。
外れていたら、中身を読む前に「その変更は外してください」と返します。
混ざったまま読むと、本当に見るべき変更が埋もれます。
D. 検証の根拠
観点11: 実装をなぞるだけのテスト
テストが通っていることは、AIのコードを受け入れる最大の根拠になりがちです。
だからこそ、テスト自体が根拠として成り立っているかを疑います。
実装と一緒に書かれたテストには、次のような共犯関係がよく見られます。
期待値を、実装と同じ計算式で作っている
テスト対象の関数そのものをモックしている
失敗する入力のテストがなく、成功するケースだけが並んでいる
実装を修正したときに、テストの期待値も一緒に書き換えている
最後のものは、差分を見ると分かります。
実装とテストの期待値が同じコミットで変わっていたら、テストが実装に合わせて動いた可能性があります。
いちばん確実な確かめ方は、実装を1行壊して、テストが落ちるかを見ることです。
条件式を反転させる、戻り値を固定の値にする、1行コメントアウトする。
それでもテストが通るなら、そのテストはその行を守っていません。
手順は5章で詳しく書きます。
観点12: 説明とコードのずれ
疑う理由は、AIの説明文やコメントが、実際のコードより正確に見えることです。
「タイムアウトを設定しました」「入力を検証しています」と書かれていても、コードがその通りとは限りません。
観点1の fetch の例がまさにそうで、説明には「5秒でタイムアウト」と書かれていることがよくあります。
説明やコメントに書かれた主張を1つ選び、コードのどの行がそれを実現しているかを指でたどることです。
たどれない主張は、実装されていないものとして扱います。
5. 確認の手順 — 外形から中身へ、6ステップ
12の観点は、差分を受け取ってから判断するまでの順番に並べ替えて使います。
先に外形を見て、読む範囲を絞ってから中身に入るのがコツです。
1. 外形を見る(2分)
- 変更ファイルの一覧。依頼の範囲から外れたファイルはないか → 観点10
- 依存ファイルとロックファイルの差分 → 観点3
2. 新しく出てきた名前を拾う(5分)
- import、呼び出している関数、オプション、設定キー、環境変数
- 1つずつ定義ジャンプ、なければドキュメントで照合 → 観点1・2・9
3. 失敗経路を拾う(5分)
- catch / except / 既定値の補完 / async を全部拾う
- 吸収してよい失敗か、呼び出し元は気づけるかを問う → 観点4・6・7
4. テストを壊す(5分)
- 実装の重要な行を1〜2か所壊し、テストを走らせる → 観点11
5. 自分の入力で動かす(3分)
- その処理でいちばんありそうな例外入力を1つ試す → 観点5
6. 説明できるかを確かめる(数分)
- 説明文の主張を1つたどる → 観点12
- 自分で説明できない行が残っていないか → 観点8合計で20〜30分ほどです。
全行を精読するより短く、見るべき場所には確実に当たります。
ステップ4の具体的なやり方
テストを壊す手順は、慣れないと大げさに感じますが、やることは単純です。
1. テストが守っているはずの行を1つ選ぶ(条件分岐・計算・戻り値)
2. その行を壊す
- 条件を反転する(> を <= に)
- 戻り値を固定値にする(return 0)
- 行ごとコメントアウトする
3. テストを走らせる
4. 落ちたら元に戻す。通ったら、その行を守るテストが欠けている通ってしまったときは、AIに「この行を壊しても落ちるテストを追加してください」と頼みます。
理由も添えて返すと、期待値を実装から逆算しないテストが返ってきやすくなります。
calcTotal の中で、数量を掛ける処理を消してもテストが通りました。
期待値を実装と同じ計算で作っているのが原因だと思います。
期待値は、手で計算した具体的な数値で書いてください。
例: 単価100円×3個 + 単価250円×2個 = 8006. AIにレビューを手伝わせるなら、判定ではなく列挙を頼む
書いたAI自身に「このコードに問題はありますか」と聞いても、あまり役に立ちません。
同じ前提で書いたものを同じ前提で見直すので、同じところを見逃します。
「問題ありません」と返ってきても、それは根拠になりません。
役に立つのは、列挙を頼む使い方です。
5章の手順のうち、ステップ2と3の「拾う」作業を任せます。
この差分について、判定はせず、次の一覧だけを出してください。
1. 差分で新しく使われている外部の名前
(関数・オプション・設定キー・環境変数・パッケージ)
それぞれについて、定義元のファイルか公式ドキュメントのURL。
見つからないものは「未確認」と書く
2. 例外を捕まえている箇所と、既定値で補っている箇所
それぞれについて、失敗したときに呼び出し側へ何が返るか
3. await されていない非同期の呼び出し
4. テストの期待値のうち、実装と同じ計算で作られているもの
出力は表にしてください。列は「種類 / 箇所(file:line) / 内容 / 確認状況」一覧が返ってきたら、未確認の行と、既定値で補っている行から自分で確かめます。
判定を任せず、確かめる対象を拾わせるだけにすれば、見逃しの責任が曖昧になりません。
7. 確認を省いてよい条件と、省いてはいけない場所
毎回6ステップを全部やると、AIを使う意味が薄れます。
省いてよい場面と、どんなに小さい変更でも省かない場所を、先に決めておきます。
確認を軽くしてよいのは、次の条件がそろっているときです。
捨てるコードである(検証用のスクリプト、使い捨ての試作)
影響が閉じている(自分しか使わないツール、本番に出ない)
すぐ戻せる(差分が小さく、切り戻しの手順が決まっている)
機械が保証している(型チェック・lint・既存のテストが観点をカバーしている)
既存のパターンをなぞっただけ(同じ形のコードがすでに複数ある)
この場合でも、ステップ1の外形だけは見ます。
依存が増えていないか、範囲外のファイルが変わっていないかは、2分で確かめられるからです。
逆に、次の場所に触れる変更は、小さくても全ステップを通します。
- 認証・認可・セッション
- 課金・決済・金額の計算
- データの削除・更新(特に一括処理)
- マイグレーション・スキーマ変更
- 外部に公開しているAPIの入出力
- 個人情報を扱う処理この一覧は、チームのものに書き換えて手元に置いておくのがおすすめです。
迷ったときに判断する時間が、そのまま減ります。
8. チームに渡す前の最終確認
自分が確かめたコードでも、PRとして出す時点で、レビュアーには「どこまで確かめたか」が見えません。
AIが書いた部分を含むPRには、説明に次の3行を足します。
## AIによる生成と確認
- 生成した範囲: 【ファイル名・関数名。全部なら「全体」】
- 確認したこと: 【実在確認した名前 / 壊して確かめたテスト / 試した入力】
- 確認していないこと: 【時間の都合で見ていない観点・環境】3行目がいちばん大事です。
確認していないことを書いておけば、レビュアーはそこを重点的に見られます。
書いていないと、レビュアーは「確認済み」だと受け取るか、全部を最初から読み直すかのどちらかになります。
出す直前のチェックも置いておきます。
[ ] 差分のファイルは、依頼の範囲に収まっているか
[ ] 増えた依存は、必要性と実在を確認したか
[ ] 新しく出てきた外部の名前は、定義までたどったか
[ ] catch と既定値の補完は、吸収してよい失敗だけか
[ ] 重要な行を1つ壊して、テストが落ちることを確かめたか
[ ] 自分で説明できない行が残っていないか
[ ] PR説明に「確認していないこと」を書いたか最後から2つ目の自分で説明できない行は、残すと必ず後で困ります。
説明できない行があったら、AIに理由を聞くか、自分が説明できる書き方に直してもらってから出します。
よくある失敗と対策
失敗1: 上から全行を同じ深さで読む
時間がかかるうえ、疲れた頃に大事な箇所が来ると見逃します。
AIのコードは粗さの手がかりがないので、読む順番を決めないと集中が続きません。
対策: 5章の手順で、外形 → 名前 → 失敗経路の順に読む範囲を絞る。
全行を読むのは、絞った後に残った箇所だけにする。
失敗2: テストが通ったことで安心する
実装と同時に書かれたテストは、実装と同じ間違いを共有していることがあります。
対策: 重要な行を1つ壊して、テストが落ちるかを見る。
落ちなければ、そのテストは根拠にならない。
失敗3: エラー処理があることで安心する
try で囲まれていると、きちんと作られているように見えます。
実際には、失敗を隠しているだけのことがよくあります。
対策: catch の中を読み、失敗したときに何が返るかを確かめる。
呼び出し側が失敗に気づけないなら、例外を上に投げるよう直す。
失敗4: AI自身に「問題ないか」を聞いて終わる
同じ前提で見直すので、同じ場所を見逃します。
対策: 確かめる対象の列挙を頼む(6章)。
判断は自分で行う。
失敗5: 観点が多すぎて、結局やらなくなる
12個を毎回全部当てようとすると続きません。
対策: 普段は3章の5行だけ当てる。
7章の「省いてはいけない場所」に触れる変更のときだけ、全ステップを通す。
まとめ
AIのコードは、分かっていない箇所も整った見た目で書かれる。粗さを手がかりに疑えない
疑う場所は、外との接点と失敗経路で決める
頻出するのは、存在しない参照・古い書き方・過剰な抽象・握りつぶした例外・実装をなぞるテストの5つ
確認は、外形 → 新しい名前 → 失敗経路 → テストを壊す → 自分の入力 → 説明の順に進める
省いてよい条件と、省いてはいけない場所を先に決めておく
PRには「確認していないこと」を書き、レビュアーに渡す
まずは次にAIのコードを受け取ったとき、3章の5行だけ当ててみてください。
どこを読めばいいか分からなかった差分に、読む場所が見えてくるはずです。