「直しました」と「本番に出ています」は別の話だった
AIに開発を任せていて、いちばん高くついた失敗は、コードのバグではなかった。
AIは指示された作業を完了させ、テストを通し、正しく報告する。そこに嘘は無い。それでもその成果が世の中に出ているとは限らない。この1か月で本番サイトに起きた事故を並べてみたら、全部この形をしていた。
コードレビューでは見つからない種類の失敗なので、実例として残しておく。
ブランチに置き去りにされた、本命の修正
自分のサイトがGoogle AdSenseの審査で「有用性の低いコンテンツ」と判定され、その対策をAIと一緒に進めていた。テンプレートのような構成になっていた記事4本を解体して、実データで書き直す作業だ。
7月29日、その作業は完了していた。git worktreeを切って作業し、コミットも積まれ、報告も受けていた。
2日後、別のセッションで同じ問題に取り組んでいるときに気づいた。そのブランチはmainにマージされておらず、デプロイもされていなかった。本番に出ていたのは、翌日に別セッションが浅く作り直した版のほうだった。
つまり本命の修正は、完成した状態でブランチの中に2日間眠っていた。その間ずっと「対策は打ったのに効かない」と思い込んでいたし、同じ作業を別のセッションがもう一度やり直してもいた。
worktreeは並行作業に便利だが、作業が完了したことと、それがmainに入ったことの間に何の保証も無い。AIは自分が作ったコミットのことは覚えているが、それが最終的にどのブランチへ流れたかまでは見ていない。セッションが終われば、その記憶ごと消える。
対策として、運用ルールにこう書いた。
worktreeで作業したら、セッション終了時に
git worktree listとgit branch --no-merged mainを実行し、未マージのブランチが残っていないかを見る。残す場合はタスク管理ファイルに「どのブランチに何が入っているか」を必ず書く。
git branch --no-merged main を1回叩くだけの話だ。それを2日間、誰も叩かなかった。
直したはずの本文が、次のビルドで消える
同じ対策の一環で、サイト内のツールページに解説文を追加した。文字数の少ないページが「内容が薄い」と見なされている可能性があったからだ。
このサイトの公開ディレクトリ public/ には、実は別リポジトリのビルド出力をコピーしただけの領域が混ざっていた。ソースはこのリポジトリに無い。それを知らずに public/ 側のHTMLへ直接本文を書き足していた。
この時点では何も壊れない。ページは正しく表示されるし、デプロイも通る。問題が出るのは次にそのアプリをビルドして配置し直したときで、追記した本文はまるごと消える。ソース側には何も残っていないので、消えたことにも気づけない。
AIから見ると、public/ の下にあるHTMLはただのHTMLだ。それがどこかのビルド成果物なのか、このリポジトリが持っている正本なのかは、ファイルを見ただけでは判別できない。判別できないものは、判別しないまま編集される。
対策は単純で、対応表をルールに書いた。
| 公開ディレクトリ | ソースの場所 | 直接編集した場合 |
|---|---|---|
| Unity WebGLアプリ | 別のUnityプロジェクト | 一部のファイルを除き上書きされる |
| Reactアプリ A | 別のWebプロジェクト | 保護なし=消える |
| Reactアプリ B | 別のWebプロジェクト | 保護なし=消える |
「このディレクトリを触るときは、同じ変更をソース側にも入れる」と明文化しておかないと、同じことが起きる。実際にこれを書いてからは再発していない。
1行直したつもりが、全行が差分になっていた
Windowsで作業していると、これが定期的に牙を剥く。
スクリプトからソースファイルを書き換えるとき、たとえばPythonの open(path, 'w') は既定のテキストモードで開く。このモードは書き込み時に改行を LF から CRLF へ変換する。元のファイルがLFだった場合、1行しか変えていなくても全行が変更扱いになる。
git上では「500行変更」のように出る。何が変わったのか誰にも読めない。レビューは事実上不可能になり、コミットを分ける意味も消える。
厄介なのは、これがエラーにならないことだ。スクリプトは正常終了し、変更内容も正しい。壊れているのは差分の見え方だけなので、テストを走らせても気づけない。
これも運用ルールに落とした。書き込むときは open(path, 'w', newline='') を使うか、確実を期すならバイナリで読み書きする。そして確認方法も決めておく。
行末が変わっていないかの確認は
git diff --statの行数で見る。実際の変更よりファイル全体に近い行数が出ていたら行末の混入。
以前は別の判定方法を使っていたが、結果が安定せず当てにならなかった。チェックの手段そのものが信用できるかまで決めておかないと、確認したつもりで通してしまう。
「確認した」の中身が間違っていた
いちばん肝が冷えたのは、これだ。
本番に正しい版が出ているかを確かめるため、公開中のページを取得してローカルのファイルと比べた。ある1ページだけ、本番が1,726文字、ローカルが1,084文字と食い違った。本番に古い版が残っている、と一瞬で結論しかけた。
念のためバイト単位で比較し直したら、両者は完全に一致していた。差は取得したHTMLの数え方にあった。最初の比較では、対象範囲の取り方が本番とローカルで揃っていなかった。
これは本番の問題でもコードの問題でもなく、検証手順のバグだ。しかも「食い違いを検出した」という結果が出たので、一見すると検証が機能しているように見える。間違った検証は、無検証より危ない。何もしていなければ「まだ確認していない」と分かるが、間違った検証は「確認済み」という誤った安心を残す。
この件以降、本番の確認は必ず実ページを取得して行い、ローカルのファイルでは判断しないことにした。
「本番に何が出ているか」はローカルのファイルで判断しない。作業ツリー・main・作業ブランチ・本番は簡単に4通りに食い違う。
4件に共通していたもの
並べてみると、どれも同じ構造をしている。
作業は正しく完了していて、報告も正確で、それでも目的は達成されていない。
AIは与えられた作業単位の中では非常に正確に動く。ファイルを直せと言えば直すし、テストを通せと言えば通す。問題はその作業単位の外側にあって、マージされたか、デプロイされたか、次のビルドで消えないか、確認方法は正しいか、といった部分は誰も見ていない。人間も見ていないから事故になる。
私が今のところ有効だと感じている対策は、報告の基準を「機構が動いたか」ではなく、作った理由が満たされたかどうかに変えることだった。テストが全部通ることと、そのテストを書いた動機が満たされていることは別の話だ。前者はAIが判定できるが、後者は本番を見に行かないと分からない。
言い換えると、完了報告に必要なのは「やりました」ではなく「外から見て変わっていることを確認しました」になる。この一手間を挟むだけで、上の4件のうち3件は起きなかったはずだ。
手を動かす前に決めておきたいこと
これから同じような構成でAIに開発を任せる人に、先に決めておくと安いと思うものを3つだけ挙げておく。
ひとつ、そのリポジトリの中に「ここは自動生成」と書いた地図を置くこと。ビルド成果物、コピーされたもの、手で書いた正本の区別は、ファイルを見ても分からない。
ふたつ、完了の定義に「本番で確認」を含めること。デプロイまでが1つの作業で、そこを別セッションに分けると高確率で落ちる。
みっつ、検証手段が正しいかを一度だけ疑うこと。差分が出たとき、まず疑うべきは対象ではなく物差しのほうだ。
どれも新しい話ではなく、チーム開発なら誰かが担っていた役割だと思う。AIとふたりで開発していると、その役割が空席のまま進む。空席だと気づくのに、私は1か月かかった。