1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AIが書いたコード、読んでますか?

1
Posted at

この記事を読んでほしい人

  • AIに書かせたコードをどこまで読むべきか決めきれていない人
  • 全部読んでいて「結局AI使う意味あるのか」と思い始めている人
  • 逆に読まなくなって、なんとなく不安を抱えている人

結論を先に

エンジニアと話していて一番よく聞かれるのがこれ。

「AIが書いたコード、読んでるんですか?」

答えは4行になる。

  1. コードレビューはしている。
  2. ただし全部は読んでいない。
  3. どこを確認するかは事前に決めてある。
  4. 成り立つ理由は、設計の段階でアウトプットを保証しているから。

4行目が本題だ。設計時に機能要件と非機能要件を潰し、単体テスト・結合テストのテストケースをガチガチに固める。 そうすると、過程がどうであれ、実装の処理を理解していなくても最終的に出るアウトプットを保証できる。

読まなくていいのではなく、読まなくても保証できる状態を先に作っている

以下、その流れを書く。

設計(要件・非機能・テストケースをガチガチに固める)
  ↓
タスク分解 → Issue 作成(テンプレ通り)
  ↓
1 Issue ごとに autopilot(実装 → AIレビュー → PR作成)
  ↓
最終確認(人間が見る5点)

なぜ「全部読む / 読まない」が二択に見えるのか

エンジニアと話すと、だいたい2つに割れる。

  • 全部読む派:「自分がマージするコードに責任を持てないのはおかしい」
  • 読まない派:「テストが通っていて動くなら、読む時間が一番のコストだ」

どちらもそのまま採用すると詰む。

  • 全部読むと、AIの速度を人間の読解速度まで引き下げる。ピーク週に300近いコミットが出るペースでは物理的に不可能になる。
  • 読まないと、レビューで捕まえるべき事故がそのまま本番に出る。

この二択が生まれるのは、「過程」を検証しようとしているからだ。

コードを読むのは、過程を辿って出力を推測する作業だ。出力そのものを検証する手段があるなら、過程を辿る必要はない。逆に、出力を検証する手段を持っていなければ、過程を全部読むしかなくなる。

だから勝負は設計で決まる。


1. 設計:ここで全部潰す

M以上の変更では、実装前に設計書を書く。書く項目は決まっている。

設計書に必ず書く14項目

# 項目 これが無いと後で何が起きるか
1 要件 実装が「動くもの」に着地して「必要なもの」に着地しない
2 ゴール状態 完了判定ができない。どこまでやれば終わりか誰も言えない
3 これをする理由 途中で目的からズレた最適化が入る
4 アーキテクチャ 層の置き場所がAIの判断に委ねられる
5 データフロー どこで値が変わるか分からず、影響範囲が読めない
6 この設計にした理由 レビューで「なぜこうなってる?」が毎回発生する
7 他の設計にしなかった理由 一度棄却した案が別Issueで復活する
8 やること AIが「正しいと思う方向」に範囲を広げる
9 やらないこと 善意でスコープ外の実装が付いてくる
10 パフォーマンスの確認 非機能が実装後の問題になる。直すと設計が変わる
11 セキュリティ 認可・入力検証が「後で見る」に流れる
12 影響範囲(外部 / 内部) 壊れる場所が実装後に発覚する
13 テストケースの設計 出力を保証できない。仕組み全体が崩れる
14 動作確認とそのやり方 最終確認で何を触ればいいか分からない

以下、効きが大きいものだけ補足する。

「この設計にした理由」と「他の設計にしなかった理由」を分けて書く

採用理由だけ書くと、棄却した案が別のIssueで復活する。AIは過去の判断を持っていないので、同じ論点を毎回持ち出してくる。

棄却理由を書いておくと、それが後続Issueへのガードレールになる。 「B案は◯◯のため採用しない」と設計書にあれば、実装が B 案に寄ったときに理由付きで却下できる。

