การสร้างแอปพลิเคชัน AI ที่ตอบสนองได้รวดเร็วในยุคปัจจุบันไม่ใช่แค่การเรียก API แบบธรรมดา แต่ต้องออกแบบระบบ streaming ที่ทำงานได้อย่างเสถียรภายใต้เงื่อนไขการเชื่อมต่อที่ไม่เสถียร ในบทความนี้ผมจะแบ่งปันประสบการณ์จริงจากการสร้างระบบ streaming ขนาดใหญ่ที่รองรับผู้ใช้หลายหมื่นรายพร้อมกัน โดยเน้นกลไก resumable stream และ incremental synchronization ที่ทำให้แอปพลิเคชันของเราทำงานได้แม้เครือข่ายขาดหายกลางคัน
WebSocket Streaming: พื้นฐานและข้อจำกัด
WebSocket เป็นเทคโนโลยีที่เหมาะสมที่สุดสำหรับ AI streaming เพราะให้ full-duplex communication ผ่านการเชื่อมต่อ TCP เดียว ต่างจาก HTTP polling ที่ต้องส่ง request ใหม่ทุกครั้ง แต่ WebSocket มีข้อจำกัดสำคัญคือเมื่อการเชื่อมต่อขาดหาย ข้อมูลที่กำลังส่งอยู่จะสูญหายทั้งหมด และ client ต้องเริ่มต้นใหม่จากต้น
สำหรับ AI streaming โดยเฉพาะ model ที่มี context ยาว การเริ่มต้นใหม่ทุกครั้งไม่ใช่แค่เสียเวลา แต่ยังเสีย cost อย่างมากเพราะต้องส่ง context ทั้งหมดไปยัง API ใหม่ ดังนั้นเราต้องออกแบบระบบ resumable stream ที่จะรักษาสถานะของการ streaming ไว้ที่ server และอนุญาตให้ client ขอ resume จากจุดที่ขาดหายได้
สถาปัตยกรรม Resumable Stream Session
แนวคิดหลักคือการแยก "การสร้าง session" ออกจาก "การส่งข้อมูล" แทนที่จะส่ง HTTP request ไปยัง API โดยตรง เราจะสร้าง streaming session ก่อน แล้วใช้ session ID เพื่อเชื่อมต่อผ่าน WebSocket เพื่อรับ stream และสามารถ resume ได้เมื่อการเชื่อมต่อขาดหาย
Server-Side Session Management
ในฝั่ง server เราต้องเก็บ state ของแต่ละ streaming session ซึ่งประกอบด้วย request parameters, context ที่ถูกส่งไปยัง AI API, ข้อมูลที่ถูกส่งไปแล้ว (ในรูปแบบ array ของ tokens), และ metadata อื่นๆ เช่น timestamp และ client ID
Client-Side Reconnection Strategy
ฝั่ง client ต้องมี logic สำหรับ auto-reconnect และ resume request ซึ่งประกอบด้วย heartbeat เพื่อตรวจจับการขาดหายของ connection, exponential backoff สำหรับการ reconnect, และการ request stream ใหม่โดยส่ง session ID และ offset ที่ต้องการ resume
Incremental Synchronization สำหรับ UI Updates
อีกหนึ่งความท้าทายคือการอัปเดต UI อย่างมีประสิทธิภาพ หากเรา re-render component ทั้งหมดทุกครั้งที่ได้รับ token ใหม่ จะทำให้เกิด performance issue อย่างมาก โดยเฉพาะเมื่อข้อความยาวหลายพันตัวอักษร เราจึงใช้ incremental synchronization โดยส่ง delta updates แทน full state replacement
Diff-Based Updates
แทนที่จะส่งข้อความทั้งหมดทุกครั้ง เราจะส่งเฉพาะส่วนที่เปลี่ยนแปลง โดยใช้ diff algorithm เพื่อคำนวณความแตกต่างระหว่าง previous state กับ current state แล้วส่งเฉพาะ operation ที่จำเป็น เช่น "insert at position X: 'abc'" หรือ "delete from position Y to Z"
Batch Updates for High Frequency
สำหรับ AI model ที่ให้ output เร็วมาก (เช่น 100+ tokens ต่อวินาที) เราต้อง batch updates เพื่อลด overhead ของ network และ UI rendering โดยกำหนด batch interval เช่น ทุก 50ms หรือเมื่อ accumulated delta ถึง threshold ที่กำหนด
โค้ดตัวอย่าง: Python Backend สำหรับ HolySheep AI
ด้านล่างคือโค้ด production-ready ที่ใช้งานจริงสำหรับการสร้าง resumable streaming session กับ HolySheep AI ซึ่งให้ latency ต่ำกว่า 50ms และรองรับ WebSocket streaming อย่างเต็มรูปแบบ ราคาของ HolySheep AI ประหยัดมากถึง 85%+ เมื่อเทียบกับผู้ให้บริการอื่น (¥1=$1)
import asyncio
import json
import uuid
import time
from typing import Dict, Optional, List, Callable
from dataclasses import dataclass, field, asdict
from enum import Enum
import httpx
import websockets
from websockets.server import WebSocketServerProtocol
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class SessionStatus(Enum):
PENDING = "pending"
STREAMING = "streaming"
COMPLETED = "completed"
FAILED = "failed"
EXPIRED = "expired"
@dataclass
class StreamToken:
index: int
content: str
timestamp: float = field(default_factory=time.time)
is_final: bool = False
@dataclass
class StreamingSession:
session_id: str
request_params: dict
context: str
tokens: List[StreamToken] = field(default_factory=list)
status: SessionStatus = SessionStatus.PENDING
created_at: float = field(default_factory=time.time)
last_activity: float = field(default_factory=time.time)
client_id: Optional[str] = None
error_message: Optional[str] = None
def add_token(self, content: str, is_final: bool = False) -> StreamToken:
token = StreamToken(
index=len(self.tokens),
content=content,
is_final=is_final
)
self.tokens.append(token)
self.last_activity = time.time()
return token
def get_tokens_since(self, offset: int) -> List[StreamToken]:
return self.tokens[offset:]
def get_full_content(self) -> str:
return "".join(t.content for t in self.tokens)
def to_dict(self) -> dict:
return {
**asdict(self),
"status": self.status.value,
"full_content": self.get_full_content()
}
class ResumableStreamManager:
"""Manager for resumable streaming sessions with HolySheep AI"""
def __init__(
self,
api_key: str,
base_url: str = "https://api.holysheep.ai/v1",
session_ttl: int = 3600,
max_sessions: int = 10000
):
self.api_key = api_key
self.base_url = base_url
self.session_ttl = session_ttl
self.max_sessions = max_sessions
self.sessions: Dict[str, StreamingSession] = {}
self._lock = asyncio.Lock()
self._cleanup_task: Optional[asyncio.Task] = None
async def start(self):
"""Start background cleanup task"""
self._cleanup_task = asyncio.create_task(self._cleanup_loop())
logger.info(f"StreamManager started, base_url={self.base_url}")
async def stop(self):
"""Stop background tasks"""
if self._cleanup_task:
self._cleanup_task.cancel()
try:
await self._cleanup_task
except asyncio.CancelledError:
pass
async def create_session(
self,
prompt: str,
model: str = "gpt-4.1",
client_id: Optional[str] = None,
**kwargs
) -> StreamingSession:
"""Create a new resumable streaming session"""
async with self._lock:
if len(self.sessions) >= self.max_sessions:
await self._evict_oldest()
session_id = str(uuid.uuid4())
session = StreamingSession(
session_id=session_id,
request_params={
"model": model,
"messages": [{"role": "user", "content": prompt}],
"stream": True,
**kwargs
},
context=prompt,
client_id=client_id
)
self.sessions[session_id] = session
logger.info(f"Created session {session_id[:8]}...")
return session
async def get_session(self, session_id: str) -> Optional[StreamingSession]:
"""Get session by ID"""
return self.sessions.get(session_id)
async def stream_completion(
self,
session: StreamingSession,
websocket: WebSocketServerProtocol
) -> None:
"""Stream AI completion to WebSocket client with token buffering"""
session.status = SessionStatus.STREAMING
accumulated_text = ""
buffer = []
BATCH_INTERVAL = 0.05 # 50ms batching
last_batch_time = time.time()
try:
async with httpx.AsyncClient(timeout=120.0) as client:
async with client.stream(
"POST",
f"{self.base_url}/chat/completions",
headers={
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
},
json=session.request_params
) as response:
if response.status_code != 200:
error_text = await response.aread()
raise Exception(f"API error: {response.status_code} - {error_text}")
async for line in response.aiter_lines():
if not line or not line.startswith("data: "):
continue
data = line[6:] # Remove "data: " prefix
if data == "[DONE]":
break
try:
chunk = json.loads(data)
delta = chunk.get("choices", [{}])[0].get("delta", {})
content = delta.get("content", "")
if content:
token = session.add_token(content)
buffer.append({
"index": token.index,
"content": content,
"timestamp": token.timestamp
})
accumulated_text += content
# Batch send every 50ms
if time.time() - last_batch_time >= BATCH_INTERVAL:
await websocket.send(json.dumps({
"type": "tokens",
"data": buffer,
"session_id": session.session_id
}))
buffer = []
last_batch_time = time.time()
except json.JSONDecodeError:
continue
# Send remaining buffer
if buffer:
await websocket.send(json.dumps({
"type": "tokens",
"data": buffer,
"session_id": session.session_id
}))
session.status = SessionStatus.COMPLETED
await websocket.send(json.dumps({
"type": "complete",
"session_id": session.session_id,
"full_content": session.get_full_content(),
"total_tokens": len(session.tokens)
}))
except Exception as e:
session.status = SessionStatus.FAILED
session.error_message = str(e)
logger.error(f"Streaming error for session {session.session_id[:8]}: {e}")
await websocket.send(json.dumps({
"type": "error",
"session_id": session.session_id,
"error": str(e)
}))
async def resume_session(
self,
session_id: str,
offset: int,
websocket: WebSocketServerProtocol
) -> bool:
"""Resume streaming from specific offset"""
session = await self.get_session(session_id)
if not session:
await websocket.send(json.dumps({
"type": "error",
"error": f"Session {session_id} not found"
}))
return False
# If already completed, send all tokens from offset
if session.status == SessionStatus.COMPLETED:
tokens = session.get_tokens_since(offset)
await websocket.send(json.dumps({
"type": "tokens",
"data": [
{"index": t.index, "content": t.content, "timestamp": t.timestamp}
for t in tokens
],
"session_id": session_id
}))
await websocket.send(json.dumps({
"type": "complete",
"session_id": session_id,
"full_content": session.get_full_content(),
"total_tokens": len(session.tokens)
}))
return True
# If still streaming, reconnect and continue
if session.status == SessionStatus.STREAMING:
# Send current state first
tokens = session.get_tokens_since(offset)
await websocket.send(json.dumps({
"type": "resume_ack",
"session_id": session_id,
"offset": offset,
"current_content": session.get_full_content()
}))
if tokens:
await websocket.send(json.dumps({
"type": "tokens",
"data": [
{"index": t.index, "content": t.content, "timestamp": t.timestamp}
for t in tokens
],
"session_id": session_id
}))
return True
return False
async def _cleanup_loop(self):
"""Periodic cleanup of expired sessions"""
while True:
await asyncio.sleep(300) # Every 5 minutes
try:
now = time.time()
expired = [
sid for sid, s in self.sessions.items()
if now - s.last_activity > self.session_ttl
]
for sid in expired:
async with self._lock:
del self.sessions[sid]
if expired:
logger.info(f"Cleaned up {len(expired)} expired sessions")
except Exception as e:
logger.error(f"Cleanup error: {e}")
async def _evict_oldest(self):
"""Evict oldest session when at capacity"""
if not self.sessions:
return
oldest = min(self.sessions.items(), key=lambda x: x[1].last_activity)
del self.sessions[oldest[0]]
logger.warning(f"Evicted oldest session {oldest[0][:8]}...")
WebSocket Server Example
stream_manager: Optional[ResumableStreamManager] = None
async def websocket_handler(ws: WebSocketServerProtocol, path: str):
"""Handle WebSocket connections"""
client_id = ws.remote_address[0] if ws.remote_address else "unknown"
logger.info(f"Client connected: {client_id}")
try:
async for message in ws:
data = json.loads(message)
msg_type = data.get("type")
if msg_type == "create":
# Create new streaming session
session = await stream_manager.create_session(
prompt=data["prompt"],
model=data.get("model", "gpt-4.1"),
client_id=client_id,
temperature=data.get("temperature", 0.7),
max_tokens=data.get("max_tokens", 4096)
)
await ws.send(json.dumps({
"type": "session_created",
"session_id": session.session_id
}))
# Start streaming
await stream_manager.stream_completion(session, ws)
elif msg_type == "resume":
# Resume from offset
await stream_manager.resume_session(
session_id=data["session_id"],
offset=data.get("offset", 0),
websocket=ws
)
elif msg_type == "ping":
await ws.send(json.dumps({"type": "pong"}))
except websockets.exceptions.ConnectionClosed:
logger.info(f"Client {client_id} disconnected")
except Exception as e:
logger.error(f"WebSocket error: {e}")
await ws.send(json.dumps({"type": "error", "error": str(e)}))
async def main():
global stream_manager
API_KEY = "YOUR_HOLYSHEEP_API_KEY" # ใช้ HolySheep API key
stream_manager = ResumableStreamManager(
api_key=API_KEY,
base_url="https://api.holysheep.ai/v1",
session_ttl=3600
)
await stream_manager.start()
async with websockets.serve(websocket_handler, "0.0.0.0", 8765):
logger.info("WebSocket server started on ws://0.0.0.0:8765")
await asyncio.Future() # Run forever
if __name__ == "__main__":
asyncio.run(main())
โค้ดตัวอย่าง: TypeScript Client พร้อม Auto-Reconnect
import React, { useState, useEffect, useRef, useCallback } from 'react';
interface StreamToken {
index: number;
content: string;
timestamp: number;
}
interface StreamState {
sessionId: string | null;
content: string;
tokens: StreamToken[];
status: 'idle' | 'connecting' | 'streaming' | 'completed' | 'error';
error: string | null;
reconnectAttempts: number;
}
interface WebSocketMessage {
type: string;
session_id?: string;
data?: StreamToken[];
full_content?: string;
total_tokens?: number;
error?: string;
offset?: number;
current_content?: string;
}
const MAX_RECONNECT_ATTEMPTS = 5;
const RECONNECT_BASE_DELAY = 1000;
const HEARTBEAT_INTERVAL = 15000;
export class ResumableStreamClient {
private ws: WebSocket | null = null;
private sessionId: string | null = null;
private tokenOffset: number = 0;
private content: string = '';
private onTokens: (tokens: StreamToken[]) => void;
private onComplete: (fullContent: string, totalTokens: number) => void;
private onError: (error: string) => void;
private onStatusChange: (status: string) => void;
private reconnectAttempts: number = 0;
private reconnectTimeout: ReturnType | null = null;
private heartbeatInterval: ReturnType | null = null;
private url: string;
constructor(
wsUrl: string,
callbacks: {
onTokens: (tokens: StreamToken[]) => void;
onComplete: (fullContent: string, totalTokens: number) => void;
onError: (error: string) => void;
onStatusChange: (status: string) => void;
}
) {
this.url = wsUrl;
this.onTokens = callbacks.onTokens;
this.onComplete = callbacks.onComplete;
this.onError = callbacks.onError;
this.onStatusChange = callbacks.onStatusChange;
}
private getReconnectDelay(): number {
// Exponential backoff with jitter
const baseDelay = RECONNECT_BASE_DELAY * Math.pow(2, this.reconnectAttempts);
const jitter = Math.random() * 1000;
return Math.min(baseDelay + jitter, 30000); // Max 30s
}
private startHeartbeat(): void {
this.heartbeatInterval = setInterval(() => {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ type: 'ping' }));
}
}, HEARTBEAT_INTERVAL);
}
private stopHeartbeat(): void {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
}
private handleMessage(event: MessageEvent): void {
try {
const message: WebSocketMessage = JSON.parse(event.data);
switch (message.type) {
case 'session_created':
this.sessionId = message.session_id!;
this.tokenOffset = 0;
this.content = '';
this.onStatusChange('streaming');
break;
case 'tokens':
if (message.data && message.data.length > 0) {
// Calculate incremental update
const newTokens = message.data.filter(
t => t.index >= this.tokenOffset
);
if (newTokens.length > 0) {
this.tokenOffset = Math.max(...newTokens.map(t => t.index)) + 1;
this.content += newTokens.map(t => t.content).join('');
this.onTokens(newTokens);
}
}
break;
case 'complete':
this.onComplete(message.full_content || '', message.total_tokens || 0);
this.onStatusChange('completed');
this.reconnectAttempts = 0; // Reset on success
break;
case 'error':
this.onError(message.error || 'Unknown error');
this.onStatusChange('error');
break;
case 'resume_ack':
// Server acknowledged resume request
this.tokenOffset = message.offset || 0;
this.content = message.current_content || '';
this.onStatusChange('streaming');
break;
case 'pong':
// Heartbeat response received
break;
}
} catch (e) {
console.error('Failed to parse message:', e);
}
}
async connect(): Promise {
return new Promise((resolve, reject) => {
this.onStatusChange('connecting');
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
this.reconnectAttempts = 0;
this.startHeartbeat();
resolve();
};
this.ws.onmessage = (event) => this.handleMessage(event);
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
this.onError('Connection error');
};
this.ws.onclose = () => {
this.stopHeartbeat();
this.attemptReconnect();
};
});
}
private attemptReconnect(): void {
if (this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
this.onError('Max reconnection attempts reached');
this.onStatusChange('error');
return;
}
this.reconnectAttempts++;
const delay = this.getReconnectDelay();
this.onStatusChange('reconnecting');
console.log(Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}));
this.reconnectTimeout = setTimeout(async () => {
try {
if (this.sessionId) {
// Resume existing session
await this.resumeSession(this.sessionId, this.tokenOffset);
}
} catch (e) {
console.error('Reconnection failed:', e);
this.attemptReconnect();
}
}, delay);
}
async createSession(prompt: string, model: string = 'gpt-4.1'): Promise