powered by TechFeed
表示モード
Deep Dive

AIエージェントをプロダクションで動かすと最初に壊れる理由 — キュー・べき等性・ワークフローエンジンの設計パターン

7月30日、Renderが「Infrastructure Patterns for Production AI Agents」と題した記事を公開した。デモでは動くエージェントがプロダクションで壊れる根本原因は、HTTPリクエストの寿命とエージェントの実行特性の根本的なミスマッチにある。記事はキュー・べき等性・ワークフローエンジンという3つのレイヤーに分けて、信頼性の高いエージェントインフラの設計パターンを詳述している。

7月30日、Renderが「Infrastructure Patterns for Production AI Agents」と題した記事を公開した。デモでは動くエージェントがプロダクションで壊れる根本原因は、HTTPリクエストの寿命とエージェントの実行特性の根本的なミスマッチにある。記事はキュー・べき等性・ワークフローエンジンという3つのレイヤーに分けて、信頼性の高いエージェントインフラの設計パターンを詳述している。


デモが壊れる理由:エージェントはHTTPリクエストと相性が悪い

多くのチームは最初、エージェントをルートハンドラに包んでレスポンスを待つ、という形で実装する。デモでは動く。しかしプロダクションでは、これが最初に壊れる。

エージェントには3つの特性がある。長時間実行(完了まで数時間〜数日かかることもある)、ステートフル(目標・計画・ツール呼び出し結果・リトライといった状態を持つ)、非決定的(モデルが実行時に次のアクションを選ぶため、ステップ数が3になるか30になるかわからない)だ。

この3つがHTTPリクエストの寿命と根本的に噛み合わない。対策の核心は、「エージェントの実行をリクエストから切り離すこと」にある。


パターン1:キューによる実行の非同期化

最初のパターンはシンプルだ。リクエストはジョブをエンキューして即座に返す。エージェントはどこか別の場所で動く。クライアントはrun IDを受け取り、ステータスをポーリングするかコールバックを待つ。

API → Queue(耐久性のあるバッファ) → Worker → Database(進捗記録)

キューは「独立したタスクが多く、ワーカーのスケーリングやリトライが必要な場合」のデフォルト出発点だ。ただし落とし穴がある。キューはジョブの存在は知っているが、そのジョブが属する論理的なプロセスの形は知らない。「ステップ3はステップ2に依存する」「人間の承認待ちが発生する」「2つのブランチが合流してから結果を統合する」——こうした管理はキューではできない。そこで登場するのがワークフローエンジンだ。


パターン2:べき等性と補償トランザクション(ここが最も重要)

キューへの移行はライフタイム問題を解決するが、実行の耐久性は解決しない。

プロダクションのキューはほぼすべてat-least-once(少なくとも1回)配信だ。同じジョブが複数回実行される可能性がある。エージェントがツール呼び出し、レコード書き込み、メッセージ送信、リソースプロビジョニングを行う以上、リトライ挙動はアプリケーションの正確性モデルの一部になる。

べき等性:同じステップが2回走ったとき

悪い例:

# クラッシュを想定していない
result = call_external_api(params)
db.save(result)

良い例(べき等境界を作る):

existing = db.find_completed(idempotency_key)
if existing:
    return existing

db.upsert_running(idempotency_key)
result = call_external_api(params)
db.mark_complete(idempotency_key, result)
return result

ツールを呼び出す副作用のある操作すべてに、この処理が必要だ。

補償トランザクション(Sagaパターン):途中で失敗したとき

カードへの課金→リソースプロビジョニング→確認メール送信、という5ステップのエージェントがステップ4で永続的に失敗したとする。最初の3ステップはDBトランザクションでロールバックできない。課金はすでに完了しており、先頭から再実行すればカードが二重請求される。

分散システムの標準的な答えがSagaパターンだ。副作用のある各アクションに対して、それを打ち消す「補償アクション」を定義する。課金には返金、メール送信には訂正メールを対応させる。実行が永続的に失敗した場合、オーケストレーターは完了済みのステップを逆順に辿って補償を実行する。Sagaパターンはマイクロサービスアーキテクチャで広く知られた手法であり、microservices.io のSagaパターン解説も参考になる。

ただし補償は万能ではない。5つの並列サブタスクのうち4つが成功して1つが失敗した場合、4つを完了させて1つを破棄する方が、4つを巻き戻すより良い結果になることも多い。部分的な成功が許容できない場合にのみ補償を実装するのが現実的だ。


