Streaming
モデル出力を段階的に受信し、イベント、差分、完了、切断を正しく処理します。
Streamingを使用すると、モデルが完全な回答を生成し終える前に、クライアントは差分を継続して受信できます。チャット画面、長文生成、最初の結果を早く表示したい場面に適していますが、クライアントはイベント境界、内容の連結、完了状態、接続中断を正しく処理する必要があります。
Streamingは返却方法を変えますが、タスク自体は変えません
同じタスクでは通常、非ストリーミングまたはストリーミングを選択できます。先に非ストリーミングリクエストを完了してからStreamingを有効にすると、リクエストパラメータの問題とストリーム解析の問題を区別しやすくなります。
使用前の確認
- モデルマーケットプレイスで、対象モデルが選択したプロトコルとStreamingに対応していることを確認します。
- プロトコルごとに対応するエンドポイントとイベント構造を使用し、1つのパーサーですべてのプロトコルを推測しないでください。
- クライアント、リバースプロキシ、ゲートウェイがレスポンス全体をバッファリングしてから一括返却しないことを確認します。
完全なフィールドは、Chat Completions、Responses、Anthropic Messages、Gemini Stream Generate Contentの各Referenceで管理されています。
最小構成のストリーミングリクエスト
次の例では、Chat Completionsでストリーミング出力を有効にします。
curl --no-buffer 'https://api.agtcloud.cn/v1/chat/completions' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer your-api-key' \
--data '{
"model": "MODEL_ID",
"messages": [
{"role": "user", "content": "指数バックオフを3つのポイントで説明してください。"}
],
"stream": true
}'--no-bufferを指定すると、cURLは受信済みのデータをすぐに表示します。サーバー側のイベント構造は変わりません。
ネットワークパケットではなくイベント単位で読み取る
HTTP接続からクライアントへ渡される1回分のバイト数は、完全なイベント1件とは限りません。1つのJSONオブジェクトが複数のデータブロックに分割されたり、複数のイベントが1つのデータブロックで届いたりする場合があります。
SSEでは、まず空行で完全なイベントを識別し、その中のdata:を読み取ります。
const response = await fetch('https://api.agtcloud.cn/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer your-api-key',
},
body: JSON.stringify({
model: 'MODEL_ID',
messages: [{ role: 'user', content: '指数バックオフとは何か説明してください。' }],
stream: true,
}),
});
if (!response.ok || !response.body) {
throw new Error(`リクエストに失敗しました:${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const events = buffer.split('\n\n');
buffer = events.pop() ?? '';
for (const event of events) {
const data = event
.split('\n')
.filter((line) => line.startsWith('data:'))
.map((line) => line.slice(5).trimStart())
.join('\n');
if (!data || data === '[DONE]') continue;
const chunk = JSON.parse(data);
const delta = chunk.choices?.[0]?.delta?.content;
if (delta) process.stdout.write(delta);
}
}本番コードでは、リクエストのキャンセル、読み取りタイムアウト、ハートビート、JSON以外のイベント、ストリーム終了時に未処理のバッファーも扱う必要があります。
4つのプロトコルのストリーミング構造
Chatは通常Chat Completion Chunkを返します。テキストはchoices[].delta.content、ツール引数はchoices[].delta.tool_callsに含まれる場合があり、終了理由はfinish_reasonにあります。イベントストリームは[DONE]で終了する場合があります。
プロトコルのエンドポイントが存在しても、すべてのモデルが上記のすべてのコンテンツタイプを返すとは限りません。アプリケーションで実際に使用し、対象モデルから返却を確認した分岐のみを実装してください。
テキストとツール引数を連結する
- テキスト差分は候補、出力項目、Content Blockのインデックスごとに連結します。
- ツール名、呼び出しID、引数は複数回に分かれて届く場合があるため、呼び出しごとに独立したバッファーを保持します。
- ツール引数は完全に終了してからJSONとして解析し、データブロックごとに実行しないでください。
- 推論サマリー、Thinking Block、最終回答は別々のバッファーを使用し、1つのテキストに混在させないでください。
- プロトコルが最後にUsageを返す場合は、完了イベントを受信するまで接続を読み続けます。
成功、失敗、中断を判断する
| 状況 | クライアントの処理 |
|---|---|
| プロトコル所定の完了イベントを受信 | 完全な結果とUsageを保存し、リクエストを成功として記録する |
| エラーまたは失敗イベントを受信 | 表示済みの内容を保存しつつ、このリクエストを失敗として記録する |
| 接続が途中で終了 | 不完全として記録し、既存テキストを完全な回答として扱わない |
| ユーザーが停止 | 読み取りをキャンセルし、画面に「生成を停止しました」と表示する |
| 長時間新しいイベントがない | アプリケーションのタイムアウト方針に従ってキャンセルし、必要な調査情報を保存する |
ストリーミングリクエストがすでに一部の内容を表示した後は、無制限に自動再送しないでください。再試行によって異なる回答が生成されたり、ツールなどの副作用を伴う処理が重複したりする可能性があります。
公開前の確認
- 接続終了まで待たず、ページにテキストが段階的に表示される。
- 日本語やマルチバイト文字がデータブロックの分割によって文字化けしない。
- ツール引数が呼び出しIDごとに正しく連結され、実行前に検証される。
- ユーザーキャンセル、ネットワーク切断、タイムアウト、サーバーエラーに明確な状態がある。
- ログにAPI Key、機密性の高い完全なPrompt、機密情報を除いていないツール引数を保存しない。
次にTool Callingとエラーとトラブルシューティングを参照してください。