メインコンテンツへスキップ
見出し画像

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個 = 800

    6. 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行だけ当ててみてください。
    どこを読めばいいか分からなかった差分に、読む場所が見えてくるはずです。

     
     
    Web エンジニアをしながら、いまは PM に挑戦中です。 プログラミングスクールでコーチもしています。 AI を使ってみてわかったことと、個人開発でやってみたことを書いています。 個人開発の相棒は Claude Code と Codex です。

    あなたへのおすすめ