開発解説
ローカルLLM APIのストリーミングを扱う方法|途中・完了・失敗を分ける

目次
ストリーミングを使うと、モデルが回答を最後まで生成する前から画面へ文字を表示できる。
ただし実装では、「小さなJSONが順番に返ってくる」とだけ覚えると、途中で切れた回答を完成扱いしてしまうことがある。
重要なのは、受信途中のデータと、正常に確定した最終回答を分けることだ。
LM StudioのOpenAI互換Responses APIでは、ストリーミング時に作成、テキストの差分、完了などを示すイベントが順番に返る仕組みが案内されている。実際のイベントにはcreated、テキスト差分を示すイベント、completedなどがある。
一方、Ollamaなど別のAPIでは形式が同じとは限らない。使用しているAPIの公式資料を基準にする。
クライアント内部で3つの状態を分ける
最低限、次を別に持つ。
1. 受信した生のイベント・データ
2. 画面表示へ使う途中の文字列バッファー
3. 正常終了後に確定したメッセージ
画面表示用のバッファーは、受信するたびに更新してよい。
ただし、まだ生成途中の内容を、そのまま保存済みの会話履歴や次のリクエストへ渡す確定メッセージとして扱うとは限らない。
1回届いたデータだけで文章の完成を判断しない
テキストの差分は文章の途中で届く。文字列やツール呼び出しなど、複数回の受信を集めて初めて意味が完成するデータもある。
受信単位ごとに「これは完成した1回答」と判定せず、イベントの種類とAPI仕様に従って蓄積する。
UTF-8の文字列処理も、利用するHTTPライブラリやSSE処理の仕組みに合わせて行い、バイト列の途中を誤って文字列化しない。
正常終了・エラー・切断・キャンセルを分ける
「データが来なくなった」だけでは、何が起きたのか分からない。
少なくとも次を分ける。
- APIが正常終了を通知した
- サーバーがエラーを返した
- ネットワーク接続が途中で切れた
- 利用者が生成をキャンセルした
これらをすべて同じ「完了」として扱うと、途中までの文章を成功した回答として保存してしまう。
JavaScriptやTypeScriptでAbortControllerなどを使って利用者側から停止する場合も、正常終了とは別の状態を持つ。
保存する前に完了状態を確認する
画面には生成途中の文章を表示していても、会話履歴へ確定保存する処理は終了状態を確認してから行う。
たとえば内部状態を次のように分けられる。
receiving 受信中
completed 正常終了
failed エラー・通信切断
cancelled 利用者が中止
状態名は自由だが、「停止した」という1状態だけにまとめない。
最初に正常終了と意図的なキャンセルを1回ずつ試す
まず短い入力で、最後まで正常に生成できるケースを1回保存する。
次に、少し長い生成を始めて途中で利用者側からキャンセルする。
その2回について、
画面表示:
途中バッファー:
保存された会話:
終了状態:
サーバーログ:
を比べる。
正常終了と途中キャンセルを区別できれば、その後に再接続、ツール呼び出しのストリーミング、より複雑なUIを追加しやすい。
最初の成功条件は文字が滑らかに表示されることではなく、途中・成功・失敗・キャンセルを正しく区別して保存できることだ。
まずcurlで通常のAPIリクエストを確認したい場合は、ローカルLLM APIへcurlで最初のリクエストを送る方法へ戻る。TypeScriptでクライアントを作る場合は、TypeScriptからローカルLLM APIを呼ぶ方法も確認できる。
まとめ
- ストリーミングで届く途中のデータを、そのまま最終回答として保存しない
- 受信するイベントやデータ形式はAPIごとに確認し、提供元が違っても同じ形式だと決めつけない
- 正常終了、サーバー側のエラー、通信切断、利用者によるキャンセルを別の状態として扱う
- 画面へ表示する途中の文字列と、会話履歴へ保存する確定済みメッセージを分ける
公式サイトで次に進む
この記事で明示的に扱うソフトだけを表示します。配布・手順は各公式ページで確認してください。
LM Studio
Ollama
local-gen.jpがファイルを配布するものではなく、互換性を保証するものでもありません。
この記事で扱ったデータ
モデル・GPU・実行ソフトの確認済みデータを、記事とは別に確かめられます。