パターン3:ワークフローエンジンによるオーケストレーション

「次に何が起きるべきか、これまでに起きたことを踏まえて」——この問いに答えることが、キューイングからオーケストレーションへの移行を意味する。

ワークフローエンジンはランの履歴(どのステップが開始・完了・失敗・リトライ・タイムアウトしたか)を保存する。クラッシュが起きても最初からやり直しにならない。この領域ではTemporalApache ConductorといったOSSのワークフローエンジンが先行して実績を持っており、Renderが提供するRender Workflowsも同様のモデルを採用している。

すべてのワークフローシステムは2つの役割に分かれる。コーディネーター(次に何が起きるかを決定)とステップ(実際の作業——モデル呼び出し、ツール実行、レコード書き込み)だ。コーディネーターには1つの厳格なルールがある。同じ履歴が与えられれば、常に同じ決定を下さなければならない。これはイベントソーシング的なリプレイ(記録された履歴を再生してランを再構築する)の仕組みに起因する。クラッシュ後、エンジンは決定ロジックを履歴に沿って再実行し、完了済みの作業には記録された結果を代入してランを再構築する。

そのため、モデル呼び出し・現在時刻・乱数・外部サービスへの呼び出しなど、2回目に異なる結果を返す可能性があるものはすべてステップに入れる必要がある。コーディネーター内に置くと、再開したランが自分自身の履歴から静かにずれていく。

ファンアウト・ファンイン

ワークフローが特に有用なのが、ランが分岐するケースだ。リードエージェントが目標を独立したサブタスクに分解して各ワーカーに送り、結果を統合する——いわゆるファンアウト/ファンインのパターンだ。

// Promise.allSettled で1つの失敗が全体を毒さないようにする
const results = await Promise.allSettled(subtasks.map(t => executeAction(t)));
const succeeded = results.filter(r => r.status === 'fulfilled');

// 成功閾値を明示的に設定する
if (succeeded.length < MIN_SUCCESS_THRESHOLD) {
  throw new Error('Too many branches failed');
}

Promise.allSettledを使うことで1つのブランチ失敗が全体に波及しないようにし、成功閾値を明示的に設定することで部分失敗時のポリシーをキューではなくワークフローが決定する。

ファンアウトはコンテンション(競合)も引き起こす。サブエージェントが3並列なら動き、11並列なら失敗する——プロバイダーのキャパシティが十分にあっても、同一のレート制限に衝突するからだ。プロバイダー・データベース・モデルAPIを共有リソースとして扱い、同時実行数の上限設定とバックオフが必要になる。


Render Workflowsの場合

記事の後半では、上記パターンをRenderのプラットフォームで実装した場合の具体例が紹介されている。Render Workflowsでは、タスクをTypeScriptまたはPythonの関数として定義すると、キュー・ワーカー・リトライ・分離・ランごとの可観測性をプラットフォームが提供する。

const decide = task('decide', async (state: AgentState) => {
  // モデルを呼び出して次のアクションを決定
});

const executeAction = task('execute-action', async (action: Action) => {
  // ツールを実行
});

const runAgent = task('run-agent', async (goal: string) => {
  let state = { goal, history: [] };
  while (!state.done) {
    const action = await decide(state);
    const result = await executeAction(action);
    state = updateState(state, result);
  }
  return state.output;
});

task()でラップされた関数を呼び出すと、それはローカル関数呼び出しではなく、独自のリトライポリシーと独自のコンピュートプロファイルを持つ独立したタスク実行として起動される。Promise.allがファンアウトを担い、ディスパッチ・分離・リトライをプラットフォームが処理する。

エージェントランの開始は1回のSDK呼び出しだ:

const run = await startTask(runAgent, goal);

現在ベータ版として提供されており、対応言語・スケジューリング・Blueprint対応・HIPAAホスト対応には制限がある。

なおプラットフォームが取り除けないのは、オーケストレーションの設計上の負担だ。べき等性・補償ロジック・アーティファクトストレージ・承認境界・コスト管理・レート制限は引き続き開発者が実装する必要がある。ただし記事では、強固な耐久性保証・ファーストクラスのべき等性・ネイティブの承認ゲートといった機能がロードマップ上にあることも触れられている。


詳細はInfrastructure Patterns for Production AI Agentsを参照していただきたい。