動くものは、たしかにできる。
コードを一行も書いたことのない人でも、指示を出せば、入力欄と一覧画面のある業務ツールが半日で形になる。
ここで言うコーディングエージェントは、指示を受けてコードを書き、実行し、失敗すれば自分で直すところまで進む道具を指す。
以下では、これを「エージェント」と呼ぶ。
コードをほとんど読まずに、生成されたものをそのまま動かして進める作り方は「vibe coding」と呼ばれる。2025年に広まった呼び名で、もともとは個人的な試作を指す軽い意味で使われていた。
問題は、そうやって作られたものが半年後にどんな状態になっているかである。
並べるのは、その時点で保守を引き継いだ側が実際に開くことになるものである。
勘違いしないで欲しいのは私はAIを使うなとは言っていない。
足で走るより自転車のほうが良い。
車のほうが速いし、ジェット機なら最高に速いだろう。
AIはF1マシンみたいなものだ。
手作業でコーディングしていた怠い作業にはもう戻れないだろうし、戻りたくはない。
ただ、間違えてはいけないのは、運転手は人間で、F1マシンに乗ったことがないという事だ。
なんなら車の運転どころか原付に跨った事もない人間でも、アクセルを踏むと進み出す。
そんなF1マシンが与えられたのが現在である。
ここに羅列した内容は、はっきり言って大袈裟かもしれない。
ただ、いくつかの内容については私自身が直面して、オイオイふざけんなよと言って何度も追加で指示をしなおした内容でもある。
三千行のファイルと、名前だけ違う四つの関数
最初に開くファイルが三千行ある。
画面の組み立て、料金の計算、データベースへの問い合わせ、外部サービスの呼び出しが、上から順に混ざって並んでいる。
その中に、同じ処理が名前を変えて何度も現れる。
def calculate_total(items): ...
def calculate_total_new(items): ...
def calculate_total_v2(items): ...
def calculate_total_fixed(items): ...四つとも動く。
実際に呼ばれているのは一つで、残りは「直して」と頼んだ回数だけ積み上がった層である。
どれを消してよいかを決めるには、四つの中身を突き合わせて、呼び出し元を全部たどるしかない。
その周りに、コメントアウトされた古い実装が数百行ある。
戻せるようにという理由で残されているが、バージョン管理を使っていれば一行も要らないものである。
三千行の一枚板も、四つに増えた同じ関数も、消えないコメントアウトも、原因は一つである。
依頼は、機能を足すか、出た不具合を直すかのどちらかの形でしか出されていない。
エージェントは頼まれたことをやり、頼まれていないことはやらない。
つまり、リファクタリングを誰も指示していないのである。
握りつぶされた例外
エラーが出るたびに「エラーが出ないようにして」と頼み続けた跡が、コードの各所に残っている。
try:
total = calculate_total_fixed(items)
except:
total = 0これが二十箇所ある。
エラーは出なくなった。
出なくなっただけで、失敗そのものは毎日起きている。
利用者の側から見えるのは、金額欄のゼロと、いつの間にか件数の減った一覧である。
どの行が落ちたのかを知る手段はない。
記録を残す処理が書かれていないか、画面に流れる文字として出ていて誰も見ていないかのどちらかである。
このコードを書いたのはエージェントだが、こう書くしかなかったのも確かである。
値が取れなかった一行を、集計から外すのか、ゼロとして扱うのか、そこで処理を止めて担当者に知らせるのか。
どれを選ぶかは業務の側で決まることで、コードを読んで決められる話ではない。
選べない相手に選ばせれば、その場でいちばん静かな選択肢が選ばれる。
例外が出たときにどうしてほしいかを、指示しただろうか。
グリーンになるテストと、動かない手順書
テストを置くディレクトリはある。
中身は、実装を読んでそのまま写したものである。
偽物の部品で本体を覆ってあって、確かめたい計算を一行も通らないまま通過するテストも混ざっている。
それでも全部グリーンになるので、テストがあるという事実だけが引き継ぎ資料に書かれる。
手順書のほうは立派である。
構成の説明、導入手順、開発への参加方法まで書いてある。
書いてある手順では動かない。
存在しない設定ファイルを参照していて、記載されたディレクトリ構成は三回前の書き換え時点のものである。
本文の量が多いぶん、それが実態と合っていないことに気付くまでに半日かかる。
推測で書かれた外部連携
決済や配送や地図といった外部サービスにつなぐ箇所も、同じ調子で書かれている。
公開されている仕様書は、誰でも読める場所にある。
その仕様書を参照した形跡はない。
書かれているコードは、それらしい形をしている。
送る項目の名前も、返ってくる値の取り出し方も、いかにもありそうな綴りになっている。
実在するかどうかは別である。
動かすと相手側から拒否され、エージェントは返ってきたメッセージに合わせて名前を直し、また試す。
これを何度か繰り返すと、いつかは通る。
通ったからといって、仕様どおりに使えているとは限らない。
省略した項目にどの既定値が入るのか、一度に取れる件数の上限がいくつなのか、金額の単位が円なのか最小単位なのか、日付をどの形式で解釈されるのか。
どれも、自分で試した一件では違いが出ない。
効いてくるのは、相手が失敗を返したときである。
時間をおけば成功する一時的な失敗と、何度送っても通らない恒久的な失敗を、仕様書は区別して定義している。
参照せずに書かれたコードは、その区別を持たない。
送り直してはいけない依頼が送り直され、送り直すべき依頼はそのまま捨てられる。
こうなるのは、仕様を確認してから書けと言われていないからである。
手元にある知識でそれらしく書けば数秒で返せるが、仕様書を読みに行けば時間がかかる。
言われていないなら、手早く返すほうが優先される。
そして、学習した時点の古い仕様が混ざっていても、書いた側にそれを疑う理由はない。
データベース代わりのスプレッドシート
料金表と取引先の一覧は、共有のスプレッドシートに置かれている。
アプリケーションは、そのシートを外部サービスとして読み書きしている。
元は担当者が手で管理していた表で、自動化を頼んだときに、その置き場所がそのまま使われた。
置き場所を選び直さなかったことには理由がある。
表はすでにそこにあり、追加の準備が要らない。
リレーショナルデータベース(RDBMS)を使うなら、どこに置くか、費用を誰が持つか、誰が管理するかを人間の側で決める必要がある。
その決定を待たずに済ませられるほうを選べば、最短で動くものが出せる。
選定について何も言われていないのだから、手早く動くほうが選ばれる。
読み書きするだけなら、これで動く。
表として使うのをやめて、台帳として使い始めた時点から、次のことが起き始める。
- 型がない:伝票番号の先頭のゼロが消え、「1-2」と入れた区分が日付に変換される。入れた文字列と読み出す値が一致しない。
- 制約がない:同じ取引先が二行に増え、片方だけ料金が更新される。一意であることも、必須であることも、他の表と対応が取れていることも、書き込む側が気を付けるしかない。
- やり直せない:二つのシートにまたがる更新が途中で失敗すると、片方だけ書かれた状態で残る。まとめて取り消す仕組みがないので、手で戻すことになる。
- 同時に書ける:アプリが書き込んでいる最中に、担当者が同じ行を手で直す。行を一つ挿入されれば、行番号を指定して書き込んでいた処理は、以後ずっと隣の行を書き換える。
- 探すのが遅い:目当ての一行を見つけるために、全部を取り出して端から調べることになる。索引がないので、件数に比例して遅くなる。
- 上限がある:一つのシートに置けるセルの数には上限がある。増え続ける記録を書き込んでいれば、ある日、書き込みが通らなくなる。
- 権限が粗い:見せてよい列と見せたくない列が同じシートに並んでいる。共有の設定は表の単位でしかかけられない。
- 誰が変えたか分からない:アプリからの書き込みはすべて同じアカウントとして記録される。担当者が手で直したのか、処理が書いたのかを、後から区別できない。
台帳に必要なのは、型と制約とやり直しの仕組みであり、それらを備えているのが関係データベースである。
書き込みの量が多く、入れる項目が定まらない用途なら、NoSQLのほうが向く場合もある。
どちらを選ぶにしても、判断の材料は今日の使い勝手ではない。
件数がどこまで増えるか、同時に触る人が何人いるか、失ったときに何が止まるか、人が手で編集し続けるのか。
この四つに答えれば、スプレッドシートで足りる範囲は、はっきり狭い。
そして、その四つは誰にも聞かれていない。
スプレッドシートに書き出してと言っておいたら、スプレッドシートを劣化したデータベース代わりに使い始めたのだ。
だって、データベースを使えとは、誰も言っていないのである。
Webhookの受け口に組まれたECS
構成の話も同じである。
外部サービスからの通知を受け取って、内容を一行書き込む。
やることはそれだけで、処理そのものは数十行しかない。
出来上がっていたのは、専用のVPCと、パブリックとプライベートに分けたサブネットと、その境目に置かれたNAT Gatewayと、前段のロードバランサと、常時起動しているECSのタスクである。
どれも、教科書に載っている構成としては正しい。
そして、通知が一件も来ない月にも、時間あたりで課金される部品が三つ並んでいる。
請求書を見て初めて、受け口一つに月あたり一万円前後が出ていることに気付く。
同じものは、関数を一つ置くだけでも作れる。
呼ばれた回数だけ課金される作りにすれば、月に数千件の通知なら、費用は無料の範囲に収まることが多い。
可用性の面でも、この用途では見劣りしない。
こうなる理由は、依頼の文面にある。
「本番で使える構成にして」と頼めば、可用性と拡張性を優先した構成が出てくる。
広く参照される設計例は、大きな規模で運用する前提で書かれていて、費用と運用の手間は読み手の側の条件として外に置かれている。
その条件を渡さなければ、条件のない状態で無難なほうが選ばれる。
費用のほかに、運用の手間も付いてくる。
コンテナを載せる基盤には更新の期限があり、証明書には期限があり、ネットワークの設計には理解が要る。
専任の担当者がいる前提の構成が、担当者のいない場所に残る。
そして、一度立った構成は減らせない。
どれが要るものでどれが要らないものかを、誰も説明できないからである。
使う人数も、来る件数も、出せる費用も、誰も指示していないのである。
画面に現れない条件
ここから先は、コードの見た目の問題ではない。
作ったものには、何ができるかとは別に、どういう状態で動き続けるかという条件が付いてくる。表示にかかる時間、同時に使われたときの挙動、失敗した処理をやり直せるか、壊れたときに戻せるか、誰がどのデータを見られるか。
これらをまとめて非機能要件と呼ぶ。
この条件と並んで抜けるのが、想定どおりに進まなかったときの処理である。
うまくいく道筋を正常系、そこから外れたときの道筋を異常系と呼ぶ。
正常系は一本道で、画面に見えているのもそれだけである。
異常系のほうは、外れうる箇所の数だけ枝がある。
入力が想定と違う、外部サービスが応答しない、途中で通信が切れる、同じ要求が二度届く、権限のない人が触る、件数が想定を超える。
枝の一本ずつに、どうするかという決定が要る。
業務で使うものになると、この異常系と非機能要件のために書かれるコードが、正常系より多くなることは珍しくない。
依頼した側から見えているのは正常系だけなので、その量は最後まで見えない。
エージェントは、頼めばこの種の処理を正しく書く。
頼まれなければ書かない。
依頼の文面に現れていない枝は、依頼を完了させるうえで書く理由がなく、正常系だけなら短く書けて、実行すれば動いて見えるからである。
そして、書かなかったという事実は完成の報告にも画面にも現れない。
機能が足りなければ開いた画面で分かるが、この種の不足は、気付く手がかりが最初から画面の外にある。
指示しなければ書かれない部分のほうが、量としては本体である。
ここまでとこれから並べる「誰も指示していない」は、小さな抜けが積み重なったものではない。
作られるはずだったものの、大半にあたる。
半年のあいだ何も起きなかったのは、条件が満たされていたからではない。
条件が試される状況が、まだ来ていなかっただけである。
自前で書かれたログイン
ログインの仕組みが自前で書かれている。
パスワードがそのままの文字列で保存されているか、二十年前に使われていた方式で変換されて保存されている。
利用者を識別する値がそのまま端末側に置かれていて、その数字を書き換えれば別人として通る。
画面に出ている「管理者のみ」の制限は、表示を出し分ける分岐として書かれているだけである。裏側の入り口を直接叩けば、誰でも同じ処理が実行できる。
検索欄に入れた文字は、組み立て中の問い合わせ文にそのまま連結されている。
社内に閉じているあいだ、この状態はまず露見しない。
取引先に開放する日が、最初の点検の日になる。
平文で保管された秘密
保管されているもののほうも、同じ状態である。
外部サービスの鍵は、設定ファイルに文字列として書かれている。
そのファイルはリポジトリに入っているので、履歴をたどれば、途中で差し替えた古い鍵まで全部読める。
取引先の口座情報も、利用者の連絡先も、データベースの列にそのまま入っている。
取り出す側で伏せる処理は書かれていないから、一覧を一度出せば全部見える。
記録にも同じものが出ている。
外部への呼び出しの中身をそのまま書き出す処理が入っていて、認証に使う文字列と送信先のアドレスが並んで残っている。
その記録は、不具合を切り分けるために誰でも読める場所に置かれ、そのままになっている。
書き出したバックアップの一式も、共有フォルダに中身のまま置かれている。
社内で使うものだからという理由で、通信も暗号化されていない。
秘匿すべき情報は安全に保管されているか。
No 平文で保管される。
暗号化した保管も、鍵を設定の外から読み込む書き方も、頼めばエージェントはそのまま書く。
平文で置いても暗号化して置いても、画面の見え方は一文字も変わらない。
つまり、暗号化しろとは、誰も言っていないのである。
三十秒かかる一覧画面
一覧画面が、テーブルの全件を取り出してから、アプリケーションの側で絞り込んでいる。
百件のテストデータでは速い。
運用一年目で三万件になったところで、開くたびに三十秒かかるようになる。
明細を表示するために、行ごとに問い合わせが走っている。
五十行を表示するのに五十一回である。
金額が小数の型で保持されていて、合計が一円ずれる。
日付が文字列として比較されていて、年をまたいだ並び順が壊れる。
どれも、動かした本人の手元では再現しない。
本人が試すのは、自分が今日入れた一件だけだからである。
送信ボタンを二度押すと二件入る
通信が遅い日に利用者が送信ボタンを押し直すと、同じ注文が二件入る。
待たされたら押し直すのは自然な動作なので、これは月に何度も起きる。
中身の同じ二件を、運用の担当者が毎回手で消している。
やり直しても結果が変わらない性質を冪等性という。
月末の集計処理が途中で失敗した日に、この性質があるかどうかが効いてくる。
最初から流し直すと、前半で書き込んだぶんが二重に計上される。
途中から流す手段はない。
どこまで進んだかを残す処理が書かれていないからである。
残る復旧手段は、データベースを直接書き換えることだけになる。
外部への支払い依頼や通知の送信でも同じことが起きる。
「失敗したらやり直す」処理はエージェントに書き足されているが、二度目の呼び出しを一度目と同じものとして扱う仕組みは書かれていない。
やり直しのたびに、通知は増え、請求も増える。
裏に回された処理
外部サービスの呼び出しに、待ち時間の上限が決められていない。
相手が応答しない日には、画面が開いたまま数分固まる。
そのあいだ、同じサーバーを使っている他の利用者も待たされる。
重いという苦情を伝えると、エージェントは処理を裏に回す。
報告には「非同期にしました」と書かれる。
実際に書かれているのは、別の流れで走らせて、その結果を受け取らない処理である。
成功したのか失敗したのかを確かめる側がないので、失敗しても画面には何も出ない。
頼んだ集計が出てこないという苦情が、翌週になって別の形で上がってくる。
順番待ちの列は、動いているプロセスの中に変数として持たれている。
再起動すれば、待っていた処理はまとめて消える。
何が消えたのかを知る方法はない。
書かれていないのは、待ち合わせの処理も同じである。
同時に走る二つの処理が何を壊すのかは、現金自動預払機を例にすると短く言える。
残高十万円の口座から、二台の機械で同時に五万円ずつ引き出す。
引き出しの処理は、残高を読む、足りるかどうかを確かめる、引いた額を書き戻す、という三段でできている。
二つの処理がこの三段を挟み込む形で重なると、次のように進む。
機械A: 残高を読む 100,000
機械B: 残高を読む 100,000
機械A: 足りる。50,000 を引いて書き戻す 50,000
機械B: 足りる。50,000 を引いて書き戻す 50,000出てきた現金は十万円で、記録に残った残高は五万円である。
どちらの処理も、単独で見れば手順どおりに動いている。
狂っているのは、二台目が読んだ十万円が、読んだ時点ですでに古い値だったことのほうである。
これを防ぐのが排他制御で、片方が残高に触れているあいだ、もう片方を待たせる。
書き戻すときに「読んだときの残高が今も十万円であること」を条件として付け、合わなければやり直す方法もある。
現金自動預払機がこの通りに壊れないのは、そこまで作り込まれているからである。
引き継いだツールで起きているのは、同じ形の事故である。
残り一個の在庫が二人に売れ、採番した伝票番号が重複し、月次処理を二人が同時に押した日には集計結果が混ざる。
残るのは数字が合わないという報告だけで、操作した時刻の記録がないので、その日に何が重なったのかは分からずじまいになる。
待たせる処理も、条件を付けた書き戻しも、頼めばエージェントは正しく書く。
頼まなければ、読んで、確かめて、書き戻すという素直な三行が書かれる。
一人ずつ順番に使うかぎり、その三行は正しく動いてしまう。
そして、排他制御を行えとは、誰も指示していないのである。
手でデプロイされる本番
デプロイの手順は、ファイルをサーバーに上書きすることである。
テストは、気が向いたときに手元で走らせる。
自動で実行する設定も一度は置かれたが、途中から失敗するようになった時点で止められている。
グリーンに戻す作業のほうが、機能を足すより時間がかかるからである。
デプロイの記録が残らないので、いつ何が変わったのかを後から追えない。
不具合が出たときに、直前の変更に当たりを付けるという調べ方ができない。
戻す手段もない。
上書きされた前のファイルは、どこにも残っていない。
急ぎの修正は、本番のサーバーの上で直接編集される。
その変更は手元のコードに入っていないので、次に上書きしたときに消える。
消えたことに気付くのは、同じ不具合が再発したときである。
再現できない環境
必要な部品を書いた一覧がない。
動いているサーバーに入っているものが仕様であり、それがどのバージョンなのかを調べるところから始まる。
データベースの表がどう作られたのかを示す記録もない。
画面から手で作った表がそのまま本番のものになっている。
バックアップは設定されていない。
使われなくなった表と、今も書き込まれている表が、名前だけで見分けられる状態で並んでいる。
止まったときに起動し直す手順は、作った本人の手元にしかない。
その本人は、もう別の部署にいる。
止まったことに誰も気付かない
動かなくなったことを知らせる仕組みがない。
最初に気付くのは利用者で、連絡は「なんか使えないんですけど」という形で届く。
そこから、いつから止まっていたのかを調べることになる。
記録の置き場所も決まっていない。
出力がディスクに溜まり続け、いっぱいになって書き込みが止まっていたと後から分かる。
逆に、調べたい日の記録がすでに消えていることもある。
何をどれだけ残すかを、誰も決めていないからである。
外部サービスの鍵には期限がある。
更新の当番も、期限を知らせる仕組みもないので、切れた朝に全部止まる。
呼び出し回数に上限を置いていないので、利用が伸びた月に請求が跳ねる。
誰がどの権限を持っているかの一覧もなく、辞めた人のアカウントが有効なまま残っている。
運用設計と呼ばれる作業の中身は、動かなくなったときに誰が何をするかを、動かす前に決めておくことである。
決めていなければ、決まるのは事故の当日であり、決めるのはそのとき居合わせた人になる。
残り一割の中身
引き継ぎの前、作った本人の申告はたいてい「九割できている」である。
機能の数で数えれば、その申告は正しい。
残りの一割に入っているのは、月末の締め処理、権限の分離、二人目が同時に使ったときの挙動、失敗した処理のやり直し、止まったときの検知と復旧、そしてここまでに並べたもの全部である。
引き継いだ側が最初に出す見積もりは、「作り直したほうが早い」になりやすい。
一つ直すたびに別の箇所が壊れるので、変更にかかる時間が読めないからである。
そして作り直しの承認は下りない。
動いているものに金を出す理由を説明するのが難しく、止まっていない以上、優先順位は上がらない。
引き継ぎのあとに残るもの
嘆きたくなるのは、コードが汚いからではない。
読みにくいコードなら昔からいくらでもあった。
違うのは、書いた本人が中身を説明できないことである。
なぜその作りになっているのかを知っている人間が、最初から一人もいない。
判断した主体は依頼のたびに立ち上がっては消えた道具であり、判断の理由はどこにも残っていない。
残っているのは、動いている成果物と、それを止められないという事実だけである。
何が言いたいかというと
頭を抱える問題の山が出来ていつ埋もれていつか爆発する前にきちんと設計をしよう。
技術選定をしよう。
非機能を考えよう。
運用を思い描こう。
非機能と運用設計は済ませたか。CI/CD設計は。
運用でカバーと言われて、ガタガタ震えて命乞いする心の準備はOK?