んなわけねえだろと思うので,つらつらと書きます.
はじめに
生成 AI の馬力によって,コーディングの速度はものすごく上がりました.成果物が早く出る時代になったからこそ,その品質をどう保証するかは大きな関心ごとです.
一方で,コードを目視するのが品質保証だというのは認識が古い (これはそう),副作用を適切に分離しているならブラックボックステストで充分 (気持ちはわかる),コンパイラが生成したバイナリをいちいち検証しないように生成 AI もある程度信頼すればよい (ある程度はね) といった主張も見かけます.生成されたコードの中身を確認しなくても品質は保証できるので,事業のスピードを優先しようとのことです.
品質保証の主要な手段がただの目視に留まらないのは,それはそう,その通りです.これは生成 AI で急に変わった話ではなくて,ヒューマンエラーや労力を減らすために入れられるものはむしろ積極的に入れたほうが良いでしょう.私はトイルを撲滅したくて仕方がない.強い型システムや Linter や自動テストを採用したり,それを CI で動かしてレビューやデプロイ前にブロックしたり,あるいは ArchUnit のようなツールでアーキテクチャ制約を機械的に検証したり,そういった人間の負荷を減らす方向自体に異論はありません.生成 AI の導入も無駄な労力を肩代わりさせて本来注力すべき作業に関心を向けるために有用でしょう.
ただ「目視に頼らない」というのと「中身を理解しなくて良い」というのは,だいぶギャップがあるように思います.ツールや手法のアップデートは良いのだが,検証そのものの放棄をするのはどうなんって話です.それに決定論的なツールと,生成 AI のような非決定論的なツールとでは,ユーザとしての向き合い方がまるで違うはずです.なのにこれらが地続きに語られていることに,エンジニアとして大きな違和感を覚えます.
関心を向けなくてよいのはどんなときか
コードの中身に関心を向けずに品質を保証しようという試み自体は自然なもので,いくつか手法は挙げられます.ただ,それは何をやっているかの理解が不要になるという話ではありません.我々がコードを逐一読まなくても良いのは,別の仕組みが一定の保証を与えてくれるからです.
ブラックボックステストは入出力の関係だけを検証します.何をやっているかではなくどう振る舞うかという,プロダクトとしての本質的な部分を確認できる重要な手法ですが,これは性能特性や副作用,あるいは並行実行での振る舞いのように,入出力で観測できない事実まで一般に保証するものではありません.たとえば のアルゴリズムでも,小さな入力では正しい結果を返してテストを通ることがあるし,処理の最後にメール送信やファイル書き込みのような副作用を紛れ込ませていても,戻り値だけを見ている限りは気付けません.また,どこをブラックボックスとして扱うかという設計判断そのものは,テスト設計する人間の理解を前提としています.
型システムは,コードの構造に対する静的な制約を課します.Rust であれば,データ競合 (レースコンディション一般ではなく) やダングリングポインタといった問題の多くをコンパイル時に排除できます.Haskell のように副作用を型で分離する言語では,関数が IO を含まないことすら型で表現できます.コードを一行ずつ追わなくても,こういった性質をコンパイラが保証してくれますが,その保証は当然ながら,型システムが表現できる範囲に限られます.
コンパイラもまた,計算機を動かしたい人間が機械語を逐一確認しなくて済むようにする仕組みです.我々は生成されたバイナリを毎回読んで検証したりはしません.それが可能なのは,ソースコードと生成された機械語との間に,仕様として定義された対応関係があるからです.コンパイラの正しさは,その対応関係が保たれているかという形で議論できます.
いずれも,人間の認知負荷を下げる枠組みです.ただし,それぞれ保証できる範囲は違います.人間がコードの正しさを直接確認しなくても済むのは,その代わりに一定の保証を与えてくれる仕組みがあるからです.人間が考えなくてよくなったのではなく,何を信頼し,何を検証すべきかを別の形で判断しているだけです.
LLM で保証されないものに目を向ける
では,生成 AI も同じように信頼してよいのでしょうか.私はそうは思いません.
コンパイラとの類推がよく持ち出されますが,同じような信頼の置き方ができるわけではありません.コンパイラへの入力はプログラミング言語であり,その構文や意味はあらかじめ定義されています.言語によって厳密さには差がありますが,少なくともプログラムの意味として何を保存すべきかは定義されており,コンパイラが正しく動いているかどうかも,元のプログラムの意味を保ったまま機械語に変換できているかという形で議論できます.
一方で,LLM への入力は自然言語です.「こういうシステムを作ってほしい」という要求そのものが曖昧で,何をもって正しい出力とするかはプロンプトを書いた人間の意図に依存します.コンパイラのように意味が保存されていることを機械的に照合する基準は存在しません.同じプロンプトから異なるコードが生成されることもありますし,そのどちらが本当に要求を満たしているか,結局は人間の判断によるものです.
もっとも,生成されたコード自体には,型検査や lint や静的解析を掛けたり,テストを実行したりはできます.しかしながら,それらが保証するのはあくまでもソフトウェアとしての断片的な性質です.自然言語で与えられた要求そのものと実装との対応関係を保証してくれる仕組みはありません.コンパイラには意味の保存を議論するための基準がありますが,LLM にはそれがないわけです.性質の検証を委譲しても,そのコードが本当に要求を満たしているのかという最終的な判断は,依然として人間の理解に依存しています.
テストに通るが本番で壊れるコード
もう少し具体的な例で考えてみましょう.生成 AI に「イベントの定員と現在の参加人数を見て空きがあれば参加登録する」ようなコードを書くよう依頼して,以下のような素朴な実装が出てきたとします.
def join_event(event_id, user_id):
event = db.fetch_one(
"SELECT capacity, participant_count FROM events WHERE id = %s",
event_id,
)
if event.participant_count < event.capacity:
db.execute(
"UPDATE events SET participant_count = %s WHERE id = %s",
event.participant_count + 1,
event_id,
)
# 参加レコードの登録など
return True
return False
見た目は問題ありません.テストも通るでしょう.しかしながら,このコードはチェックと更新が一つのトランザクションとしてアトミックに行われていないため,典型的なロストアップデートが起こりえます.READ COMMITTED のような一般的な分離レベルではこれが素通りし,行ロックなどを取らなければ防げません.定員ぎりぎりのイベントに別々の二人がほぼ同時に申し込むと,どちらも「まだ空きがある」と判断してしまいます.それで,一方の更新がコミットされる前にもう一方がチェックを通れば,参加人数の確認の後と更新の前に互いの処理が割り込み,定員を超えて登録されてしまうわけです.
本来なら SELECT ... FOR UPDATE で悲観的ロックを取ったり,UPDATE events SET participant_count = participant_count + 1 WHERE id = %s AND participant_count < capacity といったチェックと更新を一つの文に畳んでアトミックに処理し,更新された行数で成否を判定したり,といった実装が要ります.
しかしながら,実行文脈を明示されなかった LLM は,このように「取得 → 更新」という素朴なパターンを出力することでしょう.並行性を意識せずに書かれた世のサンプルコードを反映すれば自然とこの形に落ち着きますから.並行実行した際の整合性は,コード片の局所性ではなく,システム全体の実行文脈に宿るもので,プロンプトにその文脈が明示されていなければ,空気を読んだ LLM が自発的にロックまで考慮してくれる保証はどこにもありません.
厄介なことに,この手の問題はテストで見つけにくいものです.モックを使った検証や単一スレッドのテストでは再現しないし,データを大量に流せば見つかるという類でもなく,負荷テストでも確率的に踏む程度に留まります.それで顕在化したときには,定員を超えて受け付けてしまったという形で,すでに本番障害になっています.コードを見れば「ああ,ロック取ってないね」と一瞬でわかるのに.
じゃあ,最初から並行性を考慮するようプロンプトで指示すれば良いだろ? まあそうなのだが,それは事後諸葛亮ってやつですね.問題が起きた後に「こう指示すべきだった」と言うのと,起きる前に何を指示すべきかを網羅的に把握することの間には,大きな距離があります.トランザクション分離レベルと並行性やら,リソースの管理やら,エラーハンドリングの網羅性やら.これらをすべてプロンプトに書き下せる人は,すでにそのコードの設計を理解している人です.そして,それをやる人に属人化しないよう,現場やチームをどうするかが,本来問われるべき問題じゃないですかね.
データベースのスキーマ設計もそうです.先ほどの競合を一文のアトミックな更新で綺麗に防げるかどうかはスキーマ設計次第で,participant_count をカラムで持っていれば良いですが,参加人数を別テーブルの COUNT で都度求める設計であれば,イベント行のロックや参加テーブルの一意制約など別の手段が要ります.漠然とした指示からは漠然としたスキーマが出てきて終わりだし,どういった設計なら整合性を守れるかを見越して文脈を与え,出てきたものが妥当か確認することができて初めて回ります.銀の弾丸のような検証はできないし,AI レビューだってそう,結局コードの中身を理解しないと見えない問題が確かに存在するよねって話です.
テストを書くこととテスト戦略を設計することは違う
テストを生成 AI に書かせること自体は良いことです.どんどん書かせましょう.ボイラープレートを書くのとか面倒です.しかしながら,テストを書くこととその戦略を設計することは切り分けて考えるべきです.
どこをブラックボックスとして扱い,逆にどこをホワイトボックスで見るか,モックの境界はどこに引くのか,テストピラミッドをどう組んで,どのレイヤーに検証の重心を置くのか,どの規模のデータで,どの並行度で動かせば問題が表に出るのか,これらはテストコードそのものではなく,検証手段の設計にあたります.煎じ詰めれば「何を検証しなくてよいか」を決める作業です.ここはモックして構わないとか,この境界の外側は信頼してよいとか,このレイヤーは入出力だけ見ればよいとか,そういった判断のためには,その箱の中身と置かれた文脈の両方を理解していないといけません.
たとえば私は,複雑なロジックを純粋関数にして入出力だけを見るブラックボックステストに寄せる,副作用を持つ処理は境界の外に分離することで,実装の詳細に依存したモックを増やしすぎない,といった方針を好みます.何をやっているかを排して,どう振る舞うかをある種のドキュメントのように定義できるので,ブラックボックステストは積極的に使います.ところが,どこをブラックボックスにしてよいのか,その中身を理解していないと線引きできません.入出力だけを見る手法なんか使うななんて意図はまったくなくて,その境界を引く作業がコードの理解を要するという話です.
「テストが通れば充分!」という主張は,しばしばこの設計判断が終わったものとして話しています.じゃあ,その境界を引いたのは誰なのか,テスト戦略ごと生成 AI に丸投げして,出てきたテストスイートが妥当かどうかを,コードベースの設計を理解しないまま判断できるのか甚だ疑問です.テストが通ったという事実は,そのテスト戦略が妥当だったときにだけ意味を持ちます.たとえカバレッジの数字が良かったからといって,事前の設計の質を超えては何も保証しません.そして,その設計こそが人間の仕事です.生成 AI と壁打ちしたって良いですが,丸ごと移譲して良いわけがない.
これは AI レビューについても同じです.私だって反 AI じゃないですし,コードを一行ずつ目で追うなんてせず,生成 AI にレビューさせることは日常的にやっています.それ自体は良いですよ.それでも,AI のレビューがどれだけ役に立つのかは,こちらがそのコードベースをどれだけ理解しているかに大きく依存します.よく知っている領域なら,AI の指摘が的を射ているか見当違いかをすぐに判断できます.その一方で,自分の理解が及ばない領域をレビューさせれば,返ってきた指摘が妥当なのかどうかを評価できず,結局そのために自分でアドホックに書き散らかされたコードベースを読み込む羽目になるわけです.AI のレビューは,使う側の理解を肩代わりしてくれるのではなく,理解があってこそ働く道具です.
逆説的ですが,AI が書いたコードは人間が書いたコードより内容の理解をむしろ必要とする面があります.人間が書いたコードの背後には,少なくとも「なぜそう書いたか」という一貫した意図を持つ主体が存在します.もちろん人間も複雑さを埋め込むし,ブログのコピペで や クエリを仕込むこともあるし,意図しない副作用を紛れ込ませることもあるでしょう.それでも,そう書いた理由を一貫して参照できる主体がいること自体は,後からの問い合わせを原理的に可能にします.令和だと「Claude が書いたから」で終わるかもしれないが.
コードレビューがレビューとして成り立つのは,この枠組みがあるからですが,LLM にはこれがありません.そう書いた理由を LLM に問い合わせても,返ってくるのは事後的な尤もらしい説明です.少なくとも,人間の設計レビューで前提になるような「どうしてこの設計判断をしたのか」を一貫して参照できる主体としての意図が無いわけです.背景情報が減っているのに,判断の負荷だけが下がることはありません.
AI に頼りたい部分ほど検証しづらい
主体的な意図を問いただせない以上,そのコードが本当に要求を満たしているかを担保するには,人間が「正しい振る舞いとは何か」を外側から規定してやるしかありません.ここで,先ほどの「プロンプトでの指示を充分に行えば良い」とか「いっぱいテストを書けば解決する」とかいった話があるわけですが,こうした主張の裏にあるのは,そもそも関数やシステムが「何をすべきか」という仕様を,AI に指示する人間が正確に把握しているという暗黙の了解です.
簡単なプロパティベーステストを考えてみましょう.ソート関数に対して「出力がソート済みであること」だけを要件にしてしまえば,常に空リストを返すようなふざけた実装でもテストは通ってしまいます.ソートとしての意味を持たせるには出力が入力の置換である性質を加えないといけないし,安定ソートを保証するには等しい要素の順序を保存する性質を加えないといけません.つまり,充分に強いテストを書くということは,実質的に仕様を形式化することと同義なわけです.
ここで仕様をどれだけ形式化できているかが効いてきます.仕様を形式化できるほど理解しているなら,生成 AI への指示も曖昧な自然言語ではなく,もっと精密に書けるはずですし,それを検証する強いテストも設計できるはずです.裏を返せば,ふんわりした自然言語で「良い感じに」作ってくれと依頼するのは多くの場合,その仕様を形式化しきれていないからですよね.だからこそ,それを検証するテストも同様に緩くならざるを得ないわけです.
充分に仕様を理解しているけれど実装の手間を省きたい場合,たとえばボイラープレートの生成や定型的な CRUD の組み立てなどは,あまり抵抗なく AI に投げられる領域でしょう.その一方で,仕様の形式化が難しい部分ほど生成 AI に頼りたくなるのに,まさにその部分こそ厳密なプロンプトやテストでは担保しにくく,ふわっとした仕様だから AI に任せるが,ふわっとした仕様だからテストも弱くなってしまう,この構造的な課題は,プロンプトを弄り回したりテスト手法を捏ね回したりしたところで,根本的には解消しません.
技術の問題だけではない
ここまで技術的な議論をしましたが,現実の資本主義社会には,さらに厄介な問題があります.人間は「はやい!やすい!うまい!」を追い求めるということです.
せっかく生成 AI によってコーディングが速くなったのだから,その余剰時間を品質保証に充てれば,コードベースを整備するとか,静的解析や型を厳密にするとか,生成と検証のパイプラインを構築するとか,そういった技術的な手立ての多くは,どれも時間を掛ければ実現できます.しかしながら,皆さんが観測している通り,その速くなった分は更なる開発速度やコスト削減の要求に吸われます.タイパの最大化が優先され,生成 AI の活用で浮いた時間があれば「もっと多くの機能をもっと早く作れ!」と言われ,最終的な品質保証に割く時間はなかなか増えません.
結果として起きるのは,統制のないアドホックなコードの膨大な蓄積でしょうか.品質保証に時間を掛けることがスピードの敵と見做され,レビューそのものが形骸化していきます.たとえ技術的に検出可能な品質問題であっても,組織によっては知のポピュリズムの元に素通りされてしまうわけです.現代社会は案外ハリボテでも回ったりして,個人情報を漏らしてネットニュースになったり,人が死んだりしないと大きな問題にされない.結局「はやい!やすい!うまい!」ではなく「はやい!やすい!もろい!」が横行します.

