การสร้างแอปพลิเคชัน 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