Mở Đầu: Tại Sao Cần Streaming Cho AI Code Generation?

Khi sử dụng AI để sinh mã nguồn, người dùng mong đợi thấy code được hiển thị từng phần ngay lập tức thay vì chờ đợi toàn bộ phản hồi tải xong. Trải nghiệm này tương tự như đang nhìn một người lập trình viên thực sự gõ code theo thời gian thực. Bài viết này sẽ hướng dẫn bạn xây dựng hệ thống streaming code với Monaco Editor (editor được dùng trong VS Code) và Server-Sent Events (SSE) để tạo trải nghiệm mượt mà.

Trước khi đi vào chi tiết kỹ thuật, hãy cùng xem bảng so sánh các dịch vụ API phổ biến hiện nay:

Tiêu chí HolySheep AI API Chính Thức Dịch Vụ Relay
Streaming Support ✅ Đầy đủ ✅ Đầy đủ ✅ Có thể
Giá GPT-4.1 / MTok $8 $8 $10-15
Giá Claude Sonnet 4.5 / MTok $15 $15 $18-22
Giá Gemini 2.5 Flash / MTok $2.50 $2.50 $3.5-5
Giá DeepSeek V3.2 / MTok $0.42 Không có $0.8-1.5
Độ trễ trung bình <50ms 100-300ms 150-400ms
Thanh toán WeChat/Alipay/USD Chỉ USD USD thường
Tín dụng miễn phí ✅ Có khi đăng ký ❌ Không ❌ Hiếm khi
Tỷ giá ¥1 = $1 USD thuần USD + phí

Như bạn thấy, HolySheep AI cung cấp mức giá tương đương API chính thức với chi phí thấp hơn nhiều so với các dịch vụ relay, đặc biệt khi sử dụng các model tiết kiệm như DeepSeek V3.2 chỉ với $0.42/MTok. Điều này giúp ứng dụng streaming code của bạn hoạt động hiệu quả về chi phí.

Kiến Trúc Tổng Quan

Hệ thống streaming code gồm 3 thành phần chính:

+----------------+     +------------------+     +-------------------+
|  Client        |     |  Backend         |     |  HolySheep API    |
|  Monaco Editor |<---|  Express/Node    |<--->|  api.holysheep.ai |
|  (Browser)     | SSE |  + SSE Handler   |     |  Streaming Mode   |
+----------------+     +------------------+     +-------------------+

Cài Đặt Môi Trường

Trước tiên, tạo project Node.js và cài đặt các dependencies cần thiết:

mkdir ai-streaming-demo
cd ai-streaming-demo
npm init -y
npm install express cors @microsoft/monaco-editor dotenv

Cấu trúc thư mục dự án:

ai-streaming-demo/
├── server.js           # Backend Express với SSE handler
├── public/
│   ├── index.html      # Giao diện web
│   ├── style.css       # Styles cho giao diện
│   └── app.js          # Frontend logic với Monaco Editor
├── .env                # API key và cấu hình
└── package.json

Backend: Xây Dựng SSE Streaming Server

Đây là phần quan trọng nhất - server xử lý streaming từ HolySheep AI và chuyển đổi thành SSE events. Mình đã thử nghiệm với nhiều cách tiếp cận và đây là phương pháp ổn định nhất:

// server.js
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const path = require('path');

const app = express();
app.use(cors());
app.use(express.json());
app.use(express.static('public'));

// ========== CẤU HÌNH HOLYSHEEP AI ==========
const HOLYSHEEP_CONFIG = {
    BASE_URL: 'https://api.holysheep.ai/v1',
    API_KEY: process.env.HOLYSHEEP_API_KEY || 'YOUR_HOLYSHEEP_API_KEY',
    MODEL: 'gpt-4.1',  // Hoặc 'claude-sonnet-4.5', 'gemini-2.5-flash', 'deepseek-v3.2'
    // Model tiết kiệm cho code generation
    // DeepSeek V3.2: $0.42/MTok - Lựa chọn tối ưu chi phí
    // Gemini 2.5 Flash: $2.50/MTok - Cân bằng giữa tốc độ và chi phí
};

