LLMの逐次表示は、待ち時間の印象を改善します。しかし、画面に数文字届いたこと、モデルが最後まで生成したこと、利用者が受け取ったこと、外部操作が確定したことは同じ出来事ではありません。スマートフォンの回線切替、ブラウザの「停止」、Workerのタイムアウト、tool呼び出しの途中切断を一つの「失敗」として扱うと、利用者には未完の文が残り、裏側では課金や更新だけが継続する、あるいは再接続時に二重実行する状態になります。
結論は、streamを表示の輸送路として扱い、業務上の完了は別の確定状態として持つことです。具体的には、(1) 途中deltaは表示専用、(2) providerの終端eventを受けて初めて生成完了、(3) toolや更新は完全な引数を検証してから一度だけ実行、(4) 再接続は同じ実行IDの状態を読みに行く、に分けます。これは9月12日のモデル切替設計とは別の論点です。あちらが「どのモデルへ切り替えるか」の契約なら、本稿は同じ実行を利用者へどう届け、いつ中断・確定するかの設計です。
deltaを業務データにしない
OpenAIのResponses APIはstream=trueでSSEを返し、response.output_text.delta、response.completed、errorなどの型付きeventを送ります。したがって画面へdeltaを描画しても、response.completedを受けるまで回答を保存済み・送信可能とマークしません。AnthropicのMessages APIもSSEですが、最終はmessage_stopで、過負荷はstream内のerror eventとして届きます。さらにtoolの引数は途中では部分JSONであり、content_block_stop後に初めて完全なobjectとして扱えます。providerごとに終端eventとエラー表現が違うため、「接続が閉じたら成功」という共通化は危険です。
画面には、実行ID、現在の段階(生成中/tool待ち/確定/中断)、最後に検証済みのdelta番号だけを持たせます。生成中に切れても、利用者へは「回答を再接続中」か「中断しました」を明示し、本文を完成回答として会話履歴へ昇格させません。構造化出力やtool引数は、表示中の文字列から正規表現で取り出さず、providerの完了イベント後にschema検証します。
clientの切断を、上流の中断へ意図的に伝える
Cloudflare Workersはenable_request_signal互換フラグを有効にすると、clientがWorkerへのrequestを取り消したことをRequest.signalで検知できます。さらにrequest_signal_passthroughでは、そのAbortSignalをfetch()したsubrequestへ渡せます。これは「画面を閉じたら上流も必ず停止する」という自動保証ではなく、互換設定を含めた明示的な伝播経路です。利用者の停止を生成停止として扱いたい処理だけ、上流fetchへsignalを渡します。
Cloudflareの2025年5月22日付changelogも、clientによる取消しをRequest.signalのabort eventで検知する例を示しています。これはcancelを検知する入口の説明であり、toolや外部API側の処理が巻き戻る保証ではありません。
一方、切断後にも残すべきなのは、監査ログ、使用量の集計、実行状態の確定です。Cloudflareのctx.waitUntil()はresponse送信またはclient切断後に最大30秒まで延長できるものの、結果に必要な処理をそこへ逃がす用途ではありません。回答や書込みの成否をそこへ置けば、30秒上限や実行終了と競合します。確定が必要な処理はstream中にawaitし、長い処理はQueueやDurableな状態へ渡して、画面の接続寿命と切り離します。
再接続は「もう一度生成」ではなく、実行の継続確認にする
SSEのEventSourceには再接続とLast-Event-IDという仕組みがあります。ただし、これは配送されたeventの続きを要求できる仕組みであり、LLM providerが同じ生成を再開すること、toolを再実行しないことまでは保証しません。OpenAIとAnthropicでもevent名、終端、tool deltaの形式が異なります。そこでアプリ側にrun_id、連番、生成状態、最終イベント、tool実行状態、確定結果への参照を保存します。
再接続したclientはrun_idを送り、サーバーは未配信の表示deltaを返せるなら返し、返せない場合も確定済みの最終結果か明確な中断状態を返します。新しいprovider requestを作るのは、前の実行が「未開始」または安全に中断済みと確定した時だけです。外部操作にはrun_idとは別の冪等性キーを置き、tool_callを受けた時点ではなく、認可・引数・実行前条件を通してから確定結果を記録します。この分離が、再読込でメール送信やレコード更新が二度走る事故を避けます。
最小の状態遷移を先に決める
最初から複雑なresume機構は不要です。created → streaming → awaiting_tool → committing → completedと、どの段階からも入れるcancelled/failedを持つだけでよいでしょう。streamingの文字列は利用者向けの暫定表示、committingは変更を一度だけ確定する段階です。completed以外を業務完了として検索・通知・請求に流さない、と決めます。各遷移にrun ID、provider request ID、終了理由、送受信token、切断時刻だけを記録すれば、本文を無制限に残さず障害調査を始められます。
限界と確認範囲
streamingは応答開始を速く見せられますが、正答性や完了を速くする機能ではありません。provider側で生成が止まったか、接続だけが切れたか、既にtoolが実行済みかは製品・実装に依存します。外部送信、削除、支払い、権限変更は途中deltaを根拠に進めず、通常の認可・承認・冪等性・監査を別に適用してください。
導入時の最小チェックリスト
- 画面に表示したdeltaと、providerの終端eventを受けた確定回答を分けているか
- provider別の終端event、stream内error、部分tool JSONをテストしているか
- client切断を検知する互換設定と、上流へAbortを渡す処理を意図的に選んだか
- 再接続時に同じ
run_idの状態を読み、無条件に新規生成していないか - toolの外部効果に、実行前条件と冪等性キー、確定記録を置いたか
情報図解
結論: 「表示」「生成完了」「外部効果」を一本のstreamに混ぜない。
- 表示: deltaを
run_idと連番で配信し、切断時は暫定表示のまま止める。 - 確定: providerの終端event後にschema・終了理由を確認し、回答状態を
completedにする。 - 効果: 完全なtool引数を認可・前提条件・冪等性キーで検証し、結果を一度だけ記録する。
制約: SSEの再接続は配送の再試行であり、LLM生成・tool実行の再開保証ではない。provider仕様とWorkerの互換フラグは実装時に再確認する。
一次根拠
- OpenAI: Streaming API responses
- Anthropic: Streaming messages
- Cloudflare: Streams
- Cloudflare: Request cancellation
- Cloudflare: Compatibility flags
- WHATWG: Server-sent events
一次情報と確認範囲
確認日:2026年9月13日。製品仕様・制度は更新されるため、導入時はリンク先の最新版を再確認してください。海外の制度・職業規範は日本へ直接適用せず、相違点と限界を本文に明記しています。
- OpenAI Responses APIは`stream=true`でSSEを使い、`response.output_text.delta`、`response.completed`、`error`を含む型付きeventを扱う
- Anthropicはstream内で`message_stop`、`error`、部分JSONのtool deltaを定義し、tool inputはblock停止後に完全objectになる
- Workersは`enable_request_signal`でclientによるrequest取消しを検知できる
- Cloudflareは`Request.signal`のabort eventでclient取消しを検知するWorker例を公開した
- Workersのstreamはresponse bodyが完了するかclientが切断するまで実行を進め、`waitUntil()`は切断後最大30秒の延長に使える
- SSEのEventSourceは再接続時に`Last-Event-ID`をrequest headerへ設定し得る
