homula

エージェンティック・ハーネス設計ガイド — Part 3

耐久実行と再開

落ちない設計ではなく、落ちても続きから戻れる設計へ。

20〜25分難易度 ★★★(中級〜上級)最終更新: 2026年8月

耐久実行(durable execution)とは、プロセスが落ちても処理の続きから再開できるように、実行の進行状況を外部に永続化しながら進める実行方式です。ステップ境界でチェックポイントを書き、完了したステップは二度と実行せず、未完了のものを検出して同じ ID で再開します。

Part 1で挙げた6つの構成要素のうち「記憶・状態」の後半、そしてPart 2で扱った「文脈をどう保つか」の外側にあるのが、この記事の主題です。

エージェント実装で最も見落とされやすいのが、ここです。デモは通るのに業務では使えない——その原因の多くは、実行が中断されたときに何が起きるかを決めていないことにあります。しかもこの領域は、AIの問題ではなく分散システムの問題です。だからこそ、すでに答えが出ている領域でもあります。

1.中断は例外ではなく、前提である

対話型AIの応答は数秒で終わります。その間に何かが起きる確率は、実質ゼロです。一方、自律型エージェントの実行は数分から数時間に及びます。

対話型 AI数秒この間に、ほとんど何も起きない自律型エージェント数分 〜 数時間その間に起きることデプロイ再起動ネットワーク断上流の障害人の承認待ち中断は例外ではなく、前提「落ちない設計」ではなく「落ちても続きから戻れる設計」にする実行が長くなるほど、途中で何かが起きる確率は 1 に近づく。
図1実行が長くなるほど、その間にデプロイ・再起動・ネットワーク断・上流の障害・人の承認待ちが起きる確率は 1 に近づく。「落ちない設計」を目指すのではなく「落ちても続きから戻れる設計」にする。

ここで重要なのは、これがAI固有の問題ではないという点です。長時間の処理を安全に完了させる方法は、分散システムの分野で長く議論され、実装のパターンが確立しています。エージェントのために新しい理論を発明する必要はなく、既存の答えを持ち込めばよい領域です。

2.4つの不変条件

文献が共通して処方する条件は、おおむね次の4つに収まります。個別の技術選定より先に、この4つが満たされているかを確認します。

文献が共通して処方する不変条件は、おおむねこの4つに収まる。① チェックポイントステップ境界で状態を書く完了したステップは二度と実行しない② 永続化された待機起床の条件を保存する承認待ちでプロセスを保持しない③ 進捗を運ぶ heartbeat生きている証明+どこまで進んだかリトライ後の試行が続きから始められる④ ID による再開同じ ID の仕事は一度だけ未完了を検出し、同じ入力で再開する4つは独立ではない。①がなければ④は成立せず、②がなければ長い承認待ちがそもそも作れない。
図2チェックポイント・永続化された待機・進捗を運ぶheartbeat・IDによる再開。4つは独立ではなく、①がなければ④は成立せず、②がなければ長い承認待ちがそもそも作れない。

3.① 流れは決定的に、ステップは冪等に

再開を成立させる土台は、2つの性質の分業です。全体の流れは決定的に、個々のステップは冪等に。この分業があって初めて、「どこまで終わったか」を照合して続きから実行できます。

全体の流れは「決定的」に同じ入力なら、同じ順序で同じステップを辿る。分岐に乱数や現在時刻を混ぜない。ステップ Aステップ Bステップ Cステップ D各ステップは「冪等」に同じステップを 2 回実行しても、結果が変わらない。送信も登録も、重複しない。1 回目実行される2 回目同じ結果を返すだけ3 回目同じ結果を返すだけこの 2 つが揃って初めて、「どこまで終わったか」を照合して安全に再開できる。
図3同じ入力なら同じ順序で同じステップを辿る(決定的)。同じステップを2回実行しても結果が変わらない(冪等)。分岐に乱数や現在時刻を混ぜると、再開時に別の道を辿ってしまう。

よくある落とし穴

流れの分岐に「現在時刻」や乱数を使うと、再開したときに前回と違う道を辿ります。記録には「ステップB完了」とあるのに、再開後の実装はステップCへ進もうとする——このズレが、最も追いにくい不具合になります。時刻や乱数が必要なら、それ自体をステップとして実行し、結果を記録に残します。

4.二重実行を、構造的に消す

冪等性の実装は、操作に鍵を割り当て、実行前に照会するという形が基本です。