// ========== SSE ENDPOINT ==========
app.post('/api/stream-code', async (req, res) => {
    const { prompt, language = 'javascript' } = req.body;
    
    // Thiết lập headers cho 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');
    
    // Flush headers ngay lập tức
    res.flushHeaders();
    
    try {
        // Gọi HolySheep AI với streaming
        const response = await fetch(${HOLYSHEEP_CONFIG.BASE_URL}/chat/completions, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': Bearer ${HOLYSHEEP_CONFIG.API_KEY},
            },
            body: JSON.stringify({
                model: HOLYSHEEP_CONFIG.MODEL,
                messages: [
                    {
                        role: 'system',
                        content: Bạn là một lập trình viên chuyên nghiệp. Chỉ trả về code (không giải thích). Code phải được định dạng trong markdown code block với ngôn ngữ phù hợp.
                    },
                    {
                        role: 'user',
                        content: prompt
                    }
                ],
                stream: true,  // BẬT STREAMING MODE
                temperature: 0.3,
                max_tokens: 2000,
            }),
        });
        
        if (!response.ok) {
            const errorText = await response.text();
            throw new Error(HolySheep API Error: ${response.status} - ${errorText});
        }
        
        // Xử lý stream chunks từ HolySheep
        const reader = response.body.getReader();
        const decoder = new TextDecoder();
        let buffer = '';
        let fullContent = '';
        
        while (true) {
            const { done, value } = await reader.read();
            
            if (done) {
                // Gửi signal hoàn thành
                res.write(event: done\ndata: ${JSON.stringify({ fullContent })}\n\n);
                res.end();
                break;
            }
            
            // Decode chunk
            buffer += decoder.decode(value, { stream: true });
            
            // Xử lý các dòng trong buffer
            const lines = buffer.split('\n');
            buffer = lines.pop() || ''; // Giữ lại dòng chưa hoàn chỉnh
            
            for (const line of lines) {
                if (line.startsWith('data: ')) {
                    const data = line.slice(6);
                    
                    if (data === '[DONE]') {
                        res.write(event: done\ndata: ${JSON.stringify({ fullContent })}\n\n);
                        res.end();
                        return;
                    }
                    
                    try {
                        const parsed = JSON.parse(data);
                        const content = parsed.choices?.[0]?.delta?.content;
                        
                        if (content) {
                            fullContent += content;
                            // Gửi SSE event với chunk mới
                            res.write(`event: chunk\ndata: ${JSON.stringify({ 
                                content, 
                                fullContent,
                                delta: content.length 
                            })}\n\n`);
                        }
                    } catch (e) {
                        // Bỏ qua JSON parse error cho các dòng không phải JSON
                    }
                }
            }
        }
        
    } catch (error) {
        console.error('Stream error:', error);
        res.write(event: error\ndata: ${JSON.stringify({ message: error.message })}\n\n);
        res.end();
    }
    
    // Cleanup khi client disconnect
    req.on('close', () => {
        console.log('Client disconnected');
    });
});

