基本を超えて: LLM トークンストリームを再開可能かつマルチデバイスにする
多くの開発者にとって、同期的なチャットボットから非同期 AI エージェント(ユーザーが作業中にバックグラウンドで動作するツール)への移行は、単なるトランスポートの変更のように思えます。一般的なアドバイスは、Last-Event-ID ヘッダーを使用した Server-Sent Events(SSE)で耐久性のあるストリームを作ることです。理論上は簡単そうに聞こえますが、実際にスケールで実装しようとすると、かなりのアーキテクチャ上の摩擦が生じます。
シンプルなデモから本番レベルのエージェント体験へと進むと、3 つのコア要件が浮かび上がります:再開可能なストリーム、信頼できるキャンセル、そしてマルチデバイス同期です。SSE は技術的にはこれらを扱えますが、実装の詳細を見ると「可能」から「簡単」へのギャップが見えてきます。
トークンストリーミングのオーバーヘッド
アーキテクチャ上の課題に入る前に、LLM の応答の性質を理解することが重要です。Vercel AI SDK、OpenAI、Anthropic のいずれを使用していても、返されるデータは単なるテキストストリームではありません。各「トークン」には大量のメタデータが付随しています。
たとえば、Anthropic API からの単一イベントは、実際のテキスト差分が 5 文字であっても、JSON メタデータだけで 125 文字を占めることがあります。このメタデータのオーバーヘッドは、ストリームをデータベースに保存して耐久性を持たせようとした瞬間に重大なボトルネックとなります。
再開可能なストリームの課題
ストリームを再開可能にするには、クライアントが切断後に再接続し、欠落したトークンを要求できなければなりません。そのために SSE 仕様は Last-Event-ID ヘッダーを提供しています。すべてのイベントに一意の ID(例: response_id:token_index)が付与されていれば、クライアントはサーバーに正確にどこまで受信したかを伝えることができます。
しかし、ステートレスなサーバーレプリカが水平スケールする現代のアーキテクチャでは、これは大規模な書き込み増幅問題を引き起こします。どのレプリカが再接続要求を処理してもよいので、すべてのトークン をリアルタイムで共有データベースまたはキャッシュに書き込む必要が出てくるのです。
結果として、数文字のテキストごとにデータベース書き込みを行うという逆説的な状況が生まれます。多くの場合、実際に切断が起きることはほとんどありません。LLM が応答を完了すると、個々のトークンレコードは不要となり、最終的な「完全応答」レコードに置き換えるためにクリーンアップが必要です。これにより、ユーザーのごく一部にしか恩恵がない機能に対して、高コストなインフラ負荷がかかります。
耐久的な環境でのキャンセル処理
基本的な SSE 設定では、接続が切れたことがサーバーに LLM リクエストのキャンセルを通知するシグナルとなります。しかし、再開可能ストリームを実装すると、切断は必ずしもユーザーが停止したことを意味しなくなります。単にトンネルに入ったり、ブラウザをリロードしただけかもしれません。
このため、キャンセルには 帯域外(out-of-band) のメカニズムが必要です。たとえば POST /cancel/{response_id} のような別エンドポイントを用意し、共有データベースに「キャンセルマーカー」を書き込みます。LLM 推論を担当するサーバーレプリカは、トークン生成の合間にこの共有ストアをポーリングし、上流呼び出しを中止すべきかどうかを確認しなければなりません。これにより推論ループに遅延と複雑性が加わります。
マルチデバイス同期のギャップ
複数デバイスをサポートする際には、次の 2 つの問題が出てきます:
- 状態復元: 再開可能性のためにトークンがデータベースに保存されているので、別のデバイスは履歴を取得して現在のストリームに追従できます。
- リアルタイム通知: デバイス B は、デバイス A がプロンプトを送信し、応答がストリーミング中であることをどうやって知りますか?
永続的な双方向接続がない限り、デバイス B はポーリングに頼らざるを得ません。ソース資料でも指摘されているように、ポーリングは「どちらも負け」なトレードオフです。ポーリング頻度が低いと高レイテンシ、頻度が高いとサーバーに不要なトラフィックが集中します。
代替アーキテクチャ: Pub/Sub
これらの摩擦を考えると、HTTP が非同期エージェントアプリケーションに根本的に不適切だという意見もあります。Pub/Sub(Publisher/Subscriber)パターンは、接続寿命とエージェントライフサイクルを切り離すことで、よりエレガントな解決策を提供します。
Pub/Sub モデルでは:
- 永続性: チャネルはクライアントとは独立して存在します。トークンはチャネルに公開され、クライアントは再接続時に「巻き戻して」取得できます。
- マルチデバイス: 複数クライアントが同じチャネルにサブスクライブし、ポーリングなしでリアルタイムに同一ストリームを受信できます。
- 効率性: トークン差分をフルレスポンスに圧縮する処理をトランスポート層が担うため、クライアントが追いつく際に処理すべきメッセージ数が減ります。
- ルーティング: キャンセルや割り込みはチャネルへのメッセージとして公開され、サーバープロセスが即座に消費するため、データベースをポーリングしてキャンセルマーカーを探す必要がなくなります。
コミュニティの見解
SSE の課題は大きいものの、実装次第で「難しさ」は克服できるという声もあります。たとえば、あるコントリビュータは Redis Streams をバックエンドにした SSE 実装が、ステートと耐久性の課題を多く解決できると提案しています。他にも、Durable Streams やトークンイベントとソケットクローズを自動処理する専用 API といった新興ツールが挙げられています。
最終的に選択肢はアプリケーションの規模次第です。シンプルなボットであれば SSE で十分です。しかし、信頼性が高くレイテンシが低いことが求められる複雑なマルチデバイスエージェントの場合、状態管理をデータベースからトランスポート層へ移行することが強力なアーキテクチャシフトとなります。