0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AIに書かせたコードでも、途中で失敗したときの後始末は自分の責任

0
Posted at

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 }]

注文も残らず、失敗する前の状態に戻っている。

在庫3着に5着の注文を入れて失敗した直後のテーブル比較図。囲まない場合は注文テーブルに5着の行が残り在庫は3着のまま。ひとまとまりにした場合は注文テーブルが空で、在庫も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に任せられます。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?