welclaiAI·TREND·DIGEST
チュートリアル

モデルの出力をUIでストリーミング・描画する

なぜストリーミングがAI機能を速く感じさせるのか、そしてちらつき・崩れたマークアップ・レイアウトの混乱なしにトークン単位の出力をUIに描画する方法。

tutorials2026-04-26 10:23 KST·編集長·7

速く感じられるAI機能と壊れて感じられるAI機能の違いは、しばしばモデルではありません。出力をストリーミングするかどうかです。完全な答えを生成するのに数秒かかるモデルは、ユーザーがその間ずっとスピナーを見つめていれば、耐えがたく遅く感じられます。同じモデルでも、最初の言葉がほぼ即座に現れて残りが流れ込んでくれば、反応が良く感じられます。ストリーミングは長い待ち時間をライブな応答へと変える技術であり、それをうまく描画することは学ぶ価値のある小さな職人技です。

なぜストリーミングが体験を変えるのか

通常のリクエストをすると、何かが見える前に応答全体を待つことになります。長い答えなら、それは何もない状態を長く見つめることです。ストリーミングは待ち時間の形を変えます。最後に一つの大きな遅延があるのではなく、モデルが生成しながら出力を少しずつ送り、あなたは届いた各部分を表示します。

完了までの総時間はほぼ同じです。変わるのは体感速度です。最初のトークンまでの時間、つまり何かが現れるまでの時間が数分の一秒に落ち、人間はたとえ全体の生成が遅くても、ストリーミングする応答を速いものとして読みます。これはプログレスバーと固まった画面の心理と同じです。作業は同一ですが、体験は同一ではありません。人が待つものには、ストリーミングはほぼ必須です。

ストリーミングの高レベルな仕組み

内部では、ストリーミング応答は一つの最終ペイロードではなく一連の小さなイベントを届ける、長寿命の接続です。各イベントは出力の一片、しばしば数文字またはトークンを運びます。あなたのコードはこれらのイベントを届いた順に読み、各片をそれまでに蓄積したものに追加し、表示を更新します。ストリームが完了を知らせると、一片ずつ組み立てられた完全な応答が手元にあります。

擬似コードではループはシンプルです。

accumulated = ""
for chunk in stream(request):
    accumulated += chunk.text
    render(accumulated)
on_complete():
    finalize(accumulated)

プロバイダーのSDKがトランスポートの詳細を扱います。あなたの仕事はループです。チャンクを読み、蓄積し、描画し、終了を処理する。興味深い問題はほぼすべて描画側にあります。

デルタではなく蓄積されたテキストを描画する

ストリームを描画する第一のルールは、最新のチャンクだけでなく蓄積された文字列を表示することです。各デルタを生のテキストとしてそのままDOMに追加したくなりますが、何らかの書式が必要になった瞬間に破綻します。マークダウン、コードブロック、構造化された出力は、全体としてのみ意味をなします。チャンクは単語、マークダウンのトークン、タグを半分に分割するかもしれません。デルタを独立に描画すると、文字化けした部分的なマークアップが得られます。

代わりに、完全な蓄積テキストを状態に保持し、更新のたびに再描画しましょう。最新のUIフレームワークはこれを安価にします。状態の一つの文字列を更新すれば、フレームワークが表示を効率的に調整します。毎回蓄積された答え全体を描画することは、マークダウンや構文ハイライトが常に「これまで完全な」テキストを見ることも意味し、孤立した断片よりはるかに優雅に解析できます。

部分的・不正な中間状態を扱う

応答がストリーミングしている間、すべての中間状態は定義上不完全です。コードブロックには開きのフェンスがあってもまだ閉じがないかもしれません。マークダウンのリンクは入力途中かもしれません。リストは項目の途中で止まるかもしれません。レンダラーが厳格なら、これらの部分状態はちらついたりエラーを投げたりします。

解決策は寛容に描画することです。終端のない構造をエラーにせず優雅に扱うマークダウンパーサーを使い、表示が進行中の書式を一瞬見せ、テキストが届くにつれて解決していくことを受け入れましょう。とりわけコードブロックについては、開いたフェンスを検出し、閉じるまで残りをコードとして扱うと役立ちます。原則は、「このテキストはまだ完成していない」をストリーミング中の通常のケースとして設計することです。実際それが通常なのですから。

レイアウトのずれとスクロールを御する

ストリーミングする応答は伸びていき、伸びるとページが動きます。配慮がなければ、新しい行が現れるにつれ応答の下のコンテンツが飛び跳ね、上部を読もうとするユーザーは下へ引きずられます。二つの習慣がこれを穏やかに保ちます。

第一に、スペースを確保し、無関係なコンテンツの再フローを避けます。ストリーミングするテキストを、レイアウトの残りを予測不能に押し回すことなく下方向に伸びるコンテナに描画します。第二に、スクロールを意図的に扱います。よくある心地よい挙動は、ユーザーがすでに最下部にいる間は表示を最下部に固定してライブ出力を追えるようにし、しかし何かを読もうと上にスクロールした瞬間に自動スクロールを止めて、ユーザーと争わないようにすることです。ユーザーが最下部近くにいるかを検出し、いるときだけ自動スクロールしましょう。

更新の頻度も抑えましょう。速いストリームでは一トークンごとの再描画はブラウザを圧倒しかねません。更新を妥当な間隔、たとえば1フレームに数回程度にまとめると、反応性を目立って損なうことなくUIを滑らかに保てます。

状態を示し、中断を扱う

ストリーミングは状態を伝える自然な機会を与えます。最初のトークンの前に生成が始まったことを示し、進行中であることを明確に示し、完了したことを示しましょう。ストリーミング中の控えめなカーソルや「停止」ボタンは、システムが生きていて働いていることをユーザーに伝えます。

その停止ボタンは重要です。ストリームはライブな接続なので、途中でキャンセルできます。長すぎる、あるいは間違った応答を最後まで待たせるのではなく、ユーザーに中断する手段を与えましょう。リクエストをキャンセルし、それまでに届いたテキストを保持し、制御を返します。エラー側では、ストリームは途中で失敗しうるので、切れた接続を回復可能な状態として扱い、部分的な出力を保ち、すべてを破棄するのではなく再試行を提供しましょう。最初からキャンセルと部分的失敗を見越して設計するほうが、後付けするよりはるかに簡単です。

まとめ

ストリーミングは、AI機能の体感に与えられる最も安価な大きなアップグレードです。作業は同じでも、即座に現れることは速さとして読まれます。ループでチャンクを読み、生のデルタではなく常に蓄積されたテキストを描画して書式を保ちましょう。すべての中間状態は未完成なので寛容に解析し、レイアウトのずれと自動スクロールを御して読みやすさを保ち、滑らかさのために更新を抑えます。最後に、キャンセルと部分的失敗を第一級の状態として扱いましょう。これらを正しく行えば、遅いモデルでも反応良く感じられます。それこそ、ユーザーが実際に判断するもののほとんどなのです。

#streaming#ui#latency#frontend