操作「請求書 32 件を送信」鍵を決める操作の中身から一意に定まる値記録を照会この鍵は実行済みか?未実行実行済み実行する結果を鍵とともに保存実行しない保存済みの結果を返すだから、再試行しても重複しない再試行は「同じ鍵でもう一度呼ぶ」だけになり、送信・登録・課金が二重になる事故が構造的に消える。鍵は乱数ではなく、操作の中身から決まる値にする(同じ操作なら、いつ計算しても同じ鍵になること)「再試行してよいか」は、この仕組みがあるかどうかで決まる。無ければ、失敗しても再試行できない。
図4鍵は操作の中身から一意に定まる値にする。実行前に「この鍵は実行済みか」を照会し、実行済みなら保存済みの結果を返すだけにする。再試行が「同じ鍵でもう一度呼ぶ」になり、重複が構造的に消える。

鍵は乱数ではなく、操作の中身から決まる値にします。「いつ計算しても同じ鍵になる」ことが条件で、実行のたびに新しい鍵を振ってしまうと、再試行は常に新規実行として通ってしまいます。

この仕組みの有無が、そのまま「再試行してよいか」の答えになります。冪等でない操作は、失敗しても安全に再試行できません。エージェントに「失敗したらもう一度試して」と指示する前に、その操作が二重実行に耐えるかを確認する必要があります。

5.② 待機を永続化する — 承認でプロセスを握らない

人の承認は、数時間から数日かかります。この間プロセスを保持する実装は、2つの問題を抱えます。資源を握り続けることと、再起動が入ると待機ごと消えることです。

プロセスを保持して待つ実行承認待ち — プロセスは起きたまま数時間〜数日、資源を握り続ける再起動 → 待機ごと消失起床の条件を永続化して待つ実行起床条件を書いて終了「承認が来たら起こして」プロセスを解放資源を握らない承認が下りる数時間後でも、数日後でも新しいプロセスが再開続きから実行する承認待ちの間に再起動が入っても、待機そのものは失われない。長い承認は、これがないと作れない。
図5起床の条件を永続化してプロセスをいったん解放し、承認が下りた時点で新しいプロセスが続きから再開する。承認待ちの間に再起動が入っても、待機そのものは失われない。

この設計は、承認の話にとどまりません。「明日の朝まで待つ」「上流の処理が終わるまで待つ」といったあらゆる長い待機に同じ形が使えます。逆に言えば、この仕組みがないハーネスでは、長い承認フローも定時実行のような待ち合わせも安全には作れません。

あわせて設計しておきたいのが、承認待ちを実行時間の予算に算入しないことです。人を待っていた時間まで「実行時間の上限」に数えると、承認が遅れただけでタイムアウトします。

6.③ heartbeat は、生存だけでなく進捗を運ぶ

長時間の処理が生きているかを確認する仕組みが heartbeat です。多くの実装はここで打刻だけを送りますが、それでは失敗したときにゼロからやり直しになります。

打刻だけの heartbeat生きている生きている生きている生きている失敗 → ゼロからやり直し「動いていた」ことは分かるが、「どこまで進んだか」は残っていない。進捗を運ぶ heartbeat120 / 500 件260 / 500 件380 / 500 件440 / 500 件失敗 → 440 件目から再開次の試行は、前の試行が運んだ進捗を読める生存確認と進捗報告を、ひとつの仕組みにまとめる長い処理ほど、やり直しのコストは大きい。生存だけを送るのは、もったいない。
図6打刻だけでは「動いていた」ことしか分からない。進捗を一緒に運べば、次の試行が中断地点の続きから始められる。生存確認と進捗報告を、ひとつの仕組みにまとめる。

運ぶ進捗は、細かい内部状態である必要はありません。次の試行が続きを判断できる最小限——処理済みの件数、最後に成功した位置、残っている対象——で十分です。

7.「応答がない」を、1種類で扱わない

運用で最も判断に迷うのが、応答がないときに待つべきか、打ち切るべきかです。同じ症状の裏には、まったく違う2つの状態があります。

応答がない混んでいるだけ(busy)キューに積まれたまま、まだ始まっていない見るべきタイムアウトキュー投入 → 開始 の待ち時間取るべき対応 : 待つ/並列度を上げる死んでいる(dead)始まったが、進んでいない・落ちている見るべきタイムアウト1 回の試行の実行時間 / heartbeat の途絶取るべき対応 : 打ち切って再開する1 種類のタイムアウトで両方を扱うと、生きている仕事を殺すか、死んだ仕事を待ち続けるかになる。全体の上限(リトライ横断の総量)は、この 2 つとは別に持つ。
図7キューに積まれたまま始まっていない(混んでいるだけ)なら待つか並列度を上げる。始まったが進んでいない・落ちている(死んでいる)なら打ち切って再開する。見るべきタイムアウトが違う。

