ในยุคที่ผู้ใช้คาดหวังประสบการณ์แบบ 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 ต่ำ สมัครที่นี่ เพื่อรับเครด