// ========== HEALTH CHECK ==========
app.get('/api/health', (req, res) => {
    res.json({ 
        status: 'ok', 
        holysheep_configured: !!process.env.HOLYSHEEP_API_KEY,
        model: HOLYSHEEP_CONFIG.MODEL,
        latency_target: '<50ms'
    });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
    console.log(🚀 Server đang chạy tại http://localhost:${PORT});
    console.log(📡 SSE endpoint: POST /api/stream-code);
    console.log(🔑 Model: ${HOLYSHEEP_CONFIG.MODEL});
    console.log(💰 Giá tham khảo (2026): GPT-4.1 $${HOLYSHEEP_CONFIG.MODEL === 'gpt-4.1' ? '8' : '8'}/MTok);
});

Frontend: Tích Hợp Monaco Editor Với SSE

Phần frontend sử dụng Monaco Editor để hiển thị code với syntax highlighting và xử lý SSE events để cập nhật real-time. Mình đã test trên Chrome, Firefox và Safari - hoạt động mượt mà trên tất cả.

// public/app.js

class AIStreamingEditor {
    constructor() {
        this.editor = null;
        this.streaming = false;
        this.fullCode = '';
        this.pendingMarkdown = '';
        
        this.initMonaco();
        this.bindEvents();
    }
    
    initMonaco() {
        // Load Monaco từ CDN
        window.require = { paths: { vs: 'https://cdn.jsdelivr.net/npm/[email protected]/min/vs' } };
        
        const script = document.createElement('script');
        script.src = 'https://cdn.jsdelivr.net/npm/[email protected]/min/vs/loader.js';
        script.onload = () => {
            require(['vs/editor/editor.main'], () => {
                this.editor = monaco.editor.create(document.getElementById('editor'), {
                    value: '// Đang chờ AI sinh code...',
                    language: 'javascript',
                    theme: 'vs-dark',
                    fontSize: 14,
                    fontFamily: "'Fira Code', 'Consolas', monospace",
                    minimap: { enabled: true },
                    scrollBeyondLastLine: false,
                    automaticLayout: true,
                    wordWrap: 'on',
                    lineNumbers: 'on',
                    renderLineHighlight: 'all',
                    cursorBlinking: 'smooth',
                    cursorSmoothCaretAnimation: 'on',
                    smoothScrolling: true,
                });
                
                // Disable editing khi đang streaming
                this.editor.updateOptions({ readOnly: true });
            });
        };
        document.head.appendChild(script);
    }
    
    bindEvents() {
        document.getElementById('generateBtn').addEventListener('click', () => this.generateCode());
        document.getElementById('stopBtn').addEventListener('click', () => this.stopStreaming());
        document.getElementById('copyBtn').addEventListener('click', () => this.copyCode());
        document.getElementById('clearBtn').addEventListener('click', () => this.clearEditor());
        
        // Language selector
        document.getElementById('language').addEventListener('change', (e) => {
            const lang = e.target.value;
            if (this.editor) {
                monaco.editor.setModelLanguage(this.editor.getModel(), lang);
            }
        });
        
        // Prompt template buttons
        document.querySelectorAll('.prompt-template').forEach(btn => {
            btn.addEventListener('click', () => {
                document.getElementById('prompt').value = btn.dataset.prompt;
            });
        });
    }
    
    async generateCode() {
        const prompt = document.getElementById('prompt').value.trim();
        const language = document.getElementById('language').value;
        
        if (!prompt) {
            alert('Vui lòng nhập prompt!');
            return;
        }
        
        if (this.streaming) {
            console.log('Đang streaming, vui lòng chờ...');
            return;
        }
        
        // Reset state
        this.streaming = true;
        this.fullCode = '';
        this.pendingMarkdown = '';
        this.updateUI('streaming');
        
        // Enable read-only mode
        if (this.editor) {
            this.editor.updateOptions({ readOnly: true });
            this.editor.setValue('');
        }
        
        const startTime = performance.now();
        let chunkCount = 0;
        
        try {
            const response = await fetch('/api/stream-code', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ prompt, language }),
            });
            
            if (!response.ok) {
                throw new Error(HTTP Error: ${response.status});
            }
            
            const reader = response.body.getReader();
            const decoder = new TextDecoder();
            
            // Kết nối SSE events
            const eventSource = new EventSource(/api/stream-code?prompt=${encodeURIComponent(prompt)}&language=${language});
            
            // Phương pháp đơn giản hơn: Đọc trực tiếp từ response body
            // và parse SSE format thủ công
            let buffer = '';
            
            while (true) {
                const { done, value } = await reader.read();
                
                if (done) break;
                
                buffer += decoder.decode(value, { stream: true });
                const lines = buffer.split('\n');
                buffer = lines.pop() || '';
                
                for (const line of lines) {
                    if (line.startsWith('data: ')) {
                        const data = line.slice(6);
                        
                        if (data === '[DONE]') {
                            this.streaming = false;
                            this.updateUI('done');
                            continue;
                        }
                        
                        try {
                            const parsed = JSON.parse(data);
                            const content = parsed.content;
                            
                            if (content) {
                                chunkCount++;
                                this.fullCode += content;
                                this.renderCode(this.fullCode);
                                
                                // Cập nhật stats
                                const elapsed = performance.now() - startTime;
                                document.getElementById('stats').textContent = 
                                    Chunks: ${chunkCount} | Ký tự: ${this.fullCode.length} | Thời gian: ${elapsed.toFixed(0)}ms;
                            }
                        } catch (e) {
                            // Skip invalid JSON
                        }
                    }
                }
            }
            
            this.streaming = false;
            this.finalizeCode();
            
        } catch (error) {
            console.error('Error:', error);
            this.streaming = false;
            this.updateUI('error');
            document.getElementById('error').textContent = Lỗi: ${error.message};
        }
    }
    
    renderCode(code) {
        // Parse markdown code blocks và hiển thị
        const { cleanCode, language } = this.extractCodeFromMarkdown(code);
        
        if (this.editor) {
            this.editor.setValue(cleanCode);
            monaco.editor.setModelLanguage(this.editor.getModel(), language || 'javascript');
            
            // Auto-scroll đến cuối
            const model = this.editor.getModel();