LLMのレスポンスをSSEでストリーミング実装する
- •ストリーミングによりトークンを即時表示し、ユーザーが感じるLLMの待ち時間を改善する。
- •サーバー送信イベント(SSE)規格を利用して、持続的なHTTP接続でデータを小分けにプッシュする。
- •開発者は静かな切り捨て、ゴースト接続、断片化されたパケットなどのストリーム固有の不具合を処理する必要がある。
ストリーミングは、LLMが生成したトークンを順次表示することで、ユーザーが感じる応答待ち時間を短縮し、UXを向上させる。全体の生成時間は非ストリーミング時と同じだが、stream: trueフラグを設定することで、アプリケーションは約300ミリ秒以内にレンダリングを開始できる。この手法は、サーバー送信イベント(SSE)というウェブ標準を利用する。これは持続的なHTTP接続を維持し、モデルの出力に合わせてクライアントへイベントをプッシュする仕組みである。この際、テキストは「content_block_delta」イベント内の「delta.text」を通じて送信され、最終的な「message_delta」イベントで「stop_reason」が通知される。
ストリームの読み取りには、レスポンスボディのReadableStreamを反復処理し、受信したバイトをバッファリングし、二重改行で個別のSSEメッセージを分離する必要がある。開発者はこれらをJSONとしてパースしなければならない。モデルではなくネットワークの判断でトークンがパケットに分割されるため、不完全なチャンクをバッファリングする処理が不可欠である。さらに、実際のコンテンツを抽出するためには、ツール引数や署名といった無関係なメタデータを無視し、text_deltaなどの正しいデルタタイプを識別する必要がある。
堅牢なストリーミング実装には3つの共通課題への対策が求められる。1つ目はナビゲーション中も接続が残る「ゴーストストリーム」であり、AbortControllerを用いて不要になったfetchリクエストを明示的にキャンセルする必要がある。2つ目はストリーム途中でAPIエラーが発生する「静かな切り捨て」であり、データ型のエラーイベントを検知して適切に処理しなければならない。3つ目は「パケットの断片化」であり、不完全な断片を適切にバッファリングしてパースすることで解決する。また、end_turn、max_tokens、tool_use、stop_sequenceといったstop_reasonを監視し、モデルの応答が完了したか、あるいは制限により中断されたかを正確に判定することが重要である。