Claude APIの設計と運用
Claudeの操作を再試行するとき:二重実行を防ぐ状態管理
返事が来なかったことは、相手が処理していない証拠ではありません。モデルが同じ操作を再度要求したり、アプリがタイムアウトを再送したりすると、通知や登録が重複する可能性があります。再試行の前に、操作の識別と状態を設計します。
編集・運営:株式会社ノースネスト · AI支援の学習解説
公開: · 更新: · 約4分(目安)
この記事の目的
通信の失敗と業務操作の失敗を区別し、再試行による重複を防げる。
こんな方へ
ツール経由で登録や更新を実行するアプリの開発者。
一つの業務要求に一つの操作IDを付ける
再試行ごとに新しいIDを作ると、相手から見れば毎回別の操作になります。同じ意図の再送は同じキーで識別し、入力内容も照合します。同じキーで違う内容が来た場合は、過去の結果を無条件に返すのではなく不整合として扱います。
操作IDは、モデルが文章の中で覚えていることに依存させず、アプリが保持します。キーを付けただけでは重複を防げないため、保存先や実行先でその契約を実装する必要があります。
不明な状態を持てるようにする
未開始、処理中、成功、失敗に加え、結果確認が必要な状態を考えます。通信が切れた場合は、既存の操作IDで実行先へ結果を照会できると復旧しやすくなります。確認できない状態を失敗と決めつけて再送しないことが重要です。
外部サービスが重複防止を提供しない場合は、操作の範囲を見直す、手動確認へ回すなど別の手段が必要です。画面のボタンを無効にするだけでは、サーバー側の再試行や並行要求を防げません。
再試行の対象を小さくする
モデルへの再要求と、ツール実行の再送は別です。文章生成をやり直すために、完了済みの業務操作まで再実行しないようにします。実行済みの結果を履歴へ戻し、その情報を使って最終説明だけを再開する方法を検討します。
重複防止の保存と外部操作の間に注目する
アプリで「実行済み」と保存する前に外部操作が成功し、その直後に停止すると、記録には成功が残らないことがあります。逆に先に成功を保存してから外部操作に失敗すると、未実行なのに完了扱いになります。保存先と実行先の境界を確認し、外部サービスの重複防止キーや状態照会を使えるかを調べます。キーを一つ付ければ常に一度だけ実行できる、という保証にはなりません。
実行の仕組みが一体化できない場合は、処理中の記録と復旧手順を用意します。どの状態なら再送でき、どの状態なら担当者の確認が必要かを決めます。失敗したときの説明へ「登録されていません」と断定する前に、実行先での結果を確かめる必要があります。二重実行を防ぐ設計と、利用者へ正しい状態を伝える設計を一緒に確認します。
再現例で判断の境界を確認する
同じ架空要求をPromise.allで同時に渡し、処理回数が一回になることと、同じキーで別内容を渡すと拒否することを確認します。
台帳は一つのプロセスのメモリ内だけです。再起動や複数サーバーでは共有されず、実運用には永続的な一意制約、結果保存、部分成功と再開の設計が必要です。決済や注文の実行コードではありません。
具体例で考える
架空の予約登録を送信した後、応答を受け取る前に通信が切れました。
再開時:操作ID booking-request-17 の状態を確認。
登録済み:既存の予約番号を返す。
未実行:同じIDで実行。
確認不能:自動再登録せず要確認へ。新しい予約要求として再送しない点が中心です。この例のIDは説明用で、実際には衝突しない識別方法と保存期間を、業務の再送可能期間に合わせて定めます。
ローカルで再現する:同時再送とキーの衝突を区別する
同じ架空要求をPromise.allで同時に渡し、処理回数が一回になることと、同じキーで別内容を渡すと拒否することを確認します。
以下は公開コードを実行して得た結果です。入力は学習用に作った架空データで、Claude API・外部サービスへの通信は行っていません。モデルの回答品質や実サービスの性能を測定した結果ではありません。
実行:
Node.js v24.22.1 · darwin/arm64 · 2ケース
再現の手順
- 実行コードを保存する(.mjs)
- Node.js 20以上の環境で、保存したフォルダから次のコマンドを実行します。追加パッケージやAPIキーは不要です。
- 出力の各ケースを読み、入力を一つ変えて判断の違いを確かめてください。
node claude-idempotent-actions.mjs実際の入力と出力
{
"cases": [
{
"name": "同じ要求を同時に二回",
"input": {
"key": "request-1",
"payload": {
"orderId": "sample-order",
"amount": 1500
},
"calls": 2
},
"expectedEffects": 1,
"actual": {
"effects": 1,
"results": [
{
"operationId": "local-1",
"orderId": "sample-order"
},
{
"operationId": "local-1",
"orderId": "sample-order"
}
]
},
"pass": true
},
{
"name": "同じキーで内容を変更",
"input": {
"key": "request-1",
"amount": 1700
},
"expected": "key_conflict",
"actual": "key_conflict",
"pass": true
}
]
}実行したコードを読む
// In-memory concurrency experiment only. Not a durable payment/order service.
export function createLedger() {
const entries=new Map();let effects=0;
return {get effects(){return effects;},
execute(key,payload) {
const fingerprint=JSON.stringify([payload.orderId,payload.amount]);
const previous=entries.get(key);
if(previous) {
if(previous.fingerprint!==fingerprint)return Promise.reject(new Error('key_conflict'));
return previous.result;
}
// Register the promise before its asynchronous work can begin.
const result=Promise.resolve().then(()=>({operationId:`local-${++effects}`,orderId:payload.orderId}));
entries.set(key,{fingerprint,result});return result;
}
};
}
export async function run() {
const ledger=createLedger(),payload={orderId:'sample-order',amount:1500};
const results=await Promise.all([ledger.execute('request-1',payload),ledger.execute('request-1',payload)]);
const duplicate={name:'同じ要求を同時に二回',input:{key:'request-1',payload,calls:2},expectedEffects:1,actual:{effects:ledger.effects,results},pass:ledger.effects===1&&results[0].operationId===results[1].operationId};
let conflict;try{await ledger.execute('request-1',{...payload,amount:1700});conflict='accepted';}catch(e){conflict=e.message;}
return {cases:[duplicate,{name:'同じキーで内容を変更',input:{key:'request-1',amount:1700},expected:'key_conflict',actual:conflict,pass:conflict==='key_conflict'&&ledger.effects===1}]};
}
if(process.argv[1]&&import.meta.url===new URL('file://'+process.argv[1]).href)console.log(JSON.stringify(await run(),null,2));
コードのSHA-256:3807093875a149d5589ca2309bd185a03e242330c9a8eb11088e400f7c5fc4fa
この検証で分かる範囲
台帳は一つのプロセスのメモリ内だけです。再起動や複数サーバーでは共有されず、実運用には永続的な一意制約、結果保存、部分成功と再開の設計が必要です。決済や注文の実行コードではありません。
確認課題と合格の目安
操作成功後に応答だけが失われるケースを含め、再試行時の状態遷移を書いてください。
- 同じ業務意図を同じ操作IDへ結び付ける。
- 結果不明を正常な失敗と混同しない。
- 生成の再試行で完了済みの操作を繰り返さない。