AIにコードを書かせると、だいたい動く。テストを書かせれば、だいたい通る。では、そのコードは半年後、別の担当者が安全に変更できるだろうか。
この問いに即答できないなら、見ているのは「生産性が上がった」という錯覚かもしれない。生産性が上がったのではなく、コストが後ろ倒しになっただけ、ということがある。しかも厄介なのは、ボトルネックの場所そのものが「書く」から「読む・直す」へ移っていることだ。書く速度がいくら上がっても、そこは速くならない。
「動く」と「直せる」は別の軸
ソフトウェアの価値は、書いた瞬間ではなく、変更され続ける期間の総和で決まる。ある時点で正しく動くコードでも、仕様変更・バグ修正・機能追加のたびに安全に手を入れられなければ、その都度コストがかかる。これを俗に「技術的負債」と呼ぶ。負債という比喩がうまいのは、借りた瞬間は得をしているように見えて、返済(あとからの手直し)のタイミングで利息付きの請求が来る、という時間差があるからだ。
AIがコードを書く場合、この時間差がより露骨になる。AIは「与えられた入力に対して正しい出力を返す」ことには強い。しかし「このコードは半年後の自分や他人にとって読みやすいか」「変更の影響範囲が追いやすいか」は、AIにとって最適化の対象になっていないことが多い。発注側がそれを明示的に求めない限り、AIは"動く"を満たした時点で手を止める。
具体例:一見正しい関数
次のような処理を頼んだとする。「注文リストから、キャンセルされていない注文の合計金額を計算して」。AIはこう返すかもしれない。
def calc(orders):
total = 0
for o in orders:
if o["status"] != "cancelled":
total += o["price"] * o.get("qty", 1)
return total
これは要件通りに動く。テストケースを2〜3個投げても通る。しかし、半年後にこの関数を変更する立場で読むと、いくつも引っかかる。
まず命名。calc は何を計算するのか、関数名からは分からない。呼び出し元を読まないと意図が追えない。次に責務の混在。「キャンセル除外」というビジネスルールと、「合計金額の算出」という計算ロジックが同じループの中に埋め込まれている。将来「返金済みの注文も除外したい」という要件が来たとき、このifの条件を書き換えるべきか、別の関数に分けるべきかの判断材料が、コードのどこにも残っていない。
さらに o.get("qty", 1) という一行。数量が未指定なら1件として扱う、という暗黙の前提がある。これは意図的な仕様なのか、単にAIが欠損値を適当に埋めた結果なのか、コードを読むだけでは区別できない。半年後に「数量なしの注文はエラーにすべきだった」と判明したとき、この一行が静かに紛れていたことに気づくまで時間がかかる。
動いている。テストも通る。けれど、変更の起点になる「なぜこうなっているか」という情報が、コードの外に出てしまっている。これが"保守できない"コードの典型的な形だ。バグではないのに、変更しようとした瞬間に事故る。
もう一つ、別の種類の例を挙げる。「外部APIを呼んで、失敗したら3回までリトライして」と頼むと、AIはこう書くことがある。
def fetch_with_retry(url):
for _ in range(3):
try:
return call_api(url)
except Exception:
continue
return None
要件通り動く。リトライも入っている。だが、ここでは「どんな例外でも握りつぶして次に進む」という判断が、コード中に静かに埋め込まれている。ネットワークのタイムアウトのようにリトライすべき失敗と、認証エラーのようにリトライしても無駄な失敗が、同じ except Exception で一括りにされている。呼び出し元は None が返ってきたとき、それが「リトライし尽くした末の失敗」なのか「別の理由で呼ばれなかった」のかを区別できない。これもテストは通る。通るテストが、品質を保証しているわけではないという、よくある落とし穴だ。
AIがこうなりやすいのは悪意や能力不足ではなく、最適化している対象が違うからだ。AIは「指示された入出力を満たす」ことに向けて出力を生成する。例外の分類や、将来の変更可能性を見越した設計判断は、指示されなければ最適化の対象に入らない。これは人間のジュニアエンジニアが陥る失敗とよく似ている。
だが一点だけ、AIの出力には人間のジュニアのコードと違う厄介さがある。量が多く、インデントも変数名も一見整っていて、コードレビューの目が思わず素通りしてしまう体裁をしていることだ。見た目の洗練度と、設計判断の浅さは、AI生成コードにおいてはしばしば一致しない。
律速が「書く」から「読む・変える」に移った
AI以前、開発の時間の多くは「書く」作業に割かれていた。だから書く速度が生産性のボトルネックだった。AIが実装を安く速く供給できるようになった今、律速は「そのコードを読んで、意図を理解し、安全に変更する」作業に移っている。コードレビューにかかる時間、仕様変更時に影響範囲を調べる時間、テストが仕様を表しているか確認する時間——これらは書く速度が上がっても短縮されない。むしろ、AIが生成するコード量そのものが増えれば、読む・レビューする・変更する対象が増え、この工程の負荷は上がる。
ここで非自明な転回が起きる。AIが実装を安く供給する時代に相対的に価値が上がるのは、「書く力」ではない。AIの出力に対して、命名・責務の分離・テスト・境界(インターフェースや前提条件)を与えて「読んで直せる形」に整える判断力だ。先の例で言えば、calc を calculate_active_order_total のような意図の読める名前に直し、キャンセル判定を別関数に切り出し、「数量未指定は1件とみなす」という暗黙の前提をコメントかテストケースとして明文化する——この一手間こそが、AIには委任しにくく、人に残る仕事になる。
受け入れ判断の基準を変える
AIが出したコードをそのままmergeするかどうかを、「動いたか」で判断するのはもう粗すぎる。代わりに問うべきは、「このコードは、今の自分以外の誰かが、半年後に安全に変更できる状態か」だ。この基準は特別なものではない。レビューで一度は聞いたことのある問いを、AI時代に意識的に当て直すだけだ。具体的には次の4点を見る。
- 関数名やクラス名が、処理の手順ではなく意図を表しているか。
- 1つの関数やクラスが、複数の責務(判定ロジックと計算、取得と加工など)を抱え込んでいないか。
- 境界値・エッジケース(0件・null・極端な値など、入力側に起因するケース)が、テストとして言語化されて残っているか。
- 暗黙の前提(例外の握りつぶし、デフォルト値の補完、副作用の有無など、実装側の判断に起因するもの)が、コードの外に漏れたまま放置されていないか。
これらに引っかかるなら、動作確認が通っていても、その場で直す。命名を変える、責務を分ける、見落としていたケースをテストに書き起こす——この一手間は数分で済むことが多いが、やらなければ半年後に何倍もの時間で取り返すことになる。AIに任せる範囲が広がるほど、この受け入れ基準を自分の中に持っているかどうかが、チームの保守コストを決める。
「書く力」はAIが代替しやすい。「読んで直せる形に整える判断」は、今のところ人に残っている。どちらに時間を投資するかは、もう選択の余地がある問いではなくなってきている。
設計や可読性にまつわる定番書籍が、実際にどれだけ読まれ・薦められているかはデータで確かめられる。気になる人は下記を覗いてみてほしい。