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?

【2026年版】なぜAIエージェントは本番環境でクラッシュするのか:Durable Execution(Temporal vs Restate vs Inngest vs LangGraph)による耐障害性と長時間ワークフロー構築実践

0
Posted at

2026年、最新のLLM SDKを使えばAIエージェントのプロトタイプはわずか30分で構築できます。しかし、それをエンタープライズの本番環境で安定稼働させようとした瞬間、開発チームの85%が**「複雑性の崖(Complexity Cliff)」に直面します。単一ターンのチャットボットから、数分・数時間・数日間に及ぶ自律型マルチステップ・ワークフローへと進化したエージェントは、従来のインメモリ実行基盤では容易に破綻します。コンテナのOOM再起動によるコンテキスト全損、ネットワーク瞬断時の非べき等リトライによるクレジットカード二重課金、そして数日間に及ぶHuman-in-the-Loop(HITL)承認待ちによるスレッドプール枯渇――これらを解決する2026年の業界標準アーキテクチャがDurable Execution(永続実行・確定リプレイ)**です。本稿では、イベントソーシング・ジャーナル、べき等ツール呼び出しの境界設計、主要エンジン(Temporal、Restate、Inngest、LangGraph Checkpointer)の徹底比較、および本番対応のPython実装コードを詳解します。


目次

  1. 要約とアーキテクチャ境界条件
  2. 複雑性の崖:インメモリ実行の3大障害モード
  3. Durable Executionの基本構成要素
  4. エンジン徹底比較:Temporal vs Restate vs Inngest vs LangGraph
  5. 本番アーキテクチャ:べき等ツールゲートウェイ
  6. Pythonによる本番Durable Agent実装
  7. 確定的リプレイの鉄則とアンチパターン
  8. エンタープライズコスト、レイテンシSLOとストレージ経済学
  9. アーキテクチャ選定指針と関連ツール
  10. よくある質問(FAQ)

