Tech カテゴリのイメージ

LLMのストリーミングとは?速く感じる仕組みと途中表示の落とし穴

LLMの回答が完成するまで画面を止めておくと、数秒の待ち時間でも利用者は処理が始まったのか判断しにくい。ストリーミングは、生成中の文章や状態を小さなイベントとして順次受け取り、完成前から表示する仕組みである。

ただし、モデルの計算そのものが必ず速くなるわけではない。主に短くなるのは、利用者が最初の結果を見るまでの時間である。最後までの所要時間、品質、料金は別に測る必要がある。

一括応答との違いは受け取る時点にある

通常の一括応答では、モデルが出力を作り終えてから、完成した結果を一つのHTTP応答として受け取る。ストリーミングでは、出力の一部や状態変化を生成中に受け取る。

OpenAIのResponses APIでは、HTTPストリーミングにServer-Sent Eventsを使い、response.output_text.delta、response.completed、errorなど型のあるイベントを送る。アプリは文字列だけでなく、開始、テキストの増分、拒否、道具呼び出し、完了、失敗を区別して扱う。

ここで届く単位を常にトークンと考えない方がよい。APIやSDKが返す増分は、画面に追加する文字列、構造化出力の一部、道具の引数の一部などである。トークナイザー内部の一トークンとイベント一件が一致するとは限らない。

速くなるのは体感であり、完了時間とは限らない

ストリーミングで改善しやすい指標は、要求を送ってから最初の意味ある表示が出るまでの時間である。長い回答なら、利用者は先頭を読みながら後半の生成を待てる。

一方で、次の時間は分けて記録する。

  • 要求開始から最初のイベントまで
  • 最初のテキストが表示されるまで
  • 最後のイベントまで
  • 完了後の検証、保存、画面確定まで

接続、モデルの待機、入力処理が長ければ、ストリーミングを有効にしても最初の文字は早く出ない。短い回答では、細かなイベント処理や画面更新の負荷に対して利点が小さい場合もある。

文字列ではなくイベントの状態機械として扱う

受信した文字列を無条件に末尾へ足すだけでは、拒否、失敗、途中終了、道具呼び出しを正しく表せない。画面とサーバーの両方で、少なくとも次の状態を持つ。

  1. 接続中
  2. 生成中
  3. 道具の処理中
  4. 完了
  5. 取消済み
  6. 失敗または不完全

完了イベントを受けるまでは「生成中」と表示し、保存済みの最終回答と区別する。ネットワークが切れたとき、最後に見えた文章を完成回答として履歴へ確定しない。

APIが返すイベント種別は将来増える可能性がある。未知のイベントを無理に文字列として表示せず、安全に無視しながらログへ種類を残す。SDKとAPI版の更新時には、扱っているイベント一覧を見直す。

途中のMarkdownは壊れて見える

Markdown、数式、コードブロック、リンク、表は、閉じ記号が届くまで構造が完成しない。受信のたびに全文をMarkdownとして描画し直すと、表示が点滅したり、コードブロックの高さが大きく変わったりする。

生成中はプレーンテキスト寄りの表示にし、完了後に正式なレンダリングへ切り替える方法がある。逐次描画する場合は、更新を一定間隔にまとめ、画面の再計算回数を抑える。

HTMLとして直接追加するのは避ける。モデル出力を信頼できるHTMLとして挿入すると、意図しないタグやスクリプトを画面へ持ち込む危険がある。生成中も完了後も、同じサニタイズ方針を使う。

道具の引数は完成するまで実行しない

Tool Callingをストリーミングすると、道具名やJSON引数も増分で届く場合がある。途中のJSONは構文として未完成であり、後続イベントで内容が追加される。

引数の完了イベントを待ち、JSON Schemaで検証してから道具を実行する。途中で見えたファイル名、金額、宛先を先回りして使わない。最初の値が後から変わらないという前提も置かない。

画面には「検索中」「確認中」など、実際に確定した状態だけを表示する。モデルが文章中で「送信しました」と生成しただけでは、外部操作の成功にはならない。道具の実行結果と最終回答を別のイベントとして管理する。

構造化出力は途中では検証できない

JSONやSchemaに従う出力も、ストリーム途中では括弧や必須項目がそろっていない。途中断片を業務データとして保存せず、完了後に全体を組み立てて検証する。

