AIに書かせたコードでも、途中で失敗したときの後始末は自分の責任## そういえば、という話
AIに「注文を登録する処理を書いて」と頼むと、動くものがすぐ出てくる。
実際に動かすと通るし、コードも読みやすい。レビューも通る。
通らないのは、途中で失敗したときです。
注文のデータだけが残って、在庫が減っていない、みたいな状態がそのまま残る。
自分もこれをレビューで見落としたことがある。
コードを書いたのはAIだけれど、変なデータが残って困るのは自分と利用者なので、
AIに任せていい部分と、そうでない部分の線引きの話でもあると思っています。
以下は、実際にデータを壊してみたうえで、どこを見ればいいかを整理したものです。
前提:この話が要る処理と、要らない処理
最初に立場を書いておくと、すべての処理でこれを気にする必要はない。
要らないのは、更新が1つしかない処理です。
プロフィールの表示名を変える、フラグを1つ立てる、といったものは、
成功か失敗かのどちらかしかないので、中途半端な状態になりようがない。
効いてくるのは、2つ以上の更新がセットで成功してほしい場面です。
- 注文を作って、在庫を減らす
- 振替で、片方の残高を減らして、もう片方を増やす
- 申し込みを登録して、残り枠を1つ減らす
お金、在庫、残数、ポイント。数が合わなくなると後から復旧が面倒なものが並びます。
逆に、画面に表示するだけの読み取り処理には関係のない話です。
なぜ抜けるのか
理由は2つに整理できると思う。
ひとつめは、どこまでをひとまとまりにするかが、その業務でしか決まらないこと。
「注文と在庫はセット」「通知は後でもいい」という判断は、
コードを読んでも書いていないので、毎回考えることになる。
ふたつめは、検証のコストです。
普通に動かすと成功してしまうので、失敗したあとの状態は目に入らない。
失敗させるには、在庫を足りなくするなり、接続を切るなり、自分で仕込む必要がある。
AI側の理由もあって、こちらが頼んだのは「注文を登録する処理」なんですよね。
途中で失敗したときにどうするかは、聞かれなければ書かれない。
そして、聞かれないまま完成品が出てくるので、足りない前提に気づく機会もない。
新人だから抜ける、という話ではないと思っています。
出てくるのが速いほど、立ち止まる回数は減るので。
実際に壊してみる
在庫が3着しかない商品に、5着の注文が来た場面を作ります。
在庫がマイナスにならないよう、データベース側で qty >= 0 の制約をかけてあるので、
在庫を減らすところで失敗します。
よく出てくるのは、こういうコードです。
// 修正前:2つの更新を順番に実行するだけ
db.prepare('INSERT INTO orders (item, qty) VALUES (?, ?)').run(item, qty);
db.prepare('UPDATE stocks SET qty = qty - ? WHERE item = ?').run(qty, item);
これを動かすと、2行目で例外が飛びます。
ここで大事なのは、1行目は成功したままだということです。
失敗したあとのテーブルを覗くと、こうなっている。
orders: [{ id: 1, item: "T-shirt", qty: 5 }]
stocks: [{ item: "T-shirt", qty: 3 }]
5着の注文が残り、在庫は3着のまま減っていない。
画面にはエラーが出るので、利用者は「失敗した」と思って帰る。
でも注文のデータは残っているので、あとから出荷対象として拾われる可能性がある。
直し方は、2つの更新をひとまとまりとして扱うことです。
// 修正後:まとめて成功するか、まとめてなかったことにする
db.exec('BEGIN');
try {
db.prepare('INSERT INTO orders (item, qty) VALUES (?, ?)').run(item, qty);
db.prepare('UPDATE stocks SET qty = qty - ? WHERE item = ?').run(qty, item);
db.exec('COMMIT');
} catch (err) {
db.exec('ROLLBACK');
throw err;
}
BEGIN から COMMIT までが、ひとまとまりの範囲です。
途中で失敗したら ROLLBACK で、まとまりの中の更新が全部なかったことになる。
この「ひとまとまり」は、データベースの用語ではトランザクションと呼ばれるものです。
名前は知らなくても使えますが、調べるときはこの言葉で引くと早い。
同じ失敗をさせると、今度はこうなります。
orders: []
stocks: [{ item: "T-shirt", qty: 3 }]
注文も残らず、失敗する前の状態に戻っている。
上のコードは Node.js 標準の node:sqlite で動かして確認したものです。
使っているライブラリによって書き方は変わりますが、
「始めて、成功したら確定、失敗したら取り消す」という形は同じです。
手間が増える点もあって、まとまりの範囲を自分で決める必要がある。
ここを広く取りすぎると、今度は別の問題が出てきます。
どこまでをひとまとまりにするか
判断は、自分にこう聞くと早い。
片方だけ残ったら困るか。 困るなら同じまとまりに入れる。
注文と在庫は困るので、入れる。
アクセス履歴の記録は、残っても残らなくても業務は回るので、入れなくていい。
逆に、まとまりに入れてはいけないものもあります。
メールの送信、外部サービスへの通知、決済の確定のような、
自分の側で取り消せない処理です。
取り消せない処理をまとまりの中に入れると、
データベース側は取り消せても、送ってしまったメールは戻せないので、
「注文はなかったことになったのに、注文完了メールだけ届く」ことになる。
こういうものは、まとまりを確定させたあとに実行する。
もうひとつ、まとまりの中で時間のかかる処理を待たないほうがいい。
外部への問い合わせをまとまりの中に挟むと、
返事を待っている間ずっとデータを掴んだままになるので、
同じデータを触る他の処理が詰まります。
必要な情報は、まとまりを始める前に取っておく。
戻らない仕組み
一度直しても、次に似た処理を書けば同じことが起きる。
なので、わざと失敗させるテストを1本置いておくのがいいと思います。
test('在庫が足りずに失敗したとき、注文も残らない', () => {
const db = createDb(); // 在庫は3着
assert.throws(() => placeOrder(db, 'T-shirt', 5));
const orders = db.prepare('SELECT * FROM orders').all();
const stock = db.prepare('SELECT qty FROM stocks WHERE item = ?').get('T-shirt');
assert.deepStrictEqual(orders, []); // 注文は作られていない
assert.strictEqual(stock.qty, 3); // 在庫も元のまま
});
見ているのは「エラーになること」ではなく、エラーのあとのデータです。
ここを確認しておかないと、修正前のコードでもテストは緑になる。
例外は飛んでいるので。
失敗のさせ方は、データベースの制約に引っかける方法が手軽です。
在庫をマイナスにできない、同じ値を二重に登録できない、といった制約を先に付けておくと、
テストからも失敗を起こしやすいし、そもそも本番のデータも守れる。
このテスト自体はAIに書かせていい。
「この処理を途中で失敗させて、失敗後に両方のテーブルが元のままであることを確認するテストを書いて」
と頼めば出てきます。
AIへの投げ方
「注文登録の処理を書いて」だと、正常系のコードが返ってくる。
AIが手を抜いているわけではなくて、こちらが正常系しか聞いていないからです。
ひとこと足すなら、これがいいと思っています。
この処理、途中で失敗したらデータはどうなる?
セットで成功してほしい更新はどれ?
それをひとまとまりにして、途中で失敗させるテストも書いて。
最初の質問が効きます。
列挙はAIの得意な作業なので、聞けば「注文だけ残る」まで自分で書いてくる。
毎回聞くのが面倒なら、プロジェクトの指示ファイルに一行足しておけばいい。
複数の更新がセットで成功する必要がある処理は、ひとまとまりとして扱う。
取り消せない処理(メール送信、外部への通知)はそのまとまりの外に置く。
書かせたあとに、もう一度見てもらう
書かせるときの指示だけでなく、書けたあとにもう一往復するのもいいと思います。
最近のAIの開発ツールには、決まった手順を登録しておく仕組みがあります。
Claude Code のスキルのように、手順を1つのファイルに書いておけば、
毎回同じ観点で見てもらえる。
登録しておく観点は、たとえばこういうものです。
- この処理で、セットで成功してほしい更新はどれか
- ひとまとまりの範囲は、それと一致しているか
- 取り消せない処理が、まとまりの中に入っていないか
- 失敗したときのデータを確認するテストがあるか
コードを書いた直後は、自分も「動いたから大丈夫」になりやすい。
そこで一度、書いたものが想定した動きになっているかという目で見直すと、
囲み忘れのような抜けはだいたいここで見つかります。
判断そのものを預けるという話ではなくて、
自分が見落としやすい観点を、毎回同じように並べてもらう、という使い方です。
ここから先は自分で決めるしかない
まとまりの範囲は、機械では決まりません。
- 在庫は同時に減らしたいが、ポイント付与は後でもいいのか
- 通知が二重に飛ぶのと、飛ばないのと、どちらがまずいのか
- 失敗したとき、利用者に何と伝えて、次に何をしてもらうのか
このあたりは業務の都合なので、AIに聞いても答えは出ない。
逆に言うと、渡しさえすればコードは書いてもらえる。
渡すべき前提を持っているのは自分だけ、というのが今回の話です。
まとめ
- AIが書いたコードは正常系では落ちない。中途半端なデータが残るのは、途中で失敗したとき
- 2つ以上の更新がセットで成功してほしい処理では、ひとまとまりとして扱う
- 判断は「片方だけ残ったら困るか」。取り消せない処理はまとまりの外に置く
- 正常系のテストが緑でも意味がない。わざと失敗させて、失敗後のデータを見る
- 書かせたあとに、同じ観点でもう一度見てもらう手順を登録しておくと、見落としが減る
- コードを書いたのがAIでも、変なデータが残ったときに困るのは自分
まずは、最近AIに書かせた更新処理を1つ開いて、
「ここで落ちたらどうなる?」と声に出して読んでみるのがいいと思います。
更新が2つ並んでいる箇所を探すだけなら数分で終わるし、
そこが見つかれば、囲むところから先はAIに任せられます。