この2つに加えて、リトライを横断した全体の上限を別に持ちます。1回の試行が短くても、再試行を重ねれば全体としては際限なく延びます。「1回の試行の上限」と「全体の上限」は、別の目的を持つ別の設定です。

Part 1 との対応

Part 1 で示した「タイムアウトの4分類」——キュー待ち・1回の試行・リトライ横断の総量・heartbeat の途絶——は、この判断を実装に落とすための分類です。分類そのものが目的ではなく、「待つのか、殺すのか」を機械的に決められるようにすることが目的になります。

8.④ 再開は、外から起こす

最後の条件が、回復の起点をどこに置くかです。ここに落とし穴があります。

01未完了を検出する終わっていない仕事を、外から見つける02同じ ID で再開する新規実行ではなく、同じ仕事の続きとして03記録と照合するどのステップまで完了しているかを読む04続きから実行する完了済みは飛ばし、次のステップへ起点は「外からの定期チェック」に置く— 誰かが画面を開いたときに再開する設計は、誰も開かなければ永久に再開しないバージョンの取り扱い再開の途中でコードが変わると、記録と実装が食い違う。互換のあるバージョンだけが回復を引き受けるようにする。回復は「例外処理」ではなく、通常の実行経路のひとつとして設計する。
図8未完了を外から検出し、同じIDで再開し、記録と照合して完了済みを飛ばし、続きから実行する。起点を「誰かが画面を開いたとき」に置くと、誰も開かない仕事は永久に再開されない。

よくある実装は、クライアントの再訪に寄生した再開です。ユーザーが画面を開いたときに「未完了があれば再開する」処理が走る。これは動いているように見えますが、誰も開かなければ永久に再開されません。夜間に落ちたバッチは、翌朝誰かが気づくまで止まったままです。

起点は、外からの定期チェックに置きます。「未完了の仕事を探して起こす」処理を独立して走らせ、クライアントの有無に依存させない。これが能動的な回復です。

もうひとつ、実運用で効いてくるのがバージョンの取り扱いです。再開の途中でコードが変わると、記録と実装が食い違います。互換のあるバージョンだけが回復を引き受けるようにしておくと、デプロイのたびに古い仕事が壊れる事態を避けられます。

9.失敗の「層」を、つぶさずに返す

耐久実行の設計と対になるのが、失敗をどう伝えるかです。ここを雑に扱うと、上のすべてが台無しになります。

層をつぶして返す実行環境が落ちる「失敗しました」層の情報が消えるエージェントは「自分の出力が悪かった」と解釈する同じ生成をやり直し、また同じところで落ちる層を保ったまま返す実行環境が落ちるツール呼び出しの失敗どの層で何が起きたか回復可能な失敗として扱える待って再試行する/別の手段に切り替える最低限、この3つは区別する障害接続できない・落ちた再試行の対象空の結果正常に実行し、該当なし再試行しても変わらない拒否権限がない・禁止されている手段を変える/人に確認
図9実行環境の障害を「失敗しました」とだけ返すと、エージェントは自分の出力が悪かったと解釈して同じ生成をやり直す。ツール呼び出しの失敗として層を保って返せば、回復可能な失敗として扱える。

最低限、次の3つは区別して返します。

  • 障害 — 接続できない、落ちた。再試行の対象になる。
  • 空の結果 — 正常に実行し、該当がなかった。再試行しても結果は変わらない。
  • 拒否 — 権限がない、禁止されている。手段を変えるか、人に確認する。

このうち最も事故を生むのが、「障害」を「空の結果」として扱ってしまうパターンです。接続できなかっただけなのに「該当なし」として処理が進み、エージェントは何もなかったものとして次へ行く。表面上は正常に完了し、後から誰も気づけません

10.アンチパターン早見表

ここまでの内容を、実装で見かける形にして並べます。

