3秒でわかる
データを全部そろえてから送らず、できた分から順に流す通信方式。待ち時間を短くし、巨大なデータも少ないメモリで扱うために使います。
もう少し詳しく
どういうものか
ストリーミングは、データ全体が用意できるのを待たずに、生成できた部分から順に相手へ流していく方式です。受け取る側も届いた分から処理を始めます。動画配信の言葉として知られていますが、通信やファイル処理の一般的な手法でもあります。HTTPではchunked transfer encoding、Server-Sent Events、WebSocketが代表的な実現手段です。
なぜ必要か
理由は2つあります。1つは待ち時間です。LLMが2000文字の回答を作るのに20秒かかるとして、全部そろってから送ると利用者は20秒間まっ白な画面を見ます。生成した分から流せば、1秒後には文字が出始めます。処理時間は変わりませんが、体感は別物になります。もう1つはメモリです。5GBのCSVを配列に読み込むとメモリが尽きますが、1行ずつ流して処理すれば数MBで済みます。
具体例
// Server-Sent Events で<a href="/glossary/server" class="text-primary font-medium underline underline-offset-2 hover:text-primary-dark">サーバー</a>から逐次送る
res.writeHead(200, {
"<a href="/glossary/content-type" class="text-primary font-medium underline underline-offset-2 hover:text-primary-dark">Content-Type</a>": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
});
for await (const token of generate(prompt)) {
res.write(`data: ${JSON.stringify({ token })}\n\n`);
}
res.end();// 受け取り側は届いた断片をその場で描画する
const reader = (await <a href="/glossary/fetch-api" class="text-primary font-medium underline underline-offset-2 hover:text-primary-dark">fetch</a>("/api/chat")).body
.pipeThrough(new TextDecoderStream())
.getReader();
while (true) {
const { value, done } = await reader.read();
if (done) break;
output.textContent += value;
}つまずきやすいところ
途中に置かれたリバースプロキシが応答を溜め込むと、サーバーが逐次送っていても利用者には最後にまとめて届きます。nginxでは該当する設定を切る必要があり、原因が分からず数時間溶ける定番の落とし穴です。もうひとつは分割の境目です。届く断片はトークンやJSONの単位で切れているとは限らず、途中で切れた文字列をそのままJSONとして解釈しようとすると失敗します。バッファに溜め、区切りが現れた分だけ処理します。エラー処理も難しくなります。すでにヘッダを送り出したあとでは、失敗しても500を返せません。異常はデータ本文の中で伝える設計にします。
似た用語との違い
| 方式 | 向き |
|---|---|
| ストリーミング | サーバーからの一方向で逐次送る |
| WebSocket | 双方向にいつでも送り合える |
| ポーリング | クライアントが定期的に聞きに行く |