AIが書いたコードをそのまま本番に出して痛い目を見た話
先に言っておくと、これは完全に自分のミスの話。AIが悪いという話ではない。
金曜の夜、締め切りに追われて判断力が落ちている時にやらかした。
何が起きたか
社内ツールの改修で、設定ファイルのバリデーション関数をAIに書かせた。出力を見て「良さそう」と思い、テストデータ1パターンだけ通して本番にデプロイした。金曜の21時。早く終わらせて帰りたかった。
翌週月曜の朝、Slackの通知が止まらない。設定ファイルの更新が全て失敗しているというアラート。バリデーション関数が、正しい設定ファイルまで弾いていた。
朝9時の時点で、社内の設定変更が約30件溜まっていた。全部バリデーションエラー。担当チームから「設定が反映されない」「週末に入れた変更が消えた?」という問い合わせがSlackに複数来ていて、出社して画面を開いた瞬間に血の気が引いた。
原因
AIが書いたバリデーションのロジックが、仕様を部分的にしか満たしていなかった。
具体的には、オプショナルなフィールドが未指定の場合を「不正」と判定していた。仕様上はオプショナルだけど、AIに渡したプロンプトにその情報を含めていなかった。
設定ファイルのスキーマには15個のフィールドがあり、そのうち6個がオプショナル。自分がAIに渡したのは「設定ファイルをバリデーションする関数を書いて」という雑な指示と、必須フィールドの一覧だけ。オプショナルフィールドの存在を伝えなかったから、AIはそれらを「未知のフィールド」として扱い、存在しない場合にエラーを返すロジックを生成した。
つまり、AIは渡された情報の範囲で正しい実装をしている。雑なプロンプトを投げた自分が悪い。
なぜレビューが甘くなったか
金曜の夜で早く帰りたかった、というのが正直なところ。AIが出したコードが「動いている」ことを確認しただけで満足してしまった。
普段なら他人のコードをレビューする時に仕様との照合をやるのに、AIが書いたコードだと「AIが書いたから大丈夫だろう」という油断が生まれる。自動化バイアスと呼ばれるやつ。自分はこれにまんまとハマった。
振り返ると、自分のレビューにはいくつか穴があったが、一番痛かったのは既存テストを回さなかったこと。
既存のテストスイートを通していなかった。 このツールにはもともとユニットテストがあった。そのテストには、オプショナルフィールドを省略したケースがちゃんと含まれていた。「バリデーション関数を追加しただけだから」と既存テストの実行を省略したが、たった1回 go test ./... を叩いていれば、この障害は起きていなかった。既にある資産を使わなかった。これが一番悔しい。
他にも穴はあった。テストで使った設定ファイルが全フィールド揃ったサンプルデータ1パターンだけだったこと。AIの出力をざっと眺めただけで、各フィールドのバリデーション条件を仕様書と1つずつ照合しなかったこと。コードの構造が綺麗だったので、中身も正しいだろうと思い込んだ。どれも初歩的なミスだけど、疲れと焦りで全部すっ飛ばした。
修正と対応
社内ツールだったのが不幸中の幸い。ユーザー向けサービスだったら始末書では済まなかった。
修正自体は10分。オプショナルフィールドの判定を追加するだけ。原因特定より、影響確認と関係者への「すみません」連絡の方がよほど時間がかかった。
対応タイムラインはこう。
- 09:00 アラート検知、Slackで問い合わせを確認
- 09:15 原因特定(バリデーション関数のオプショナルフィールド判定)
- 09:25 修正コードをデプロイ
- 09:30 滞留していた設定変更30件が正常処理されたことを確認
- 10:00 関係者への障害報告とポストモーテムのドラフト完了
障害時間は約60時間(金曜21時のデプロイから月曜朝9時25分の修正デプロイまで)。社内ツールだったので週末は利用者がおらず、実質的な影響は月曜朝の1時間半程度だった。それでも30件の設定変更が詰まっていたのだから、週末挟んでなかったらもっと被害は大きかった。
学んだこと
一番の教訓は「既存のテストを回せ」。これに尽きる。10分で書けるコードのために、既にある検証の仕組みを省略して障害を起こした。新しいプラクティスを導入する前に、今あるものを使い切れという話。
あとは2つ。
AIは仕様の全体像を知らない。 自分がコンテキストを渡し忘れたらそのまま穴になる。AIにテストを書かせても、同じコンテキスト不足を引き継ぐ。テストケースの設計だけは仕様書を見て人間がやる方が安全だった。
金曜の夜にデプロイするな。 これはAI関係ない。でも今回、疲れと焦りでレビューが雑になったのは事実で、もし翌日すぐ対応できる時間帯にデプロイしていれば、被害は数分で済んでいた。
この失敗を受けて導入したレビュー体制
ポストモーテムの結果、チームでいくつかルールを変えた。
CIが通るまでデプロイをブロックするようにした。 一番効いたのがこれ。もともとCIにテストは組み込まれていたが、ローカルで動作確認したらCIの結果を待たずにデプロイできる状態だった。これを物理的にブロックした。仕組みで防げるものは仕組みで防ぐ。今回の障害はこれだけで防げていた。
PRに「AIが生成したコードか」を明記するルールにした。 AIコードの場合、レビュアーは追加で以下を確認する。
- AIに渡したコンテキストは十分か
- 仕様書とコードの各条件分岐が1対1で対応しているか
- 境界値・異常系のテストケースが網羅されているか
- AIが「知らないはず」の暗黙の仕様がないか
正直、チェックリストがどこまで機能するかは分からない。でも「AIコードは追加で確認が必要」という意識をチームに共有できたのは良かった。
AIへの丸投げをやめた。 AIと対話しながらコードを書くスタイルに変えた。途中で「オプショナルフィールドはある?」と聞ける。丸投げして結果だけ受け取るのとでは、仕様の抜け漏れへの気づきやすさが全然違う。
その後、変わったこと
この障害以降、AIにコードを書かせる時のやり方がだいぶ変わった。
プロンプトに仕様を丸ごと渡すようになった。 「バリデーション書いて」ではなく、仕様書のURLか全文を含める。手間はかかるが、出力の精度が段違い。今回の障害も、オプショナルフィールドの仕様を渡していれば起きなかった。
AIの出力を「知らない人のPR」として読むようになった。 自分が指示を出したから自分のコードだ、という感覚があると、レビューが甘くなる。知らない人がPull Requestを出してきたと思って読む。そうすると仕様との照合を省略しなくなる。
テストコードだけは自分でケースを洗い出すようになった。 AIにテストを書かせるのは良い。ただし「どのケースをテストするか」は人間が決める。AIが書いたテストは、AIが書いたコードと同じ盲点を持っている。テストが通ったから安心、は危ない。
金曜の夜にデプロイしなくなった。 社内ツールでも。段階デプロイも導入して、ステージングで1日動かしてから本番に出すようにした。
結局のところ
AIが書いたコードを本番に出す判断をしたのは自分。障害の責任はAIではなく自分にある。
今回一番身に染みたのは、「AIが速くコードを書いてくれること」と「そのコードが正しく動くこと」は全く別の話だということ。速く書けるようになった分、レビューと検証に時間を使わないと、結局障害対応で倍の時間を持っていかれる。
あと、既存のテストを回していれば防げたという事実が地味にきつい。新しいツールやプラクティスの前に、今あるものを使い切る。当たり前のことを当たり前にやる。疲れている金曜の夜でもそれができる仕組みを作る。自分にとってはそれが一番の収穫だった。
関連記事
他の記事
ブラウザ横スクロールアクションゲーム開発記|TypeScript×Canvasで作るネオンランナー
HTML5 CanvasとTypeScriptで横スクロールアクションゲーム「ネオンランナー」を自作した。物理エンジン、衝突判定、操作性の工夫を解説する。
ブラウザで遊べるテトリス風パズルをTypeScriptで作った話
HTML5 CanvasとTypeScriptでテトリス風ブロックパズルを実装した。エンジンとUIを分離するSnapshotパターンや、ネオン演出のこだわりを解説する。
ゲーム実況のサムネイル作りで学んだデザインの基本
非デザイナーがゲーム配信のサムネイルを自作し続けて気づいた、視認性・配色・構図の基本ルール。試行錯誤の記録。
配信のコメント欄が動いた瞬間の話。ゼロからイチの体験
ゲーム配信でコメントがゼロの日々を超えて、初めてリアルタイムでコメントが来た瞬間の話。ゼロからイチの体験がモチベーションを変えた。