AIの回答をストリーミング表示するには、サーバーが型付きイベントを中継し、画面側で途中表示と確定結果を分けて管理します。 本記事では、キャンセル、切断、部分出力、完了判定までを実装手順に沿って解説します。
AI回答のストリーミング表示にSSEを採用する前提と実行環境
ストリーミング表示が解決するのは、回答全体が完成するまで何も見えない待ち時間です。OpenAIの公式ガイドは、Responses APIでstream: trueを指定し、HTTP上のServer-Sent Eventsとして型付きイベントを受け取る方式を説明しています。代表的なイベントは作成、テキスト差分、完了、エラーであり、差分文字列だけを成功の根拠にしてはいけません[1]。
想定構成は、Node.js 20以上のバックエンド、OpenAI JavaScript SDK、HTTPストリームを中継できるWebサーバー、AbortControllerを扱うブラウザ画面、ステージング用Projectです。APIキーはサーバーの環境変数か鍵管理サービスから読み、ブラウザへ配布しません。公式APIリファレンスもBearer資格情報をクライアント側コードへ露出せず、サーバーで管理するよう示しています[2]。
| 項目 | 検証設定 | 開始できない状態 |
|---|---|---|
| 基準経路 | 同じ入力を一括応答で正しく表示できる | 非ストリーミングでも失敗原因が分からない |
| 中継 | プロキシのバッファを無効化し、長時間接続を許可する | 応答が最後まで蓄積され、途中で届かない |
| 認証 | 利用者認証とOpenAI資格情報をサーバーで分離する | ブラウザがOpenAI APIを直接呼び出す |
| 状態 | 準備中、受信中、完了、失敗、中止を画面に持つ | 部分出力を完了済みとして保存する |
| 試験 | 正常20件、遅延8件、切断8件、拒否4件の計40件 | 切断とキャンセルを再現する方法がない |
ストリーミングが向かないケース
全文を検査してからでなければ一文字も見せられない審査回答、途中の表現が誤解や損害を生む法務・医療・金融の通知、数百ミリ秒で完了する短い定型応答では、複雑さに対する効果が小さくなります。部分出力を許可できない場合は、進行表示だけを出し、回答は完了後に一括表示します。
接続経路の確認方法として、約1,200字の固定回答を使い、25%受信時の利用者中止、75%受信時の中継切断、完了直前のブラウザ再読込を各5回再現します。合格条件は、途中文が確定履歴へ入らないこと、古い要求の差分が再実行後の画面へ混ざらないこと、上流接続の終了を要求IDで確認できることです。これは製品性能の実績値ではなく、状態遷移と保存境界を確かめるための試験例です。自社の平均応答長と通信経路に合わせて件数を増やしてください。
途中表示と確定結果を分ける完成形
完成形では、ブラウザが自社APIへ認証済み要求を送り、自社サーバーがOpenAIへ接続します。サーバーはテキスト差分だけを画面へ中継し、ツール呼び出しの引数、内部エラー、APIキーは流しません。画面は受け取った差分を「生成中」の領域へ追記し、完了イベントを受けた後にだけ確定表示へ変更します。保存やコピー、後続処理のボタンは完了前に無効化します。
利用者が中止を押した場合は、ブラウザの受信を止めるだけでなく、自社サーバーのAbortSignalへ伝え、上流接続も中断します。ただし通信の時間差により、キャンセル直前までに利用量が発生する可能性は残ります。そのため「中止したから費用はゼロ」と表示せず、状態をcancel_requestedからcancelledへ遷移させ、サーバーが終了を確認するまで再送ボタンを待たせます。
部分テキストは完成回答の早送り表示ではなく、後から変更・中断され得る暫定情報です。確定、保存、実行の境界を見た目とコードの両方で分けます。
| 層 | 持つ責任 | 持たせない情報 |
|---|---|---|
| ブラウザ | 利用者認証、表示状態、中止操作、再接続案内 | OpenAI APIキー、内部プロンプト |
| 自社サーバー | 入力検査、API認証、イベント選別、監査ログ | 未加工の秘密情報を含む永続ログ |
| OpenAI API | イベント生成、完了・失敗状態、リクエストID | 自社画面の確定・保存判断 |
サーバー中継と画面更新の実装手順
手順1:一括応答を基準として固定する
同じモデル、指示、入力で一括応答を実装し、品質、認証、タイムアウト、入力上限、エラー表示を先に通します。ストリーミング版と同じケースIDを使い、最終回答が基準経路と許容範囲内で一致するか比較できる状態を作ります。一括応答は障害時の切り戻し先にもなるため、削除せず機能フラグで選べるようにします。
手順2:バックエンドで意味のあるイベントだけを中継する
OpenAIのResponses APIはイベントにtypeを持たせています。公式ガイドではresponse.created、response.output_text.delta、response.completed、errorが主要イベントとして示されています[1]。自社サーバーは型を列挙して処理し、未知のイベントをテキストとして画面へ流しません。
const stream = await openai.responses.create({ model: process.env.OPENAI_MODEL, input: sanitizedInput, stream: true});for await (const event of stream) { if (event.type === "response.output_text.delta") { sendToBrowser("delta", { text: event.delta }); } else if (event.type === "response.completed") { sendToBrowser("done", { responseId: event.response.id }); } else if (event.type === "error") { sendToBrowser("failed", { code: "UPSTREAM_STREAM_ERROR" }); }}
実コードでは、自社のtrace_id、利用者IDの匿名化値、開始時刻、OpenAIのリクエストIDをログへ結び付けます。APIキーと入力本文はイベントへ含めません。ブラウザへ返すエラーは公開可能な社内コードへ変換し、上流の詳細は運用ログだけに残します。
手順3:画面を状態機械として更新する
画面状態をidle、connecting、streaming、completed、failed、cancelledへ限定します。差分受信時は一文字ごとにDOMを更新せず、50〜100ミリ秒単位でまとめて描画します。読み上げソフトへ大量の差分を通知しないよう、生成中領域は必要以上にライブ更新せず、完了時に確定回答を通知します。
手順4:キャンセルを上流へ伝播する
一件の要求につき一つのAbortControllerを作り、利用者の中止、タブ終了、自社サーバーの期限超過を同じ中断処理へ集約します。中止後に届いた遅延イベントは画面へ反映せず、別要求の差分と混ざらないよう要求IDを照合します。中止直後の再実行には新しいIDを発行し、古い接続の完了イベントで新画面を確定させないようにします。
手順5:確定時だけ保存と操作を解放する
完了イベントを受け、最終テキストの検査を通した時点で、保存、コピー、次の処理を有効にします。エラーや切断で終わった途中文は画面に残す場合でも「未完了」と明示し、正式な回答履歴へ登録しません。ツール呼び出しを併用する場合、途中引数を実行せず、引数完成イベントと権限検査を経てからサーバー側で処理します。
切断・重複・部分出力の障害例
通信が始まった後の失敗は、HTTP要求そのものが失敗した場合より見落としやすくなります。200相当で接続できても、途中にエラーイベントが届く、プロキシが接続を切る、利用者が中止する、完了イベントだけ失われる場合があります。途中文を正式回答として扱わず、接続、受信、完了の各段階を別々に記録します。
| 症状 | 観測点 | 利用者表示 | 復旧方法 |
|---|---|---|---|
| 最後にまとめて表示される | 自社プロキシのバッファ、圧縮、フラッシュ設定 | 処理中のまま待たせる | 中継設定を直し、SSEの到着間隔を測る |
| 同じ文が二重になる | 再接続時の要求ID、描画バッファの再利用 | 重複部分を確定させない | 新しい要求として分離し、古いイベントを破棄する |
| 途中で無言停止する | 完了イベント、接続期限、ブラウザ中断 | 未完了と再実行案内を示す | 自動再送せず、同一IDの状態を照会する |
| 中止後も費用が増える | 上流Abortの到達、サーバー側処理の終了時刻 | 中止要求中と中止済みを分ける | 切断伝播を修正し、上限時間を設ける |
| 401または403 | Project、資格情報、許可IP、権限 | 認証障害として再試行を止める | 認証担当が資格情報を確認する |
| 429・500・503 | エラーコード、リクエストID、発生率 | 一括表示への切替か後で再試行を案内 | 回数制限付き待機または旧経路へ戻す |
OpenAIのエラー資料は401、429、500、503の原因と対処を区別しており、429にはレート制限だけでなく利用額上限なども含まれます[3]。ステータス番号だけを見て同じ要求を再送すると、上限超過や二重処理を悪化させます。エラーコードとストリーム状態を読み、再試行可能な一時障害だけを対象にします。
もう一つの重要な障害は、途中表示した内容が最終検査に合格しないことです。公式ガイドは、部分出力は評価が難しく、本番でのストリーミングはモデレーションを難しくすると注意しています[1]。全文検査が必須の画面では生成中テキストを公開せず、進捗表示と完了後の回答だけを見せます。
UXと通信を同時に確かめる試験
40件の固定試験では、最終回答の品質だけでなく、最初の差分までの時間、完了、失敗、中止、再実行、画面の操作可否を記録します。品質比較には同じ入力の一括応答を使い、ストリーミング化によって最終テキストや安全判定が変わっていないかを確認します。固定入力と期待結果の継続評価にはOpenAIのEvals公式資料を参照します[4]。
| 試験 | 操作・注入条件 | 合格条件 |
|---|---|---|
| 通常完了 | 長さの異なる20入力を送る | 差分順序が保たれ、完了後だけ保存可能になる |
| 遅い開始 | 最初のイベントを3秒遅延させる | 接続中表示が出て二重送信を防ぐ |
| 中途切断 | 50%受信時に上流を切る | 途中文へ未完了表示が付き、履歴に確定保存されない |
| 利用者中止 | 受信中に中止を3回行う | 上流Abortが届き、古い差分が次要求へ混ざらない |
| 認証失敗 | ステージングの失効資格情報を使う | 401を一回で停止し、秘密情報を画面へ出さない |
| 読み上げ | キーボードと読み上げソフトで操作する | 状態名が伝わり、差分の連続通知で操作を妨げない |
リリース必須条件:- 完了イベントのない回答が確定保存される件数 = 0- 中止後に別要求へ混ざる差分 = 0- 一括応答との重大な内容差 = 0- 40件の完了率 >= 97.5%- TTFTのP95 <= 2.0秒- 全体完了時間のP95 <= 一括応答基準の1.15倍
最後の二つはこの画面向けの想定基準です。TTFTが短くても全体完了が極端に遅い場合や、速く表示できても途中文の誤解が増える場合は合格にしません。端末、回線、ブラウザ、応答長を試験記録へ残し、社内高速回線だけの結果を利用者全体へ一般化しないようにします。
一括応答と並べて進める段階リリース
初回リリースは社内利用者の10%に限定し、回答内容は従来の一括応答と同じ保存処理へ渡します。機能フラグはstreaming_uiとし、モデルやプロンプトを同時に変更しません。三営業日の観測期間に、TTFT、完了時間、完了率、中止率、再実行率、問い合わせ件数を比較します。利用者が待ち時間を短く感じても、失敗後の再実行が増えた場合は改善と判断しません。
次の50%へ進む条件は、受入試験の必須条件を本番観測でも満たし、サポート担当が未完了と中止の問い合わせを手順書だけで切り分けられることです。公開範囲を広げる日には、一括応答へ戻す操作、プロキシ設定の監視、資格情報の所有者、障害時の利用者告知文を確認します。ストリーミング機能を止めても入力済みの質問と完了済み回答が失われない設計を維持します。
イベント状態機械と表示ルール
状態機械を作る目的は、届いたイベントの数ではなく、利用者が今できる操作を一意に決めることです。connectingでは中止だけ、streamingでは中止とスクロールだけ、completedでは保存とコピー、failedでは再実行と問い合わせ、cancelledでは入力編集を許可します。同じ画面で複数要求を並行させる場合も、状態は要求IDごとに持ちます。
| 現在 | 受け取る事象 | 次の状態 | 画面動作 |
|---|---|---|---|
| idle | 送信操作 | connecting | 入力を固定し、要求IDを表示する |
| connecting | response.created | streaming | 生成中領域を開始する |
| streaming | output_text.delta | streaming | 差分を描画バッファへ追加する |
| streaming | response.completed | completed | 最終検査後に保存操作を解放する |
| connecting / streaming | errorまたは通信切断 | failed | 途中文を未完了として区別する |
| connecting / streaming | 利用者の中止 | cancelled | Abortを送り、古いイベントを無視する |
未知のイベントを受けた場合は、互換性のため無視するだけで終えず、型名とSDK版を記録して運用通知を出します。公式API概要は、ストリーミングAPIへの新しいイベント型追加を後方互換の変更として挙げています[2]。すべての未知イベントを障害にすると将来の追加で停止し、すべてを画面へ流すと内部情報が露出するため、記録して非表示という扱いが現実的です。
監視ログで一件の接続を再構成する
一件の記録には、社内要求ID、利用者の匿名ID、画面版、モデル、開始時刻、最初のイベント時刻、最初の文字時刻、完了時刻、終了状態、中止元、入力・出力トークン、再試行回数、OpenAIリクエストIDを保存します。差分本文を一片ずつログへ書くと容量と個人情報リスクが増えるため、内容調査が必要なときだけ、承認済み保存先の確定回答へ関連付けます。
アラートは接続数ではなく利用者影響へ結び付けます。5分間の完了率が95%未満、TTFTのP95が2秒超、切断率が3%超、中止要求から上流終了まで10秒超のいずれかを監視します。401は一件で認証担当へ通知し、429は要求率とトークン率を分けて基盤担当へ送ります。部分出力が確定保存された場合は件数に関係なく重大障害です。
問い合わせ対応では「表示が止まった時刻」と画面の要求IDを聞き、社内ログからOpenAIリクエストIDへたどります。公式APIリファレンスは本番調査のためx-request-idの記録を推奨しており、必要なら自社のX-Client-Request-Idも送れます[2]。入力本文をサポート担当へ共有しなくても、接続経路と終了状態は切り分けられます。
費用・性能・完了率の評価票
ストリーミングは通常、生成される内容を短くする機能ではありません。費用を評価する際は、入力・出力トークン、キャンセルまでに生成された利用量、障害後の再実行、監視基盤の費用を含めます。次は実績ではなく想定例です。月20,000件、API費用120,000円、切断後の再実行が400件で追加4,000円、監視費が16,000円なら、一件当たり運用費は(120,000+4,000+16,000)÷20,000=7円です。
計測点はブラウザとサーバーで分けます。ブラウザでは送信操作から最初に描画した時刻をTTFT、確定表示までを利用者完了時間として記録します。サーバーではOpenAIへの送信、最初の上流イベント、最後のイベント、ブラウザ切断、上流終了を測ります。ブラウザTTFTが遅くサーバー側が速ければ中継や描画を疑い、両方が遅ければモデル・入力長・上流待機を調べます。OpenAIのopenai-processing-msはAPI側処理の手掛かりですが、利用者が体験したネットワークと描画を含まないため、これだけでUXを合格にしません。
キャンセル率は低いほどよいとは限りません。利用者が誤送信を止められることは正常な操作だからです。問題にするのは、中止後も上流接続が長く残る割合と、同じ入力が直後に再送された割合です。想定例として中止200件のうち20件が10秒以内に終了しなければ、上流終了遅延率は20÷200×100=10%です。この値と中止要求後の追加出力トークンを並べると、操作が表示だけで終わっていないか判断できます。
| 評価軸 | 配点 | 満点の条件 | 失格条件 |
|---|---|---|---|
| 体感性能 | 25 | TTFT P95が2秒以下 | 開始表示が一括応答より遅い |
| 完了信頼性 | 25 | 完了率99%以上、混線ゼロ | 別要求の差分を表示 |
| 最終品質 | 20 | 一括応答との重大差ゼロ | 未検査の途中文を確定 |
| 操作性 | 15 | 中止、再実行、読み上げが全試験で合格 | 中止できない接続が残る |
| 費用 | 15 | 一件当たり費用が予算内 | 利用量を要求単位で追えない |
90点以上かつ失格条件ゼロを次段階の条件とします。TTFTだけを短くするため、最初に意味のない定型文を生成させる施策は採用しません。利用者が最初に判断可能な情報を見た時刻も補助指標として測り、文字が届いた時刻とのずれを確認します。数字は端末、回線、回答長をそろえた同じ期間で一括応答と比較します。
ブラウザ・回線・プロキシを変える試験マトリクス
開発端末だけでSSEが動いても、企業ネットワーク、CDN、リバースプロキシ、モバイル回線で同じとは限りません。Chrome、Edge、Safariの現行社内対応版、PCとスマートフォン、高速回線と遅延200ミリ秒の模擬回線を組み合わせます。応答長は短文、約1,000字、約4,000字に分け、各条件で到着間隔と終了状態を取ります。
| 経路 | 注入する条件 | 確認する結果 |
|---|---|---|
| 社内プロキシ経由 | レスポンス圧縮と標準バッファ | 差分が最後まで蓄積されない |
| モバイル回線 | 遅延200ミリ秒、1%パケット損失 | 画面が固まらず、中止操作を受け付ける |
| バックグラウンドタブ | 受信中に別タブへ5秒移動 | 復帰時に重複せず、状態が維持される |
| 再送操作 | 接続中に送信ボタンを連打 | 一要求だけが有効になり、二重課金を抑える |
| サーバー再起動 | 応答中に中継プロセスを終了 | 未完了表示となり、一括経路を案内する |
失敗ケースでは画面だけでなく、自社サーバーに接続終了が残ったか、上流接続が解放されたか、トークン利用量を要求へ関連付けられたかを確認します。再現手順と合格条件をCIのネットワーク試験またはステージング手順へ組み込み、プロキシやSDKの更新後に同じ組み合わせを再実行します。
停止条件と一括表示への切り戻し
部分出力の誤保存、別利用者への差分混入、APIキー露出、禁止された内容の途中公開は一件でストリーミングを停止します。完了率が95%未満、切断率が3%超、TTFTのP95が4秒超、一括応答より再実行率が2ポイント以上悪化した状態が15分続いた場合も、機能フラグを無効にします。これらは想定の初期値であり、本番基準は業務責任者と基盤担当が事前に承認します。
streaming_uiを無効にし、新規要求を一括応答へ送る。- 進行中の接続へ終了案内を返し、上流Abortを実行する。
- 未完了の要求IDを抽出し、正式回答として保存されていないか確認する。
- プロキシ、画面版、SDK、モデルの変更履歴から最初の差分を特定する。
- 40件の固定試験と障害再現が合格するまで利用率を0%に保つ。
内容公開に関する事故は、通信担当だけで復旧を決めません。セキュリティ、法務・コンプライアンス、業務責任者へ証跡を渡し、表示された範囲と利用者を確定します。認証情報がブラウザへ出た疑いがある場合は、機能停止と同時に資格情報を失効し、ログ・キャッシュ・監視サービスへの残存も調べます。
運用へ引き継ぐ責任と日次確認
フロントエンド担当は表示状態、二重送信防止、アクセシビリティを所有します。バックエンド担当は資格情報、イベント選別、中断伝播、要求IDを担当します。基盤運用はプロキシ、接続数、タイムアウト、アラートを監視し、業務責任者は部分表示を許す範囲と停止後の利用者案内を承認します。どの担当も単独で公開範囲を100%へ上げません。
| 役割 | 毎日見るもの | 変更時の承認 |
|---|---|---|
| 画面担当 | 失敗表示、混線、中止後の操作 | 状態遷移とUI変更 |
| API担当 | 完了率、Abort、認証エラー | SDK、モデル、イベント処理 |
| 基盤運用 | 同時接続、TTFT、切断、プロキシ | 接続期限と中継設定 |
| 業務責任者 | 再実行率、問い合わせ、内容事故 | 対象利用者と部分公開範囲 |
引き継ぎ完了は資料配布ではなく、担当者が要求IDから一件の接続を追い、一括応答へ切り替え、未完了回答を抽出できた時点です。日次で重大条件と前日差を見て、週次で評価票、月次で費用とブラウザ構成、四半期で部分表示を許す業務範囲を再承認します。
小規模リリースを判定する
次の行動は、既存の一括応答を残したまま、40件の固定試験をステージングで実行することです。状態遷移表、TTFTと完了時間、キャンセル伝播、途中文の保存禁止、認証失敗、プロキシ経由、読み上げ操作を一つの受入記録へまとめます。90点以上かつ失格条件ゼロになったら、社内利用者10%の三営業日観測へ進みます。
観測中はモデルやプロンプトを変えず、ストリーミング有無だけを比較します。途中表示によって待ち時間が短く感じられても、完了率、最終品質、一件当たり費用、サポート問い合わせのいずれかが悪化した場合は範囲を広げません。一括表示へ戻す操作を担当者が再現できることを、公開承認の最後の条件にします。
判定記録には、公開率を上げる人と即時に0%へ戻せる当番を実名で割り当て、承認時刻を残します。
翌営業日には未完了要求が正式回答へ混入していないことも再確認します。
参考文献・出典
- OpenAI公式ドキュメント「Streaming API responses」(版・更新日表記なし、2026年7月31日参照)。AIの回答をストリーミング表示する際のSSEイベント、完了判定、部分出力の安全上の注意に使用。
- OpenAI公式APIリファレンス「API Overview」(REST API v1、openai-version 2020-10-01、更新日表記なし、2026年7月30日参照)
- OpenAI公式ドキュメント「Error codes」(版・更新日表記なし、2026年7月30日参照)
- OpenAI公式ドキュメント「Evals」(版・更新日表記なし、2026年7月30日参照)