ストリーミング出力が必要な理由

非ストリーミング(stream=false)は完全なレスポンスが返るまで待つため、ユーザーは長時間の空白待ちを強いられます。ストリーミング出力(stream=true)は生成内容をトークン単位で段階的に返すため、ユーザーはテキストがリアルタイムで表示されるのを確認でき、体験が大幅に向上します。DeepSeek V4 は OpenAI 互換の SSE(Server-Sent Events)ストリーミングプロトコルをサポートしています。

Python でのストリーミング呼び出し

from openai import OpenAI
import os

client = OpenAI(
    api_key=os.environ.get('DEEPSEEK_API_KEY'),
    base_url='https://api.deepseek.com'
)

stream = client.chat.completions.create(
    model='deepseek-v4-flash',
    messages=[{'role': 'user', 'content': 'Python を紹介する500字の記事を書いて'}],
    stream=True
)

for chunk in stream:
    if chunk.choices[0].delta.content:
        print(chunk.choices[0].delta.content, end='', flush=True)

Node.js でのストリーミング呼び出し

import OpenAI from 'openai';

const openai = new OpenAI({
  baseURL: 'https://api.deepseek.com',
  apiKey: process.env.DEEPSEEK_API_KEY,
});

const stream = await openai.chat.completions.create({
  model: 'deepseek-v4-flash',
  messages: [{ role: 'user', content: '機械学習とは何か説明してください' }],
  stream: true,
});

for await (const chunk of stream) {
  const content = chunk.choices[0]?.delta?.content || '';
  process.stdout.write(content);
}

フロントエンドでの一文字ずつの描画

fetch と ReadableStream を使ってフロントエンドでタイプライター効果を実装します:

async function streamChat(prompt) {
  const response = await fetch('https://api.deepseek.com/v1/chat/completions', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + apiKey
    },
    body: JSON.stringify({
      model: 'deepseek-v4-flash',
      messages: [{ role: 'user', content: prompt }],
      stream: true
    })
  });

  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let fullText = '';

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    const chunk = decoder.decode(value);
    const lines = chunk.split('\n').filter(l => l.startsWith('data: '));
    for (const line of lines) {
      if (line === 'data: [DONE]') break;
      const data = JSON.parse(line.slice(6));
      const content = data.choices[0]?.delta?.content;
      if (content) {
        fullText += content;
        document.getElementById('output').textContent = fullText;
      }
    }
  }
}

ストリーミング出力の中断

AbortController を使用してユーザーが生成を停止できるようにします:

const controller = new AbortController();
// 中断シグナルを fetch にバインド
const response = await fetch(url, { signal: controller.signal, ... });
// ユーザーが停止ボタンをクリック
 document.getElementById('stopBtn').onclick = () => controller.abort();

思考モードのストリーミング処理

思考モードを有効にすると、ストリーミング出力に reasoning_content と content が交互に現れます。それぞれを個別に処理できます:思考プロセスは折りたたみ可能な領域にまとめ、最終的な回答は直接表示します。

ベストプラクティス

  • デフォルトでストリーミングを使用:完全な JSON 解析が必要でない限り、stream=true に設定
  • 再接続メカニズムを追加:ネットワーク中断時に自動的に再試行
  • レンダリングをスロットル:フロントエンドで requestAnimationFrame を使用して DOM 更新をバッチ処理
  • エラーハンドリング:ストリーミングエラーをキャッチしてユーザーに通知
  • タイムアウト制御:無限待ちを避けるために適切なタイムアウトを設定