コンテンツにスキップ

Short DB Transaction, Long Agent Session|長セッション・短トランザクション

一言で(TL;DR)

エージェントセッションは数秒から数十分に及びますが、データベーストランザクションはミリ秒単位で閉じます。LLM推論の間はトランザクションを開かず、推論結果を短い独立トランザクションで書き込むことで、デッドロック・コネクションプール枯渇・ロック競合を防ぎます。ステップ間の状態は外部ステートストアに保持します。

解決する問題

従来のWebアプリケーションでは「リクエスト受信 → トランザクション開始 → 処理 → コミット/ロールバック」がミリ秒で完結します。この前提でコネクションプールサイズやロックタイムアウトが設計されています。

AIエージェントの場合、1リクエストが秒〜数十分に及びます(F1)。LLM呼び出し中にDBトランザクションを開いたままにすると以下が起きます。

  • コネクションプール枯渇 — 同時セッション数がプールサイズを超えた瞬間、新規リクエストがブロックされます。LLM応答待ちの数十秒×数十セッションでプールが飽和します。
  • デッドロック・ロック競合 — 行ロックやテーブルロックが長時間保持され、他のトランザクションが待たされます。複数エージェントが同じリソースを操作するシナリオでデッドロックが頻発します。
  • トランザクションタイムアウト — RDBの idle_in_transaction_session_timeout やアプリケーションサーバのコネクションタイムアウトに引っかかり、正常な処理が中断されます。

さらに確率的な動作(F3)により、LLM呼び出しの所要時間のばらつきが大きくなります。P99レイテンシがP50の数倍になることがあり、「平均的には問題ない」設計でもテール側でプール枯渇やタイムアウトが発生します。

選定条件(When to use / When NOT)

  • 使う条件
    • エージェントがLLM呼び出しの前後でDB読み書きを行うワークフローを持っています。
    • LLM呼び出し1回あたりの所要時間が概ね1秒を超えます(ほとんどのLLM呼び出しが該当します)。
    • 同時セッション数がコネクションプールサイズの概ね半分に達する可能性があります。
  • 使わない条件
    • LLMを使わない従来のCRUD処理 → 通常のリクエストスコープトランザクションで十分です。
    • 単一トランザクション内でアトミックに完結しなければならない操作(銀行間送金の借方・貸方など) → 短トランザクションに分離すると整合性が崩れます。この場合は操作をLLM呼び出しと分離し、LLM推論後に単一の短トランザクションで全操作を実行する設計にします。
    • 複数外部システム間の整合性が必要 → 本パターンだけでは不十分です。C8 補償トランザクションを併用し、サーガの各ステップを短トランザクションとして実装します。

駆動変数とチューニング(程度)

目盛り 効かなすぎ ⇔ 効きすぎ 決め方 [駆動変数] 目安(出発点)
トランザクション粒度 粗い(LLM呼び出しを含む広範囲) → プール枯渇・デッドロック ⇔ 細かすぎ → 部分書込で不整合 [reversibility] が低いほど「1つの論理操作 = 1トランザクション」を厳守し、中途半端な状態を防ぐ LLM呼び出しの直前でコミット、応答後の書込は新トランザクションで。概ね 10–100 ms 以内にコミット
ステート永続化頻度 低い → クラッシュで作業消失 ⇔ 高い → I/Oオーバーヘッド [reversibility] が低い操作の直前は必ず永続化。高ければ数ステップまとめてもよい 各LLM呼び出しの結果を受け取るたびに1回
コネクションプール使用パターン 長時間借用 → プール枯渇 ⇔ 毎回取得・返却 → オーバーヘッド 同時セッション数とプールサイズの比率で判断 セッション単位ではなくトランザクション単位でコネクションを借用・返却

相反における立ち位置(相反)

  • F-14 インコンテキスト状態 vs 外部ステートストア → external-state。長時間セッションの中間状態をDBトランザクション内に閉じ込めるのではなく、外部ステートストア(Redis、DynamoDB、専用のセッションテーブルなど)に都度書き出します。[reversibility] が低い操作を含むセッションでは特に、各ステップの結果を永続化してクラッシュ後の再開を可能にします。インコンテキスト状態(LLMのコンテキストウィンドウ内のみで状態を保持)は、セッションが短く(数秒以内)、失敗時の再実行コストが低い場合に限ります。

構造

flowchart TD
  subgraph エージェントセッション(数秒〜数十分)
    S1[Step 1: DB読取]
    LLM1[LLM呼び出し 1]
    S2[Step 2: 結果書込]
    LLM2[LLM呼び出し 2]
    S3[Step 3: 最終書込]
  end

  S1 -->|短TX: SELECT| DB[(DB)]
  S1 -->|コミット・コネクション返却| LLM1
  LLM1 -->|推論完了| S2
  S2 -->|短TX: INSERT/UPDATE| DB
  S2 -->|コミット・コネクション返却| LLM2
  LLM2 -->|推論完了| S3
  S3 -->|短TX: INSERT/UPDATE| DB

  S1 -.->|状態保存| SS[(外部ステートストア)]
  S2 -.->|状態保存| SS
  S3 -.->|状態保存| SS