少し余談になりますが,柞刈湯葉さんの『未来職安』という作品をご存じでしょうか.テクノロジーの発展によって,国民の 99 % が働かない消費者となり,逆に 1 % のエリートが働く生産者となる未来社会を描いた 2018 年の SF 小説です.作中には,とても示唆に富んだ職業が登場します.問題が起きたときに責任を取って辞職する専門職とか,監視カメラに写る職業とか.
テクノロジーが発達して生産活動が自動化されても「責任を取る」という社会的なプロセスだけは人間にしか担えません.生成 AI はバグを埋め込んだとしても,釈明のために顧客の前に立って頭を下げることは当然できませんから.そういうわけで,責任を持つ主体としての我々人間が,品質を検証して保証するプロセスを構造的に手放すわけにはいかないのですが,インセンティブの構造は,生産性を品質ではなく量へ変換する方向に働きます.技術的な対策を現場で幾ら用意しても,組織として時間を割けないなら絵に描いた餅です.
ではどうするか
だからといって,人間がコードを一行ずつ読めという精神論をやりたいわけではありません.生成 AI がもたらすコードの量とペースに対して,手作業のレビューで立ち向かうのが既に現実的ではない現場も多いことでしょう.
じゃあ,サービスの品質を妥協して馬鹿の一つ覚えみたいに SLA を下げてしまえば良いのかというと,そういう単純なトレードオフの話でもないはずです.デプロイ頻度を落とせとか,リードタイムを延ばせとか,そういう話でもないです.DORA の調査が示してきた通り,デリバリーの速さとシステムの安定性はトレードオフと思われがちですが,実際には両者の相関が報告されています.速く作ること自体が悪いのではなく,速くなった分で構造的な検証プロセスまで押し流してしまうことを何とかすべきです.
必要なのは「読まなくて良い」と放棄することではなく,読む主体と方法を変えることでしょう.その方向性は,技術的なものと組織的なものに分けられるかと思います.
技術的な方向性は,人間が確認しなければならない範囲を仕組みとして小さくしていくことに尽きます.量産されるコードを全部読むのが無理なら,読まなくて済む部分を増やし,読むべき部分を浮かび上がらせればよいわけです.
まずはコードベースのアーキテクチャです.生成 AI は放っておくとコピペの延長のようなアドホックな差分を無尽蔵に量産します.ここで,システムが適切に抽象化され境界が整理されていれば,同じような判断があちこちに散らばることを防ぎ,人間がレビューすべきコードの表面積そのものを抑え込めます.密結合に陥らない範囲でのいわゆる “DRY” な設計が,AI 時代にはレビュー負荷の防衛線として効いてくるわけです.
それから,機械に任せられるものは機械に弾かせましょう.リソースリークや非効率なクエリパターン,意図しない依存関係の検知といった,人間が目で追うにはしんどいものを静的解析に回していきます.TypeScript の strict モードや Python の型ヒントと mypy のように,既存のエコシステムを壊さず保証を足せるツールの活用も同じ方向です.Rust の所有権なども,人間が見るべき範囲を言語の機能で小さくする工夫と言えます.決定論的なツールで潰せるものを潰していけば,最終的に残るのは,本当に人間の理解を要する部分になってくることでしょう.
そうして残った核心にこそ,人間の集中を全振りしましょう.決定論的なツールで雑音を削ぎ落としてきたのは,このためです.ここで AI をレビューアとして噛ませるのは良いと思いますが,返ってきた指摘が妥当かを判断するためには結局こちらの理解が要ります.決定論的なツールは CI でマージを止めるゲートにできても,非決定論的な AI はあくまでアドバイザーとしての役割りに留め,主従を取り違えないことが重要でしょう.
一方で,技術的な対策を幾ら現場で用意しても,それらを回す時間が組織から与えられなければ行動に移せません.だからこそ,組織的な方向性が必要です.
まずは,時間の配分です.AI の馬力で浮いた時間の一部を品質保証に充てる意思決定を,組織として明示的に行っていきます.検証パイプラインの整備や丁寧なレビューを,ビジネス速度の敵ではなく,長期的なスピードを担保するための投資だと位置づけるわけです.マネジメントの問題だからと匙を投げて諦めたくなるところですが,品質保証を怠ってアドホックなコードを堆積させれば,結局は障害対応や調査に追われて将来の開発速度が落ちます.そういった ROI を考慮して,仕組みづくりのための投資を正当化するとともに時間を勝ち取るのも,AI 時代のエンジニアが担っていくべき交渉の範疇になってくるのでしょう.
知識の属人性についても考えます.特定のエンジニアだけが,並行性やアーキテクチャを見越して生成 AI に精緻な指示を出せたり,出力されたコードの瑕疵を見抜けたりする状態では,組織として脆いままです.だからこそ,個人の技能を組織の資産に変えていく必要があります.特定の誰かが暗黙にやっている「こう指示して,こう検証する」といったノウハウを,タスクに特化したツール (Claude Code でいう Subagent や Skill など) の整備や,CI に組み込むレビュープロンプトといった,システムとして再利用できる形に落とし込んでチームで共有していきます.そうすれば,一部の名人芸に依存した品質保証を減らし,理解のプロセスを個人の頭の中だけではなく,組織の側にも蓄積していけるでしょう.
品質保証のコストを,開発速度に対する税金として最初から組み込むか,障害対応のコストとして事後的に高く払うか,どちらかは結局払うことになります.生成 AI が膨大なコードを量産していけば,後者のコストは人間の処理能力をあっという間に超えていくことでしょう.だからこそ,コードの中身を何らかの方法で理解するプロセスを残しておくことが重要です.外から見える振る舞いだけを確認して良しとするのではなく,その構造を理解する営みを手放さないことこそが,AI が量産するシステムに対して,我々人間の果たすべき責任かと思います.
まとめ
生成 AI へのタスク移譲は,本来注力すべき問題への集中を助けてくれますし,私も仕事や日々の生活で日常的に使っています.これから先,コード生成やレビューへの活用はもちろん,ビジネスやエンジニアとしてのあり方の変化はますます進んでいくかと思います.
ただ,そのことと我々が理解や責任を放棄することは別問題です.我々は以前から,テストや型システムや静的解析といった仕組みと向き合いながら開発を進めてきたかと思います.生成 AI もまた開発を支える強力な道具ですが,何を委譲できるのか,そしてどこから先は人間が把握しなければならないのかという境界は,これまで以上に意識する必要があるでしょう.
コードの中身を,誰が,何が,どうやって理解するのか,AI 時代に問われるのは,その理解のあり方なのだと思います.
私の好きな言葉に “Trust, but verify” というのがあります.レーガン大統領が好んで使っていたやつです.
生成 AI 時代の品質保証も,結局はそこに帰着するのだと思います.ツールの進化は目醒ましいものですが,エンジニアとしてうまく向き合っていきたいですね.