画面へ途中経過を見せたい場合は、検証済みの最終データとは別の表示用バッファを使う。請求額、予約内容、権限変更など正確性が必要な値は、完成と検証が終わるまで確定表示しない。

接続切断時に途中JSONを修復して採用すると、欠けた項目を推測で補うことになる。再開可能なAPIならカーソルから再接続し、そうでなければ未完了として再実行する。

切断と生成の取消は同じとは限らない

利用者が画面を閉じたり通信が切れたりしても、モデル提供者や自分のサーバーでは処理が続く場合がある。ブラウザからバックエンド、バックエンドからモデルAPIという二段の接続を分けて考える。

不要になった生成を止められるAPIでは、取消を明示する。停止できない場合は結果を捨てるだけで料金が発生し得ることを想定する。道具呼び出しが始まった後は、通信を切るだけで外部操作が元に戻るわけではない。

長時間処理を再接続可能にしたい場合は、前の記事で扱った非同期処理と組み合わせる。OpenAIのBackground modeでは、ストリームの連番をカーソルとして保持し、切断後に続きから受け取る方式が案内されている。通常の短い会話まで複雑な再開機構へ変える必要はなく、失われたときの影響で選ぶ。

プロキシのバッファリングで一括表示になる

アプリが増分を受け取っていても、CDN、リバースプロキシ、サーバーレス基盤、圧縮処理が応答をため込むと、ブラウザにはまとめて届く。開発環境で動いたことだけでは、本番のストリーミングを確認できない。

本番経路で最初のイベント時刻と各イベントの到着間隔を記録する。一定量たまるたびに大きな塊で届くなら、アプリコードより前にプロキシのバッファ設定、タイムアウト、圧縮、HTTP接続を調べる。

Server-Sent Eventsはサーバーからクライアントへの一方向通信である。双方向の音声や頻繁な入力更新が必要なら、WebSocketやWebRTCの方が適する。文章を一方向へ順次送るだけなら、SSEの構成は比較的単純である。

遅い利用者に合わせるバックプレッシャー

生成側が送る速度より、ネットワークや画面更新が遅いと、イベントがメモリへたまる。サーバーは書き込み待ちを無視せず、接続ごとのバッファ上限とタイムアウトを持つ。

画面側も一文字ごとに再描画せず、短い間隔でまとめて反映する。タブが非表示になったときや低速端末では更新頻度を下げても、受信データの順序と欠落確認は維持する。

同時接続数も監視する。長いストリームは一件当たりの接続保持時間が長く、通常の短いAPIより、ロードバランサーやブラウザの接続上限の影響を受けやすい。

途中表示はモデレーションを難しくする

完成後に全文を検査してから表示する方式では、不適切な出力を利用者へ見せる前に止められる。ストリーミングでは、後半で問題が判明する前に、先頭部分をすでに表示している可能性がある。

OpenAIの公式資料も、部分出力は評価しにくく、ストリーミングによってモデレーションが難しくなると注意している。高リスクな用途では、全文を検査してから表示する、短い区間をためて検査する、利用者や機能を限定するなど、待ち時間との交換条件を決める。

入力検査、出力上限、禁止操作、道具の権限制限は、ストリーミングの有無にかかわらず必要である。途中出力を消しても、すでに利用者が読んだ内容は取り消せない。

成功率と完了率を一緒に測る

ストリーミング導入後は、最初の文字が速くなったかだけでなく、最後まで正しく届いたかを測る。

  • 最初の意味ある表示までの時間
  • 全体の完了時間
  • 完了前の切断率
  • 再接続と取消の件数
  • 不完全回答を誤って保存した件数
  • 道具呼び出しと構造化出力の検証失敗率
  • 利用者が生成中に離脱した割合

開始が速くても切断が増え、途中回答が履歴へ残るなら改善ではない。オンライン経路では、体感速度と完了の正しさを別の指標として追う。

ストリーミングは、完成時間を魔法のように短くする機能ではない。生成中の状態を早く、正直に伝えるための通信と画面設計である。イベント種別、完了判定、途中表示、切断、モデレーションを先に決めれば、速く見せるだけでなく、未完成の結果を完成品として扱わない体験を作れる。

参照資料

生成AI・機械学習講座の一覧へ