1. 要約とアーキテクチャ境界条件 {#quick-summary-architectural-boundaries}

  • セッションメモリはDurable Executionではない:RedisやPostgreSQLに対話履歴(messages: [...])を保存するのは対話の文脈復元に過ぎません。これでは実行状態を守れません。全11ステップのマイグレーション中、ステップ7でプロセスがクラッシュした場合、メモリはアクティブなコールスタックや実行途中のツールプロミスを復元できません。
  • 透過的なクラッシュリカバリ:エージェントのホストが死んだ(OOM Kill、K8s Spot回収、デプロイ切り替え)場合でも、新しいワーカー上で中断された正確なコード行から再開され、完了済みの副作用は二度と再実行されません。
  • 外部副作用の厳密なべき等性:外部ツール呼び出し(決済、メール送信、DB更新)は、ワーカーのリトライやネットワーク切断に関わらず、絶対に2回以上実行されてはなりません。
  • 非ブロッキングな長時間停止(Durable Suspension):外部イベント(人間の承認やWebhookで72時間待機など)の際、CPU・RAMの消費を完全にゼロにし、ソケット接続を保持しません。
  • イベントソーシングによる完全な監査性:すべての状態遷移、ツール呼び出し、LLMの推論ステップが追記専用イベントジャーナルに改ざん不能な形で記録されます。
+─────────────────────────────────────────────────────────────────────────+
|               Durable Agentic Execution Topology (2026)                 |
|                                                                         |
|  [ Inbound Trigger / Webhook ] ──▶ [ Durable Ingestion Gateway ]        |
|                                                │                        |
|                                                ▼                        |
|                                     [ Event-Sourcing Log ]              |
|                                     (Append-Only Journal)               |
|                                                │                        |
|                   ┌────────────────────────────┴────────────┐           |
|                   ▼                                         ▼           |
|         [ Worker Node A (Active) ]               [ Worker Node B (Idle) ]|
|       ┌─────────────────────────────┐           ┌──────────────────────┐|
|       │ - Step 1: LLM Plan [Cached] │           │ (Hot Standby for     │|
|       │ - Step 2: Query DB [Cached] │           │  instant deterministic│|
|       │ - Step 3: Tool Call ──▶ CRASH!          │  replay if A dies)   │|
|       └─────────────────────────────┘           └──────────────────────┘|
|                   │                                         ▲           |
|                   └─────────── Replay & Resume ─────────────┘           |
|                                     │                                   |
|                                     ▼                                   |
|                       [ Idempotent Tool Gateway ]                       |
|                   ┌─────────────────┴─────────────────┐                 |
|                   ▼                                   ▼                 |
|      [ External Tool: Charge Card ]      [ Durable Sleep / HITL Signal ]|
|       (Idempotency Key Guaranteed)        (Zero-Resource 72h Pause)     |
+─────────────────────────────────────────────────────────────────────────+

2. 複雑性の崖:インメモリ実行の3大障害モード {#the-complexity-cliff-why-in-memory-agents-die}

なぜ単純なエージェントループ(while not done: res = llm.generate(); execute(res.tool))は本番環境で確実に破綻するのでしょうか。テレメトリデータが示す3大障害モードを解説します:

        1. OOM&Spot回収によるコンテキスト全損
        
            Kubernetes環境ではSpotインスタンスが30秒の予告で回収され、重いマルチモーダル解析でLinux OOM Killerが発動します。20分かかる監査タスクの19分目にプロセスが死ぬと、中間推論結果がすべて消失し、膨大なトークン費用の無駄とユーザー待ち時間の倍増を招きます。
        
    
    
        2. 非べき等リトライによる二重実行の悪夢
        
            LLMは分散トランザクションを理解しません。決済やインフラ構築のHTTP POSTを発行した直後、200 OKを受信する前にネットワークが瞬断すると、単純なリトライ機構は同一ステップを再実行し、顧客への二重請求や本番リソースの重複作成を引き起こします。
        
    
    
        3. Human-in-the-Loopによるスレッド枯渇
        
            エンタープライズ業務では重要操作に人間の承認が必要です。同期的なブロック待機(`time.sleep()`やメモリ内キュー待機)を行うと、週末の48時間にわたり500件の承認待ちが発生しただけでワーカーのスレッドプールが枯渇し、システム全体の障害へ波及します。

3. Durable Executionの基本構成要素 {#durable-execution-core-primitives}

Durable Executionは、揮発性の実行(RAM内)から、不変のイベント履歴に基づく永続的な実行へとパラダイムを転換します。それを支える4つの要素があります:

    **1. 追記専用イベントジャーナル(Append-Only Journal)**:変更可能なスナップショットではなく、すべての操作を不変イベントとして記録します(`WorkflowStarted`, `ActivityScheduled`, `ActivityCompleted`, `TimerStarted`)。
    **2. 確定的コードリプレイ(Deterministic Replay)**:ワーカー再起動時、コードを1行目から再実行します。ジャーナルに記録済みのステップに達すると、エンジンが呼び出しをインターセプトしてキャッシュされた結果をマイクロ秒で即時返却し、未完了の障害地点まで一瞬でファストフォワードします。
    **3. Durableタイマーとシグナル**:`workflow.sleep(timedelta(days=3))`の呼び出しはDBに起床イベントを登録してスレッドを即時解放します。人間の承認が行われると`Signal`がジャーナルに追加され、空いている任意のワーカーで処理が再開されます。
    **4. 仮想アクターモデル(Restateアーキテクチャ)**:Restateなどの最新システムはDurable ExecutionをステートフルなVirtual Actorとして実装します。状態がエンティティキー(`agent_id`)に直接紐づき、分散ロックの競合なしにシングルライターの一貫性とサブミリ秒のローカルアクセスを実現します。

4. エンジン徹底比較:Temporal vs Restate vs Inngest vs LangGraph {#engine-showdown-temporal-restate-inngest-langgraph}

適切なフレームワークの選定は、2026年のAI基盤において最も重要なアーキテクチャ判断の1つです。主要4エンジンの技術比較を以下に示します:

比較項目 Temporal Restate Inngest LangGraph Checkpointers
アーキテクチャモデル イベント駆動ワークフローエンジン(Cluster + DB) Durable Virtual Actor Runtime(単一バイナリ) イベント駆動Serverlessオーケストレーター アプリ層グラフチェックポイント(Postgres/Redis)
状態の永続化 追記型履歴シャード(Cassandra/Postgres) ログ構造化ストレージ+ローカルキャッシュ イベントストア+一時的Serverless状態 ノード毎のシリアライズされたスナップショット
クラッシュリカバリ イベント履歴からの確定的コードリプレイ ジャーナルファストフォワードとアクター起床 ステップ単位のメモ化再実行 最新チェックポイントの読み込みとノード再実行
ストリーミング・遅延 高(タスク配信毎に20〜50ms程度) 極低(内部配信2ms未満、HTTP/2ネイティブ) 中(Serverlessオーバーヘッド30〜80ms) エンジン遅延ゼロ(DBの読み書き速度に依存)
Human-in-the-Loop Signal&Query組み込み(堅牢で実績多数) Durable Promises&Awakeables(直感的) TTL付きステップ単位waitForEvent interrupt()プリミティブと状態再注入
運用負荷 大(Temporal Server、Matching、DB、UI構築) 極小(単一バイナリ、最小フットプリント) 小(マネージドSaaS利用が主流) 外部エンジン不要(既存のPostgres/Redis活用)
最適な適用ユースケース 数日間に及ぶ金融・ERPエンタープライズ業務 リアルタイム対話エージェント、低遅延ストリーミング イベント駆動型Webhook、Serverless / Edge LangChainエコシステム内の推論チェーングラフ

5. 本番アーキテクチャ:べき等ツールゲートウェイ {#production-architecture-idempotent-tool-orchestration}

LLMとDurable Executionを組み合わせる際のアキレス腱は「外部の副作用」です。Durable Engineはコードリプレイを用いて状態を復元するため、外部ツール呼び出しが厳密にべき等でない場合、リプレイ時に壊滅的な重複実行が発生します。その解決策が**べき等ツールゲートウェイ(Idempotent Tool Gateway)**です:

+─────────────────────────────────────────────────────────────────────────────+
|                    Idempotent Tool Gateway Sequence                         |
|                                                                             |
| [ LLM Reasoner ]  [ Durable Engine ]  [ Tool Gateway ]    [ External API ]  |
|        │                  │                  │                   │          |
|        │── Decide Tool ──▶│                  │                   │          |
|        │   "charge_card"  │                  │                   │          |
|        │                  │── Execute Step ─▶│                   │          |
|        │                  │   (Token/RunId)  │                   │          |
|        │                  │                  │── Check Cache ───▶│          |
|        │                  │                  │   (IdempotencyKey)│          |
|        │                  │                  │                   │          |
|        │                  │                  │── POST Charge ───▶│          |
|        │                  │                  │   (Key in Header) │          |
|        │                  │                  │◀── HTTP 200 OK ───│          |
|        │                  │                  │                   │          |
|        │                  │                  │── Write Journal ──│          |
|        │                  │◀── Tool Return ──│                   │          |
|        │                  │    (Persisted)   │                   │          |
|        │                  │                  │                   │          |
|  === CRASH & REPLAY ===   │                  │                   │          |
|        │                  │── Re-eval Step ─▶│                   │          |
|        │                  │   (Same RunId)   │                   │          |
|        │                  │                  │── Cache HIT! ─────│ (Skip    |
|        │                  │◀── Return Cached─│   (No HTTP call)  │  Remote) |
|        │                  │    Result        │                   │          |
+─────────────────────────────────────────────────────────────────────────────+

べき等キーの導出規則:LLMにべき等キーを生成させてはなりません。LLMは確率的でありリプレイ時に異なる文字列を生成するためです。暗号ハッシュを用いて確定的に導出します:

    IdempotencyKey = SHA256(WorkflowID || NodeID || StepSequence || ToolName)

6. Pythonによる本番Durable Agent実装 {#production-implementation-durable-agent-python}

以下は、確定的ステップ実行、べき等ツールラッパー、および外部暗号承認シグナルが届くまでリソース消費ゼロで待機するHITLゲートを備えた、実稼働可能なPythonコードです:

# Production Durable AI Agent Workflow Implementation (2026)
# Demonstrates deterministic execution, idempotent tool calls, 
# and zero-resource Human-in-the-Loop (HITL) suspension.

import os
import json
import hashlib
import asyncio
from typing import Dict, Any, Optional
from dataclasses import dataclass, asdict

class DurableContext:
    def __init__(self, workflow_id: str, journal_storage: Optional[Dict[str, Any]] = None):
        self.workflow_id = workflow_id
        self.journal: Dict[str, Any] = journal_storage if journal_storage is not None else {}
        self.step_counter: int = 0
        self.is_replaying: bool = False

    def generate_idempotency_key(self, tool_name: str, payload: Dict[str, Any]) -> str:
        """Derives a deterministic SHA256 idempotency key."""
        raw_seed = f"{self.workflow_id}:{self.step_counter}:{tool_name}:{json.dumps(payload, sort_keys=True)}"
        return hashlib.sha256(raw_seed.encode("utf-8")).hexdigest()

    async def step(self, name: str, fn, *args, **kwargs) -> Any:
        """Executes a code block with deterministic memoization."""
        self.step_counter += 1
        step_key = f"step_{self.step_counter}_{name}"

        # If step was previously completed, return cached result (Fast Replay)
        if step_key in self.journal:
            print(f"⏩ [DURABLE REPLAY] Fast-forwarding step: '{name}' (Key: {step_key})")
            return self.journal[step_key]

        # First-time execution: execute side effect and commit to journal
        print(f"⚙️  [DURABLE EXEC] Executing real-time step: '{name}' (Key: {step_key})")
        result = await fn(*args, **kwargs) if asyncio.iscoroutinefunction(fn) else fn(*args, **kwargs)
        self.journal[step_key] = result
        return result

    async def wait_for_signal(self, signal_name: str, timeout_seconds: int = 86400) -> Any:
        """Durable HITL suspension: releases all thread resources until external signal arrives."""
        self.step_counter += 1
        signal_key = f"signal_{self.step_counter}_{signal_name}"

        if signal_key in self.journal:
            print(f"⏩ [DURABLE REPLAY] Signal '{signal_name}' already resolved from journal.")
            return self.journal[signal_key]

        print(f"⏸️  [DURABLE SUSPEND] Workflow paused. Waiting for external signal: '{signal_name}'...")
        print(f"    (Resources released: 0 CPU, 0 RAM, 0 Sockets held. Timeout: {timeout_seconds}s)")
        
        # In real production, this thread terminates and state is flushed to DB.
        await asyncio.sleep(1) # Simulated trigger arrival
        simulated_approval = {"status": "APPROVED", "approver": "secops_admin@enterprise.ai", "token": "sig_valid_99"}
        self.journal[signal_key] = simulated_approval
        return simulated_approval

@dataclass
class AgentState:
    task_id: str
    target_repo: str
    vulnerability_score: float
    patch_generated: bool
    deployment_status: str

async def mock_llm_code_analysis(repo: str) -> Dict[str, Any]:
    await asyncio.sleep(0.5)
    return {
        "vulnerabilities_found": 3,
        "criticality": "HIGH",
        "patch_diff": "--- a/auth.py\n+++ b/auth.py\n@@ -12,2 +12,4 @@\n+ import hmac\n- if token == secret:\n+ if hmac.compare_digest(token, secret):"
    }

async def idempotent_deploy_tool(idempotency_key: str, repo: str, patch: str) -> Dict[str, Any]:
    print(f"🚀 [EXTERNAL TOOL CALL] Deploying hotfix with Idempotency-Key: {idempotency_key[:16]}...")
    await asyncio.sleep(0.5)
    return {"deploy_id": "dep_88192a", "status": "SUCCESS", "timestamp": 1774167200}

async def run_autonomous_secops_agent(ctx: DurableContext, repo: str) -> AgentState:
    print(f"\n🏁 Initializing SecOps Agent Workflow for repository: {repo} (Workflow ID: {ctx.workflow_id})")

    # Step 1: LLM Security Analysis
    analysis = await ctx.step("llm_security_scan", mock_llm_code_analysis, repo)
    
    # Step 2: Policy Verification & HITL Gate
    if analysis["criticality"] in ["HIGH", "CRITICAL"]:
        print(f"⚠️ High-severity patch detected. Escalating to SecOps Human-in-the-Loop gate.")
        approval = await ctx.wait_for_signal("secops_patch_approval")
        if approval.get("status") != "APPROVED":
            raise PermissionError("Patch deployment rejected by Security Operations.")

    # Step 3: Idempotent Deployment Execution
    idem_key = ctx.generate_idempotency_key("production_deploy", {"repo": repo, "patch": analysis["patch_diff"]})
    deploy_result = await ctx.step(
        "deploy_hotfix_production", 
        idempotent_deploy_tool, 
        idempotency_key=idem_key, 
        repo=repo, 
        patch=analysis["patch_diff"]
    )

    return AgentState(
        task_id=ctx.workflow_id,
        target_repo=repo,
        vulnerability_score=9.4,
        patch_generated=True,
        deployment_status=deploy_result["status"]
    )

7. 確定的リプレイの鉄則とアンチパターン {#deterministic-replay-rules-and-antipatterns}

Durable Executionにおける最大のバグ原因は**非確定的ドリフト(Non-Deterministic Drift)**です。エンジンが過去の履歴に基づきコードを1行ずつリプレイするため、ワークフロー関数は同一の入力に対して完全に同一の挙動を示す必要があります。

カテゴリ ❌ 禁止される非確定的コード ✅ 適合するDurableパターン 技術的理由
システム時刻 datetime.now() await workflow.current_time() リプレイは数時間後に走るため、通常の時計は異なる時刻を返し条件分岐が破壊されます。
乱数生成 random.randint(100, 999) await workflow.random_int() 乱数ジェネレータがリプレイ時に異なる数値を返し、後続ツールの引数が変化します。
直接ネットワークI/O requests.get(url) await workflow.execute_activity(fn) 生の通信はリプレイ時にも再実行されてしまいます。Activity経由にすることでキャッシュから即時返却されます。
OSスレッド threading.Thread(target=fn) [workflow.spawn(fn) for ...] OSネイティブのスレッドは競合状態(Race Condition)を生み、確定的リプレイが不可能になります。

8. エンタープライズコスト、レイテンシSLOとストレージ経済学 {#enterprise-cost-and-latency-benchmarks}

エンジニアリングリーダーから「Durable Executionの導入で遅延やストレージ費用が肥大化しないか」という質問がよく寄せられます。2026年の実測ベンチマークは以下の通りです:

+─────────────────────────────────────────────────────────────────────────+
|      Cost of Failure: Naive In-Memory vs. Durable Execution             |
|                                                                         |
| Task: 10-Step Document Migration (Total Tokens: 85,000 | Cost: $1.70)   |
|                                                                         |
| [ Naive Agent: Crash at Step 9 ]                                        |
|  ├── Step 1-9 Compute: $1.53 (Vaporized)                                |
|  ├── Restart from Step 1: $1.70                                         |
|  └── Total Cost: $3.23 (90% Cost Penalty, 2x Latency)                   |
|                                                                         |
| [ Durable Agent: Crash at Step 9 ]                                      |
|  ├── Step 1-9 Journal Replay: $0.00 (Cached from Event Log)             |
|  ├── Step 10 Compute: $0.17                                             |
|  └── Total Cost: $1.70 (0% Cost Penalty, Zero Wasted Tokens)             |
+─────────────────────────────────────────────────────────────────────────+
  • ローカル記録の遅延:Restateなどの最新エンジンでは内部ディスパッチのオーバーヘッドは2.5ミリ秒未満であり、800ms〜4000msのLLM推論時間と比較して誤差範囲です。
  • コールドスタート時のリプレイ速度:メモリ上での50ステップのリプレイは15ミリ秒未満で完了します。すべての外部I/Oがスキップされジャーナルキャッシュから読み出されるためです。
  • 月間コスト削減効果:月間10万件のマルチステップ処理を行うシステムにおいて一時障害率が4%の場合、Durable Executionの導入により月間42,000ドル以上のLLM API課金の無駄が防止されます。

9. アーキテクチャ選定指針と関連ツール {#decision-framework-related-tools}

適切なDurableフレームワークの選択は、既存スタック、レイテンシ要求、および運用体制によって決まります:

[ Is your primary stack Python or Polyglot? ]
                                   │
                   ┌───────────────┴───────────────┐
                   ▼                               ▼
              [ Python ]                      [ Polyglot ]
                   │                               │
       [ Deep LangChain ecosystem? ]     [ What is your latency SLO? ]
           │                 │                     │                 │
          Yes                No               < 5ms Realtime     Batch/ERP
           │                 │                     │                 │
           ▼                 ▼                     ▼                 ▼
     [ LangGraph ]      [ Inngest ]           [ Restate ]       [ Temporal ]
     Checkpointers       (Serverless)        (Virtual Actor)    (Heavy Duty)

LangGraph
グラフフレームワーク

            PythonおよびTypeScriptにおけるグラフ型エージェントオーケストレーションの標準。PostgreSQLおよびRedisアダプターによる状態チェックポイント機能を標準装備。
        
        [
            LangGraphを見る →
        ](https://agdex.ai/tools/langgraph.html)

OpenAI Agents SDK
公式SDK

            エージェントワークフローに特化した軽量フレームワーク。ツール呼び出し、サブエージェント間のハンドオフ、ガードレール機能をネイティブサポート。
        
        [
            OpenAI Agents SDKを見る →
        ](https://agdex.ai/tools/openai-agents-sdk.html)

CrewAI
マルチエージェント

            明確な役割を持つエージェント群とクルーを構築するためのマルチエージェント共同作業フレームワーク。階層的タスク委譲とメモリ永続化をサポート。
        
        [
            CrewAIを見る →
        ](https://agdex.ai/tools/crewai.html)

Modal
サーバーレスクラウド

            コンテナ化されたAIエージェントワーカーやGPUアクセラレーションを瞬時にスケールさせる、低コールドスタートのサーバーレス基盤。
        
        [
            Modalを見る →
        ](https://agdex.ai/tools/modal.html)

10. よくある質問(FAQ) {#frequently-asked-questions}

Q1: セッションメモリ(Mem0、Zep)とDurable Executionの決定的な違いは何ですか?

            セッションメモリは*データ*(会話ログ、埋め込みベクトル、ユーザー属性)を保存します。一方、Durable Executionは*制御フローと状態遷移機械*(コールスタック、現在の実行ステップ、未解決のプロミス、シグナルリスナー)を保存します。DBに会話ログがあっても、API移行中にDockerコンテナが死んだらエージェントを復旧できません。
        
    
    
        Q2: イベントソーシングによってデータベース容量が肥大化しませんか?
        
            Durable Engineは**スナップショット作成とログ圧縮(Compaction)**によりこの問題を解決します。ワークフロー完了後、詳細なイベント履歴は安価なオブジェクトストレージ(S3/GCS)へ退避され、プライマリDBには最終状態のみが保持されます。
        
    
    
        Q3: 既存のLangGraphアプリケーションをDurable Executionへ移行するには?
        
            まずLangGraphの`PostgresSaver`をチェックポインタとして設定します。さらにインフラ障害やPod回収から完全に保護したい場合は、LangGraphの実行全体をRestateやTemporalのActivityステップでラップし、スレッドIDを永続識別子として渡します。
        
    
    
        Q4: LLMのストリーミング出力とDurable Executionを併用できますか?
        
            はい。RestateなどのモダンエンジンはHTTP/2やSSEによるストリーミングをネイティブサポートしています。初回のリアルタイム実行時はトークンが逐次クライアントへストリーム配信され、クラッシュ後のリプレイ時はジャーナルから一括で高速返却されます。
        
    
    
        Q5: べき等キーをサポートしていない外部APIはどのように保護すべきですか?
        
            分散予約テーブルを用いた2フェーズロックを構築します。外部APIを呼び出す直前に、ACID準拠DBにべき等ハッシュとともにPENDINGレコードを書き込み、成功後にCONFIRMEDへ更新します。リプレイ時にCONFIRMEDが見つかれば、呼び出しをスキップします。

元記事:AgDex.ai - Why AI Agents Fail in Production: Building Crash-Resilient Workflows with Durable Execution

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?