Next.js で LLM のストリーミング応答を扱う実装パターン#
OpenAI(や Anthropic)の Chat Completions API でストリーミング応答を Next.js サーバーから受けて、ブラウザにリアルタイム表示する実装パターン。Server-Sent Events(SSE) が標準解。
全体のデータフロー#
sequenceDiagram
participant B as ブラウザ
participant N as Next.js API
participant O as OpenAI API
B->>N: POST /api/chat
N->>O: stream: true でリクエスト
O-->>N: chunk 1
N-->>B: SSE chunk 1
O-->>N: chunk 2
N-->>B: SSE chunk 2
O-->>N: [DONE]
N-->>B: SSE close
実装の要点#
1. Node.js の Readable stream を使う
Next.js の Response に直接 ReadableStream を渡す。OpenAI SDK の openai.chat.completions.create({ stream: true }) を for await で回す。
export async function POST(req: Request) {
const openai = new OpenAI()
const stream = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [...],
stream: true,
})
const encoder = new TextEncoder()
const readable = new ReadableStream({
async start(controller) {
for await (const chunk of stream) {
const text = chunk.choices[0]?.delta?.content || ''
if (text) controller.enqueue(encoder.encode(text))
}
controller.close()
},
})
return new Response(readable, {
headers: { 'Content-Type': 'text/event-stream' },
})
}
2. クライアント側は ReadableStream を読む
const res = await fetch('/api/chat', { method: 'POST', body })
const reader = res.body!.getReader()
const decoder = new TextDecoder()
while (true) {
const { value, done } = await reader.read()
if (done) break
setText(prev => prev + decoder.decode(value))
}
ハマりどころ#
1. バッファリングで止まる
CDN やリバースプロキシ(Vercel, Cloudflare 等)がレスポンスをバッファリングして、ストリームが届かないことがある。
- 対策:
Cache-Control: no-cache, no-transformを返す - 対策(Vercel): Edge Runtime を使う(
export const runtime = 'edge')
2. 中断処理
ユーザーがページを閉じた後も OpenAI 側でトークンが消費され続ける。
- 対策:
AbortControllerを使い、クライアント切断時にアップストリームも中断する
sequenceDiagram
participant B as ブラウザ
participant N as Next.js API
participant O as OpenAI API
B->>N: POST /api/chat
N->>O: stream リクエスト
O-->>N: chunk 1
B-xN: 切断
N->>N: AbortController.abort()
N->>O: 中断
3. エラーハンドリング
ストリーム途中でエラーが起きた場合、すでに 200 OK を返しているので HTTP ステータスでエラーを示せない。
- 対策: ストリーム内に JSON 形式のエラーイベントを埋め込む
学び#
- ストリーミングはバッファリングが最大の敵。本番環境で動かないときは、まずプロキシのバッファリング設定を疑う
- クライアント切断をアップストリームに伝播させる。放置するとコストが膨らむ
- エラーはストリーム内で伝える設計にする。HTTP ステータスだけでは不十分