非機能は設計で潰す。実装後に回すと設計が変わる

パフォーマンスの確認セキュリティ を設計書の項目にしているのは、この2つが「実装後に発覚すると設計まで戻る」種類の要件だからだ。

  • クエリの件数・レスポンス目標を設計時に決めていないと、実装後に「N+1 だった」で構造が変わる
  • 認可の粒度を設計時に決めていないと、実装後に「role 分岐が足りない」でインターフェースが変わる

機能要件のバグは実装の中で直る。非機能要件のバグは設計に戻る。だから先に潰す。

影響範囲は「外部」と「内部」で分ける

ここを分けないと、影響範囲が「触ったファイル一覧」になってしまう。

見るもの
外部 公開API・外部連携先・DBスキーマ・他チームが依存している境界。壊すと自分たちだけでは戻せない
内部 モジュール間の参照・共通コンポーネント・型定義。壊しても自分たちで直せる

分ける理由は、戻せるかどうかが違うから。外部影響があるなら後方互換とマイグレーション戦略が要る。内部だけなら実装の裁量で済む。この判定が設計時にできていると、最終確認で見る場所が確定する。

テストケースの設計:ここが仕組みの心臓

ここをガチガチに固める。 仕組み全体がこの1項目に乗っている。

設計書のテスト仕様は、表で書く。

# テストケース 入力条件 期待結果 種別
1 正常に取得できる 有効なID データ返却 正常系
2 存在しない場合 無効なID NotFoundError 異常系

そして テストケース名を、実際のテストファイルのテスト名と一致させる。