よくある実装何が問題かどうするか
実行状態をプロセスのメモリだけに持つプロセスが死ぬと、進行中の作業が丸ごと消えるステップ境界で外部に書く
冪等キーを持たずに再試行する送信・登録・課金が二重になる操作の中身から鍵を決め、実行前に照会する
承認待ちの間、プロセスを保持する資源を握り続け、再起動で待機ごと消える起床条件を永続化して、いったん解放する
heartbeat が打刻だけ失敗するとゼロからやり直しになる進捗を一緒に運ばせる
タイムアウトが1種類しかない混雑と死亡を区別できないキュー待ちと試行時間を分けて持つ
誰かが画面を開いたときに再開する誰も開かなければ、永久に再開されない外からの定期チェックを起点にする
実行環境の障害を「失敗しました」で返すエージェントが自分の出力のせいだと解釈するどの層で何が起きたかを保って返す

Summary

この記事の要点

  1. 1中断は例外ではなく前提。「落ちない設計」ではなく「落ちても続きから戻れる設計」にする。
  2. 2これはAI固有の問題ではなく分散システムの問題。すでに答えが出ている領域から持ち込めばよい。
  3. 3全体の流れは決定的に、各ステップは冪等に。分岐に乱数や現在時刻を混ぜない。
  4. 4冪等キーは操作の中身から決める。乱数で振ると、再試行が常に新規実行として通ってしまう。
  5. 5承認待ちでプロセスを保持しない。起床条件を永続化して解放し、承認時に新しいプロセスが再開する。
  6. 6承認を待っていた時間を、実行時間の予算に算入しない。
  7. 7heartbeat には進捗を載せる。打刻だけでは、失敗するとゼロからやり直しになる。
  8. 8「応答がない」を1種類のタイムアウトで扱わない。混雑と死亡は、見るべき指標が違う。
  9. 9回復の起点は外からの定期チェックに置く。クライアントの再訪に寄生した再開は、誰も来なければ動かない。
  10. 10失敗は層を保って返す。とくに「障害」を「空の結果」として扱うと、誰も気づけない失敗になる。

FAQ

よくある質問

耐久実行とは、プロセスが落ちても処理の続きから再開できるように、実行の進行状況を外部に永続化しながら進める実行方式です。ステップ境界でチェックポイントを書き、完了したステップは二度と実行せず、未完了のものを検出して同じIDで再開します。AIエージェントの実行は数分から数時間に及ぶため、この方式が必要になります。

実行が長くなるほど、その間にデプロイ・再起動・ネットワーク断・上流の障害・人の承認待ちが起きる確率が上がるためです。数秒で終わる対話型AIでは無視できた事象が、数時間の実行では必ず起きます。「落ちない設計」を目指すのではなく「落ちても続きから戻れる設計」にするのが現実的です。

再試行を安全にするためです。同じステップを2回実行しても結果が変わらない(=冪等である)なら、失敗したときにそのまま再試行できます。冪等性がないと、再試行のたびにメールが二重送信されたり、登録が重複したりするため、そもそも安全に再試行できません。実装は、操作の中身から一意に定まる鍵を割り当て、実行前に「この鍵は実行済みか」を照会する形が基本です。

止めるべきです。承認は数時間から数日かかることがあり、その間プロセスを保持すると資源を握り続けるうえ、再起動が入ると待機ごと消えます。起床の条件を永続化してプロセスを解放し、承認が下りた時点で新しいプロセスが続きから再開する方式にします。長い承認フローは、この仕組みがないとそもそも作れません。

不十分です。打刻だけでは「動いていた」ことは分かっても「どこまで進んだか」が残らず、失敗するとゼロからやり直しになります。heartbeat に進捗を載せておけば、次の試行が中断地点の続きから始められます。長い処理ほど、やり直しのコストは大きくなります。

タイムアウトを1種類で扱わないことです。キューに積まれたまま始まっていない(混んでいるだけ)なら待つか並列度を上げる、始まったが進んでいない・落ちている(死んでいる)なら打ち切って再開する、という判断になります。この2つを区別できないと、生きている仕事を殺すか、死んだ仕事を待ち続けるかのどちらかになります。

Part 2

コンテキスト設計 — 捨てる・書き出す・畳む

有限の予算としての文脈管理。要約を最初の手段にしない設計。

Part 4 — 準備中

ツール設計とスキル — 読ませすぎない

実行可能な改善を返すエラー設計、名前空間、段階的開示の3層、決定性のスクリプト化。

実行基盤を、自社で持ちたい方へ

homulaは、AIエージェント実行基盤の要件定義から設計・構築・運用までを支援しています。耐久実行や再開の設計は、受入条件として先に定めておくべき領域です。