LLM呼び出しの間(LLM1LLM2)ではDBコネクションを保持しません。各ステップで必要なときだけコネクションプールから借用し、短トランザクションをコミットした直後に返却します。

実装メモ

セッション管理と短トランザクションの分離パターンを示します。

class AgentSession:
    def __init__(self, session_id: str, state_store, db_pool):
        self.session_id = session_id
        self.state_store = state_store
        self.db_pool = db_pool

    async def run_step(self, step_fn, llm_fn):
        # 1. 短トランザクションでDB読取
        async with self.db_pool.acquire() as conn:
            async with conn.transaction():
                db_data = await step_fn.read(conn)
        # コネクションはここで返却済み

        # 2. LLM呼び出し(トランザクション外・コネクション外)
        llm_result = await llm_fn(db_data)

        # 3. 短トランザクションで結果書込
        async with self.db_pool.acquire() as conn:
            async with conn.transaction():
                await step_fn.write(conn, llm_result)

        # 4. セッション状態を外部ストアに永続化
        await self.state_store.save(self.session_id, step_fn.name, llm_result)

落とし穴:

  • 楽観的ロックの併用 — LLM推論中に別のプロセスが同じ行を更新する可能性があります。version カラムや updated_at を用いた楽観的ロック(UPDATE ... WHERE version = ?)を書込トランザクションに含め、競合時はリトライします。悲観的ロック(SELECT ... FOR UPDATE)をLLM呼び出し前に取得して保持するのは本パターンの趣旨に反します。
  • 読取と書込の間の一貫性 — Step 1 で読んだデータが Step 2 の書込時点で古くなっている可能性があります。業務上の整合性が重要な場合、書込トランザクション内で再度条件を検証します(read-modify-write パターン)。
  • コネクションプールの設定 — エージェントの同時セッション数に対してプールサイズを設計する際、「セッション数 = 必要コネクション数」ではなく「同時にトランザクション実行中のステップ数」で計算します。本パターンにより、概ね同時セッション数の 10–20% 程度のコネクション数で運用できます。
  • ステートストアの選定 — Redis(高速だが耐久性に注意)、DynamoDB / Cloud Datastore(耐久性が高い)、RDB上のセッションテーブル(既存インフラを活用)などが選択肢になります。[reversibility] が低いセッションではディスク永続化を持つストアを選びます。

効かせる力学(forces)

  • F1(1リクエストが長い) — LLM呼び出しが数秒〜数十分かかるため、その間トランザクションやコネクションを保持するとリソースが枯渇します。短トランザクションへの分離により、リソース保持時間をミリ秒単位に抑え、長時間セッションとDB運用を両立させます。
  • F3(確率的) — LLM呼び出しのレイテンシや成否にばらつきがあります。途中でLLM呼び出しが失敗しても、各ステップの結果が短トランザクションで確定済みであれば、失敗したステップからのリトライが可能になります。C4 冪等コマンド包装と組み合わせることで、リトライ時の二重書込も防止できます。

関連・代替

  • A2 耐久非同期 — チェックポイントの書込は短トランザクションで行います。本パターンはA2のチェックポイント実装における基本原則を提供します。
  • C4 冪等コマンド包装 — 短トランザクションへの分離でアトミック性を手放した分、冪等キーによるリトライ安全性で補います。各書込トランザクションに冪等キーを付与することで、セッション再開時の二重書込を防ぎます。
  • C8 補償トランザクション — 複数の短トランザクションを跨ぐ整合性をサーガで管理します。サーガの各ステップは本パターンの短トランザクションとして実装されます。
  • G2 エンドツーエンドトレース — セッション内の各短トランザクションをトレースIDで関連付け、分散した書込操作を一連のセッションとして追跡可能にします。

コーディングエージェント向け指示(machine-actionable)

  • [ ] DB操作を含むエージェントワークフローで、LLM呼び出し中にトランザクションが開いたままになっている箇所がないか確認したか
  • [ ] コネクションプールのサイズが同時セッション数ではなく同時トランザクション数に基づいて設計されているか確認したか
  • [ ] LLM推論中のデータ競合に対して楽観的ロックを実装する設計を提案したか
  • [ ] セッション状態の永続化先を選定し、[reversibility] に応じた耐久性要件を提示したか
  • [ ] 短トランザクション分離で失われるアトミック性を、C4 冪等コマンド包装で補完する設計を提案したか
  • [ ] 複数ステップ間の整合性が必要な場合、C8 補償トランザクションとの併用を検討したか
  • [ ] 目盛り(上表)の値を [reversibility] から導き、理由を添えて提示したか