設計書(テスト仕様 #1〜#N) → テスト(RED) → 実装(GREEN)

単体だけでなく、結合の層も設計時に決める。この2つは別のものを保証している。

保証するもの
単体 個々の関数・クラスの入出力
結合 実際の呼び出し経路(router → handler → usecase → domain)と認可分岐・トランザクション

結合テストの書き方も設計で決めておく。実際にはこの形が主力になっている。

// router → handler → usecase → domain は実物を通す
// DB境界だけ DI でモックを差す
const createCaller = createCallerFactory(accountFeeRouter);
const c = createCaller({
  user: { id: 9, role: 'planner', departmentId: 1 },
  prisma: mockPrisma,
  ip: '127.0.0.1',
});

vi.mock でモジュールを差し替えるのではなく 依存注入でDB境界だけ差し替える。こうすると認可(role分岐)・トランザクション・日付境界まで実コードパスが通る。実装の中身を読まなくても、通っている経路が保証される。

設計書のテスト仕様が固まっていれば、最終確認は「設計書の表」と「実際のテスト名」を突き合わせるだけで済む。 実装コードを読まずに網羅性が判定できる。これがレビュー量を削る一番大きなレバーだった。

動作確認とそのやり方も、設計に書く

「動作確認する」だけでは最終確認で何もできない。どの画面で・どの操作をして・何が見えれば OK なのか を設計時に書く。

これを書いておくと、最終確認が「コードを読む」ではなく「決めた操作をして、決めた結果が出るか見る」になる。

設計で意識していること

  • 「やらないこと」の欄が空の設計は、まだ終わっていない
  • テストケースが正常系だけの設計は、まだ終わっていない。異常系・境界値・副作用が揃って初めて出力を保証できる
  • 「テストケースは実装しながら考える」をやると、この仕組みは成立しない。順番が逆になる
  • ステータス(下書き / レビュー中 / 承認済み / 実装中 / 完了)を飛ばさない。承認前に実装を始めると、検証の基準そのものが動く

2. タスク分解と Issue 化

設計ができたら、そこからタスクに分解して Issue を作る。

1 Issue = 1 スコープ

分解の基準は「その Issue だけで完了判定ができるか」。設計書のテストケースが Issue に割り当てられない粒度なら、分解が粗いか細かすぎる。

Issue はテンプレ通りに書く

空の Issue は作れないようにしている(blank_issues_enabled: false)。用途別の6フォームから起票する。

フォーム 用途
feature_request / task 実装
bug_report 不具合
documentation ドキュメント
epic 親タスク
postmortem 障害振り返り

task フォームの必須項目はこの3つ。

必須項目 中身
Description 概要
Acceptance Criteria GWT 形式(Given / When / Then)で書く
Scope 変更対象

AC を GWT で書かせているのが要点。 「◯◯できるようにする」だと検証できないが、「Given ◯◯の状態で / When △△したら / Then ××になる」なら、そのまま検証手順になる。設計書のテストケースが、そのまま Issue の AC に落ちる形にしている。

Issue 化で意識していること

  • Issue に AC が無いなら、それは Issue ではなくメモ。着手しない
  • Scope(変更対象)が書けないなら、まだ分解が終わっていない
  • 設計書のどのテストケースに対応するかを Issue から辿れるようにする

3. 実装:1 Issue ごとに autopilot

Issue が揃ったら、1 Issue ずつ自動パイプラインに流す。

/autopilot #123
  → Issue 検証(番号がなければ BLOCKED)
  → パイプライン選択
  → 実装
  → Quality Gates
  → AIレビュー
  → PR 作成(テンプレ通り)

Issue 番号が無いと起動しないようにしてある。ここを塞いでおくと、設計もACもない状態で実装が始まるルートが消える。

Quality Gates は固定順序

format → lint → typecheck → test → build

順序が重要で、format前にlintを見ると差分がノイズだらけになって読めない。逆にtypecheckが落ちている状態でtestの失敗原因を追うのは時間の無駄になる。CIが赤いとき何から直すかを毎回考えないよう、順序を固定した。

console.log が残っている、コメントアウトされたコードがある、マジックナンバーがある。これを人間がレビューで指摘しているチームは、レビューコストを自分で作っている。

AIレビューは「2軸」にする。1軸だと見落とす軸が固定される

AIに書かせたコードを、別系統のAIにレビューさせる。2種類、並列で。

レビュアー 見るもの 見ないもの
Claude architecture-reviewer 層境界、依存の向き、カップリング、命名規約、設計書との整合 実装の細かいバグ
Codex CLI (codex review) 実装バグ、パフォーマンス、エッジケース、エラーハンドリング、型の抜け プロジェクト固有の規約・設計思想

重要なのは 役割を意図的に重ねないこと。同じ観点で2回レビューさせても、同じものを2回見つけて終わる。「構造を見る係」と「挙動を見る係」に分けると、片方が構造的に見えない領域をもう片方が拾う。

実際、Codexは「東京23区トークンをUI選択集合の展開後に判定すべき」のような、仕様を踏まないと出てこない実装レベルの穴を拾ってくる。一方でCodexは「このリポジトリではdomain層にフレームワークをimportしない」を知らない。それはClaude側の担当になる。

両方 read-only で走らせる。レビュアーに書き込み権限を渡すと、指摘と修正が混ざって差分が追えなくなる。

npx -y @openai/codex review --base develop < /dev/null

< /dev/null は入力待ちハング対策。これで一度ハマった。

セキュリティ敏感領域(infra/ ・ Prismaスキーマ ・ 認証/認可 ・ .env* ・ IAM/Secret Manager ・ 公開API境界)を触る変更では、この2軸レビューをスキップできない。「今回は小さいから飛ばす」というフラグを用意していない。飛ばしたい気持ちが出るときこそ、飛ばしてはいけないタイミングだった。

PR もテンプレ通りに書く

PR 本文は最終確認の入力になる。だからテンプレを埋めさせる。

  • Closes #<issue>
  • 変更の概要
  • AC 確認(Issue の AC を表で1つずつチェック)
  • チェックリスト(Issue番号 / コミット形式 / console.log / CI / 設計書承認)
  • 動作確認(スクリーンショットまたは動画)

実装フェーズで意識していること

  • AIレビューは「見つけたもの」を信じる道具で、「見つからなかったこと」を信じる道具ではない
  • 指摘は全部反映しない。P2まで直すと差分が膨らんで、元の変更が指摘対応に埋もれる
  • どのレビュアーが何を見つけたかをコミットメッセージに残す。仕組みが効いているかを感覚ではなく履歴で測るため

4. 最終確認:人間が見る5点

ここが「全部は読まない」の実体。見るのはこの5つだけ。

# 確認箇所 何を見るか
1 動作確認と PR 本文 設計書に書いた手順で実際に動かす。PR 本文の AC 表が全部埋まっているか
2 テストが通っているか CI が緑か。加えて、設計書のテストケースが実際のテスト名として存在するか
3 変更ファイルの一覧 設計の「やらないこと」に書いたパスに差分が出ていないか
4 マイグレーション / ライブラリ更新の有無 あれば別扱い。戻せるか、実DBで検証したか、依存追加の妥当性
5 AIレビューの指摘 P0/P1 は必ず対応。P2 は落とすか別Issueに逃がす

このリストに「差分を1行ずつ読む」が入っていない理由

設計時にテストケースを固めているから、出力は保証されている。 出力が保証されているものの過程を追うのは、二重の検証になる。

代わりに見ているのは 「保証の外に出ていないか」 だ。

  • 3(変更ファイル一覧)→ 「やらないこと」の外に出ていないか
  • 4(マイグレーション / ライブラリ)→ テストで保証できない領域に入っていないか
  • 1(動作確認)→ テストが通ることと実際に動くことは別なので、そこだけ人間が見る

つまり読んでいるのはコードではなく、保証の境界線だ。

3(変更ファイル一覧)が効く理由

差分の中身より先に、ファイル名の一覧だけを見る。設計の「やらないこと」と突き合わせるので、これが一番速く異常を検出できる。

「認証ロジックに手を入れない」と設計に書いてあるのに認証のファイルに差分がある。この時点で中身を読む必要はなく、差し戻しになる。

4(マイグレーション / ライブラリ更新)を別扱いにする理由

この2つは CI が保証してくれない領域 だから。

  • マイグレーション:戻せるか。DB制約(EXCLUDE / CHECK / FK / CASCADE)は実 Postgres でしか検証できない
  • ライブラリ更新:依存追加の妥当性は静的解析では判定できない。pnpm audit を通したうえで人間が見る

だから スキーマ変更は無条件に最重要扱いにしている。ここだけは差分の全行を読む。

5(AIレビューの指摘)の扱い

優先度 内容 対応
P0 セキュリティ・データ損失リスク マージ前に必ず直す
P1 バグ・アーキテクチャ違反 マージ前に直す
P2 スタイル・提案 やれたらやる(別Issueに逃がすことも多い)

「P2は今回やらない」と決めるのも判定業務のひとつだ。判定の出口は Proceed / Request Changes / Block の3つで、ここだけは必ず人間が出す。

最終確認で意識していること

  • 「良いコードか」ではなく「決めた出力が出ているか」を見る。 AIのコードはだいたい綺麗なので、綺麗さで判定すると全部通る
  • AIの「問題ありません」は「見えませんでした」と区別できない。 出力が同じだから、判定には使えない
  • 読めない差分は読めない。1000行を超えたら確認箇所を決めていても機能しないので、差分を切る方を先にやる

なぜこの仕組みで成り立つのか

ここまでを一言でまとめる。

設計で機能要件・非機能要件を潰す
        +
設計でテストケース(単体・結合)をガチガチに固める
        ↓
過程がどうであれ、アウトプットを保証できる
        ↓
実装の処理を理解していなくても、マージ判断ができる

「AIが書いたコードを読まなくていい」のではない。「読まなくても保証できる状態を、設計で先に作っている」。

だから設計をサボると、この仕組みは即座に崩れる。設計が甘いまま autopilot を回すと、出力を検証する基準がないまま実装が出てくる。そうなったら全部読むしかない。楽をするために、上流で一番苦労しているという構造になっている。

この仕組みが崩れる条件

正直に書いておく。これは万能ではない。

テストケースの設計が甘いと、全部崩れる

仕組みが1点に依存している。設計書のテストケースが不十分なら、保証されているのは「不十分な範囲」だけになる。正常系しか書いていない設計は、正常系しか保証しない。

だから設計レビューでは、実装の議論よりテストケースの網羅性を先に見る。

テストで保証できない領域がある

ここは仕組みの外なので、人間が別で担保している。

保証できない領域 どう担保しているか
DB制約(EXCLUDE / CHECK / FK / CASCADE) 実 Postgres が必須でモック不可。CI に DB サービスが無いので DATABASE_URL 未設定時は skip される。ローカルで実行し、結果を PR 説明に記録する
外部APIの実挙動 結合テストのモック境界の外。ステージングでの動作確認
ブラウザ上の実際の挙動 動作確認(設計書に手順を書いておく)

1つ目が典型例で、テストコード自体には設計書のテストケース #1〜#11 との対応が書かれているが、CI では常に skip される。「テストがある」と「CIで走っている」は別なので、ここを混同すると保証していないものを保証したと思い込む。

最終確認の4番(マイグレーションの有無)を独立項目にしているのは、この穴を人間が塞ぐためだ。


ハマったこと

自分がやって失敗したものを並べておく。

テストケースを実装しながら考えていた

これが一番大きい。テストケースを後から書くと、実装に合わせたテストになる。 「実装が正しいこと」を確認するテストになってしまい、「仕様どおりであること」を確認していない。設計時に固める順番でないと、出力の保証にならない。

非機能要件を実装後に見ていた

パフォーマンスと認可を「動いてから確認」にしていた。結果、N+1 と role 分岐漏れで設計に戻った。機能要件のバグは実装で直るが、非機能要件のバグは設計に戻る。

レビュアーを1つにしていた

最初はClaudeだけでレビューしていた。アーキテクチャ違反はよく見つかるのに、実装の細かいエッジケースが本番で出た。同じモデルは、同じ種類のミスを見落とす。 別系統のAIを入れて初めてそこが埋まった。

AIに最終判定させようとした

「レビューして問題なければマージして」とやったことがある。AIは「問題なし」と返す。でも判定できて問題なしなのか、見えていなかったのかが区別できない。統合判定を人間に戻した。

「テストがある」で安心していた

CI で skip されているテストを、通っていると思っていた。保証しているつもりで保証していないのが一番危ない状態だった。

差分を大きくしていた

差分が1000行を超えると、確認箇所を決めていても機能しない。1 Issue = 1 PR がこの仕組みの前提条件だった。


まとめ

二択で考えていたとき
コードを読んで出力を推測する 設計でテストケースを固めて出力を保証する
レビュー時に確認箇所を考える 設計時に確認箇所が決まっている
全部読む / 読まない 保証の境界線だけを見る(5点)
人間が指摘する 機械に落とせる指摘は人間まで上げない
AIレビューは1回 構造係と挙動係に分けて2軸・並列
非機能は動いてから確認 設計で潰す
AIが「問題なし」と言ったら通す 判定は人間が持つ

最初の4行に戻る。

コードレビューはしている。全部は読んでいない。確認箇所は決めてある。成り立つのは、設計でアウトプットを保証しているから。

AIを使うようになって一番増えた作業は、コードを読む時間ではなく テストケースを設計する時間 だった。


自社データ基盤をNestJS + Prisma + BigQueryで内製しています。ここで挙げた設計書項目・Issue/PRテンプレート・2軸レビューはリポジトリのルールとして明文化してあり、AIエージェントがそれを読んで動く構成にしています。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?