ในยุคที่ผู้ใช้คาดหวังประสบการณ์แบบ real-time การ streaming response จาก LLM กลายเป็นฟีเจอร์ที่ขาดไม่ได้ โดยเฉพาะแอปพลิเคชันที่ต้องการความรวดเร็วในการตอบสนอง บทความนี้จะพาคุณสร้างระบบ Claude API streaming ด้วย Server-Sent Events (SSE) ตั้งแต่พื้นฐานจนถึง implementation จริงใน 3 use cases ยอดนิยม
ทำไมต้อง Streaming + SSE?
จากประสบการณ์การพัฒนาแชทบอทสำหรับร้านค้าออนไลน์ขนาดใหญ่ การใช้ streaming response ช่วยลด perceived latency ได้ถึง 60% เพราะผู้ใช้เห็นข้อความปรากฏทีละส่วนแทนที่จะรอทั้งหมด 2-3 วินาที Server-Sent Events เป็นทางเลือกที่เหนือกว่า WebSocket ในกรณีนี้เพราะใช้ HTTP protocol ปกติ ไม่ต้องตั้งค่า connection พิเศษ และรองรับ auto-reconnect อัตโนมัติ
Use Case 1: แชทบอท AI สำหรับ E-commerce
สถานการณ์จริงที่ผมเคยเจอคือร้านค้าออนไลน์ที่มีลูกค้าถามเรื่องสินค้าพร้อมกันหลายร้อยคนในช่วง Flash Sale ระบบเดิมที่รอ response ทั้งหมดก่อนแสดงผลทำให้ server timeout บ่อยครั้ง การใช้ streaming ช่วยให้ลูกค้าเห็นคำตอบเริ่มต้นภายใน 200ms แม้ข้อความยังไม่เสร็จสมบูรณ์
Use Case 2: Enterprise RAG System
องค์กรที่ต้องค้นหาเอกสารขนาดใหญ่ด้วย RAG (Retrieval-Augmented Generation) มักเจอปัญหาเดียวกัน คำตอบที่ถูกต้องอาจใช้เวลาสร้าง 5-10 วินาที streaming ช่วยให้ผู้ใช้ทางธุรกิจเห็นกระบวนการค้นหาและสังเคราะห์แบบ real-time สร้างความเชื่อมั่นว่าระบบทำงานจริง ไม่ใช่ค้าง
Use Case 3: โปรเจกต์นักพัฒนาอิสระ
สำหรับนักพัฒนาที่ต้องการสร้าง MVP (Minimum Viable Product) ภายในเวลาจำกัด การใช้ streaming ทำให้แอปของคุณดู premium กว่าคู่แข่งที่ใช้วิธีรอทั้งหมด คุณสามารถ implement ได้ใน 1 วันและเริ่มเก็บ feedback จากผู้ใช้ได้ทันที
พื้นฐาน Server-Sent Events
Server-Sent Events เป็น mechanism ที่ server ส่งข้อมูลให้ browser ผ่าน HTTP connection ที่เปิดค้างไว้ โดยส่ง events เป็น text/event-stream format ข้อดีหลักคือ automatic reconnection และ simplicity เมื่อเทียบกับ WebSocket ที่ต้องจัดการ protocol ที่ซับซ้อนกว่า
Implementation ด้วย HolySheep AI API
ก่อนเริ่มต้น คุณต้องมี API key จาก สมัครที่นี่ ซึ่ง HolySheep AI ให้บริการ Claude API compatible endpoint ที่รองรับ streaming โดยมีความหน่วงเพียง <50ms และราคาที่ประหยัดกว่าค่ายอื่นถึง 85%+ (Claude Sonnet 4.5 $15/MTok, GPT-4.1 $8/MTok) รวมถึงรองรับการชำระเงินผ่าน WeChat/Alipay สำหรับผู้ใช้ในประเทศจีน
1. Frontend: EventSource Client
// frontend/sse-client.js
class ClaudeStreamingClient {
constructor(apiKey, baseUrl = 'https://api.holysheep.ai/v1') {
this.apiKey = apiKey;
this.baseUrl = baseUrl;
this.eventSource = null;
}
// ส่งข้อความและรับ streaming response
async sendMessageStream(messages, onChunk, onComplete, onError) {
try {
// ส่ง POST request ไปยัง SSE endpoint
const response = await fetch(${this.baseUrl}/chat/completions, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': Bearer ${this.apiKey},
'Accept': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
},
body: JSON.stringify({
model: 'claude-sonnet-4-20250514',
messages: messages,
stream: true,
max_tokens: 2048,
temperature: 0.7
})
});
if (!response.ok) {
throw new Error(HTTP ${response.status}: ${response.statusText});
}
// อ่าน stream เป็น Reader
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let fullContent = '';
while (true) {
const { done, value } = await reader.read();
if (done) {
if (onComplete) onComplete(fullContent);
break;
}
// ถอดรหัส chunk และต่อกับ buffer
buffer += decoder.decode(value, { stream: true });
// ประมวลผล events ที่ complete แล้ว
const lines = buffer.split('\n');
buffer = lines.pop() || ''; // เก็บบรรทัดสุดท้ายไว้ใน buffer
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
// ตรวจสอบ SSE termination
if (data === '[DONE]') {
if (onComplete) onComplete(fullContent);
return;
}
try {
const parsed = JSON.parse(data);
// ดึง content จาก delta
const content = parsed.choices?.[0]?.delta?.content || '';
if (content) {
fullContent += content;
if (onChunk) onChunk(content, fullContent);
}
} catch (e) {
// ข้าม JSON parse error ที่เกิดจาก incomplete JSON
console.debug('Incomplete JSON in stream:', data);
}
}
}
}
} catch (error) {
if (onError) onError(error);
}
}
// ยกเลิก stream ที่กำลังทำงาน
cancel() {
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
}
}
// วิธีใช้งาน
const client = new ClaudeStreamingClient('YOUR_HOLYSHEEP_API_KEY');
const chatContainer = document.getElementById('chat');
const messages = [
{ role: 'system', content: 'คุณเป็นผู้ช่วยขายสินค้าออนไลน์ที่เป็นมิตร' },
{ role: 'user', content: 'แนะนำหูฟังไร้สายราคาดีให้หน่อย' }
];
let fullResponse = '';
client.sendMessageStream(
messages,
(chunk, full) => {
// อัพเดท UI ทีละ chunk
document.getElementById('response').textContent = full;
},
(complete) => {
console.log('Stream completed:', complete.length, 'characters');
},
(error) => {
console.error('Stream error:', error);
document.getElementById('response').textContent = 'เกิดข้อผิดพลาด: ' + error.message;
}
);
2. Backend: Express.js SSE Endpoint
// backend/server.js
import express from 'express';
import cors from 'cors';
import fetch from 'node-fetch';
const app = express();
app.use(cors());
app.use(express.json());
// Proxy SSE endpoint สำหรับ streaming
app.post('/api/chat/stream', async (req, res) => {
const { messages, model = 'claude-sonnet-4-20250514' } = req.body;
// ตั้งค่า headers สำหรับ SSE
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no'); // ปิด Nginx buffering
// Flush headers ทันที
res.flushHeaders();
try {
const response = await fetch('https://api.holysheep.ai/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': Bearer ${process.env.HOLYSHEEP_API_KEY}
},
body: JSON.stringify({
model: model,
messages: messages,
stream: true
})
});
if (!response.ok) {
throw new Error(Upstream API error: ${response.status});
}
// Pipe stream ไปยัง client
response.body.pipe(res);
// จัดการเมื่อ client disconnect
req.on('close', () => {
response.body.destroy();
});
} catch (error) {
console.error('SSE Error:', error);
res.write(data: ${JSON.stringify({ error: error.message })}\n\n);
res.end();
}
});
// Health check endpoint
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(🚀 SSE Server running on port ${PORT});
});
3. RAG System: Streaming with Context Injection
# backend/rag_streaming.py
import asyncio
import json
import aiohttp
from dataclasses import dataclass
from typing import AsyncIterator, List
@dataclass
class Document:
content: str
source: str
score: float
class RAGStreamingClient:
def __init__(self, api_key: str):
self.api_key = api_key
self.base_url = "https://api.holysheep.ai/v1"
async def retrieve_documents(self, query: str, top_k: int = 5) -> List[Document]:
"""
จำลองการ retrieve เอกสารจาก vector database
ใน production ใช้ ChromaDB, Pinecone, หรือ Weaviate
"""
# TODO: เชื่อมต่อ vector database จริง
mock_docs = [
Document(
content="นโยบายการคืนสินค้า: สามารถคืนได้ภายใน 30 วัน...",
source="policy_return.md",
score=0.95
),
Document(
content="วิธีการสั่งซื้อ: เลือกสินค้า > เพิ่มลงตะกร้า > ชำระเงิน...",
source="howto_order.md",
score=0.88
)
]
return mock_docs[:top_k]
async def stream_rag_response(
self,
query: str,
temperature: float = 0.3
) -> AsyncIterator[str]:
"""
Streaming RAG response พร้อมแสดง context ที่ใช้
"""
# Step 1: Retrieve relevant documents
docs = await self.retrieve_documents(query)
context = "\n\n".join([f"[{d.source}]: {d.content}" for d in docs])
# Step 2: Build system prompt พร้อม context
system_prompt = f"""คุณเป็นผู้ช่วยตอบคำถามลูกค้า
ใช้ข้อมูลจาก context ด้านล่างในการตอบ ถ้าไม่แน่ใจให้บอกว่าไม่ทราบ
---
CONTEXT:
{context}
---
กฎ: ตอบเป็นภาษาไทย กระชับ เข้าใจง่าย"""
# Step 3: Send streaming request
payload = {
"model": "claude-sonnet-4-20250514",
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": query}
],
"stream": True,
"temperature": temperature,
"max_tokens": 2048
}
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
async with aiohttp.ClientSession() as session:
async with session.post(
f"{self.base_url}/chat/completions",
json=payload,
headers=headers
) as response:
if response.status != 200:
error_text = await response.text()
yield f"error:API Error {response.status}"
return
# Process SSE stream
buffer = ""
async for line in response.content:
buffer += line.decode('utf-8')
# ประมวลผลทีละ event
while '\n\n' in buffer:
event, buffer = buffer.split('\n\n', 1)
if event.startswith('data: '):
data = event[6:] # ตัด 'data: ' ออก
if data == '[DONE]':
return
try:
json_data = json.loads(data)
content = json_data.get('choices', [{}])[0].get('delta', {}).get('content', '')
if content:
yield content
except json.JSONDecodeError:
continue
วิธีใช้งานใน FastAPI
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
app = FastAPI()
@app.post("/rag/stream")
async def rag_stream(query: str):
client = RAGStreamingClient(api_key="YOUR_HOLYSHEEP_API_KEY")
async def event_generator():
async for chunk in client.stream_rag_response(query):
yield f"data: {json.dumps({'token': chunk})}\n\n"
yield "data: [DONE]\n\n"
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive"
}
)
ทดสอบ
if __name__ == "__main__":
async def test():
client = RAGStreamingClient("YOUR_HOLYSHEEP_API_KEY")
query = "นโยบายการคืนสินค้าเป็นอย่างไร?"
print("Query:", query)
print("Response: ", end="", flush=True)
async for chunk in client.stream_rag_response(query):
print(chunk, end="", flush=True)
print("\n✅ Done")
asyncio.run(test())
ข้อผิดพลาดที่พบบ่อยและวิธีแก้ไข
1. CORS Error: No 'Access-Control-Allow-Origin' header
อาการ: Browser console แสดง error เกี่ยวกับ CORS เมื่อ client พยายามเชื่อมต่อ SSE endpoint จาก frontend
สาเหตุ: API ต้นทางไม่ได้ส่ง CORS headers ที่ถูกต้อง หรือ frontend อยู่คนละ origin กับ backend
// วิธีแก้ไข: ใช้ backend เป็น proxy แทนการเรียก API ตรงจาก browser
// ❌ วิธีที่ผิด - เรียก API ตรงจาก frontend
fetch('https://api.holysheep.ai/v1/chat/completions', ...);
// ✅ วิธีที่ถูก - สร้าง proxy endpoint ใน backend
// backend/routes/proxy.js
app.post('/api/proxy/chat', async (req, res) => {
const response = await fetch('https://api.holysheep.ai/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': Bearer ${process.env.HOLYSHEEP_API_KEY},
'Content-Type': 'application/json'
},
body: JSON.stringify(req.body)
});
// ตั้งค่า CORS headers ที่ backend
res.setHeader('Access-Control-Allow-Origin', 'https://your-frontend.com');
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// Pipe response กลับไปยัง client
response.body.pipe(res);
});
2. Stream หยุดกลางคันโดยไม่มี error
อาการ: Response แสดงเพียงบางส่วนแล้วหยุดลง response ไม่เสร็จสมบูรณ์
สาเหตุ: ปัญหาเกิดจาก buffering ของ Nginx หรือ CDN ที่รับ stream แต่ไม่ส่งต่อจนกว่าจะได้ complete chunk
# วิธีแก้ไข: ตั้งค่า Nginx ให้ปิด buffering สำหรับ SSE endpoints
nginx.conf
server {
location /api/ {
# ปิด proxy buffering
proxy_buffering off;
proxy_cache off;
# ตั้งค่า timeout ให้นานขึ้น
proxy_read_timeout 300s;
proxy_connect_timeout 75s;
# บังคับ flush ทุก 5 วินาที
proxy_flush 5s;
}
}
// ใน Express.js หรือ Node.js
app.post('/api/stream', (req, res) => {
res.setHeader('X-Accel-Buffering', 'no'); // สำหรับ Nginx
res.flushHeaders();
// ทำ manual flush ทุก 2 วินาที
const flushInterval = setInterval(() => {
res.flush();
}, 2000);
req.on('close', () => {
clearInterval(flushInterval);
});
});
3. JSON Parse Error ใน Stream Processing
อาการ: Console แสดง "Incomplete JSON" error แม้ว่า streaming จะทำงานได้
สาเหตุ: SSE ส่งข้อมูลเป็น chunks ที่อาจตัด JSON object กลาง ทำให้ JSON.parse ล้มเหลว
// วิธีแก้ไข: ใช้ buffer และ retry parsing
function processStreamChunk(buffer) {
// ลบ prefix 'data: '
let data = buffer.trim();
if (!data.startsWith('data: ')) return null;
data = data.slice(6);
// Terminal event
if (data === '[DONE]') return { done: true };
// ลอง parse ทีละครั้ง
try {
return { done: false, data: JSON.parse(data) };
} catch (e) {
// JSON ไม่ complete ให้ return null และเก็บไว้ใน buffer
return { done: false, partial: true, raw: data };
}
}
// วิธีใช้งานที่ถูกต้อง
let buffer = '';
let incompleteJson = '';
reader.read().then(function processResult({ done, value }) {
if (done) return;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop(); // เก็บบรรทัดสุดท้ายไว้
for (const line of lines) {
const result = processStreamChunk(line);
if (result === null) {
// เก็บบรรทัดที่ยังไม่ complete ไว้
incompleteJson += line;
continue;
}
if (result.done) {
onComplete();
return;
}
if (result.partial) {
incompleteJson += result.raw;
// ลอง parse รวมกับ incomplete
try {
const parsed = JSON.parse(incompleteJson);
onData(parsed);
incompleteJson = '';
} catch (e) {
// ยัง parse ไม่ได้ รอ chunk ถัดไป
}
} else {
onData(result.data);
}
}
// อ่าน chunk ถัดไป
reader.read().then(processResult);
});
4. Memory Leak จาก Stream ที่ไม่ถูก cancel
อาการ: Memory usage เพิ่มขึ้นเรื่อยๆ เมื่อเปิดหน้าเว็บทิ้งไว้นาน server ค่อยๆ ช้าลงจน crash
สาเหตุ: Stream connection ไม่ถูกปิดเมื่อ user ปิด tab หรือ navigate ไปหน้าอื่น
// วิธีแก้ไข: ใช้ AbortController และ cleanup on unmount
class StreamManager {
constructor() {
this.activeStreams = new Map();
}
async startStream(endpoint, options) {
// สร้าง AbortController ใหม่
const abortController = new AbortController();
const streamId = Date.now().toString();
this.activeStreams.set(streamId, abortController);
try {
const response = await fetch(endpoint, {
...options,
signal: abortController.signal
});
// ประมวลผล stream...
await this.processStream(response, options.onChunk);
} catch (error) {
if (error.name === 'AbortError') {
console.log('Stream cancelled by user');
} else {
options.onError?.(error);
}
} finally {
// cleanup ทุกครั้ง
this.activeStreams.delete(streamId);
}
}
// ยกเลิก stream เฉพาะตัว
cancelStream(streamId) {
const controller = this.activeStreams.get(streamId);
if (controller) {
controller.abort();
}
}
// ยกเลิกทุก stream (เรียกเมื่อ component unmount)
cancelAll() {
for (const controller of this.activeStreams.values()) {
controller.abort();
}
this.activeStreams.clear();
}
}
// ใช้ใน React component
function ChatComponent() {
const streamManagerRef = useRef(new StreamManager());
const [messages, setMessages] = useState([]);
useEffect(() => {
// ยกเลิก stream ทั้งหมดเมื่อ component unmount
return () => {
streamManagerRef.current.cancelAll();
};
}, []);
const sendMessage = async (text) => {
setMessages(prev => [...prev, { role: 'user', content: text }]);
await streamManagerRef.current.startStream('/api/chat', {
body: { messages: [{ role: 'user', content: text }] },
onChunk: (chunk) => {
setMessages(prev => {
const last = prev[prev.length - 1];
if (last?.role === 'assistant') {
return [...prev.slice(0, -1), {
...last,
content: last.content + chunk
}];
}
return [...prev, { role: 'assistant', content: chunk }];
});
}
});
};
// ยกเลิกเมื่อ user click ปุ่ม cancel
const handleCancel = () => {
streamManagerRef.current.cancelAll();
};
}
สรุป
การ implement Claude API streaming ด้วย Server-Sent Events เป็นเทคนิคที่ช่วยเพิ่ม user experience อย่างมีนัยสำคัญ โดยผู้ใช้เห็นคำตอบเริ่มต้นภายใน 200-500ms แทนที่จะรอ 2-10 วินาที ในบทความนี้ครอบคลุม 3 use cases หลัก ได้แก่ แชทบอท e-commerce, RAG system และ MVP สำหรับนักพัฒนาอิสระ พร้อมโค้ดตัวอย่างที่รันได้จริงและวิธีแก้ไขปัญหาที่พบบ่อย 4 กรณี
หากคุณกำลังมองหา API provider ที่ราคาประหยัดและ latency ต่ำ สมัครที่นี่ เพื่อรับเครด