Tích hợp ChatGPT API vào ứng dụng web: Xử lý lỗi thực tế từ production

Artificial Intelligence tutorial - IT technology blog
Artificial Intelligence tutorial - IT technology blog

Lúc 2 giờ sáng, production đột ngột trả về lỗi 429

Slack bắt đầu ringing. Cái feature chat AI mình vừa deploy hôm trước — cái được sếp khen “ngon lắm” — giờ đang trả về lỗi cho toàn bộ người dùng. Dashboard Sentry toàn màu đỏ.

Error message: openai.RateLimitError: You exceeded your current quota. Cổ điển.

Chuyện đó không phải ngoại lệ. Tích hợp ChatGPT API trông đơn giản — vài dòng code là chạy được. Nhưng production mới là nơi mọi thứ vỡ vụn: rate limit, quota, token limit, latency bất ổn. Không xử lý đúng từ đầu, sớm muộn cũng gặp lúc ít muốn nhất.

Phân tích nguyên nhân: Tại sao tích hợp ChatGPT API hay gặp sự cố?

Rate limit và quota

OpenAI giới hạn theo nhiều chiều cùng lúc:

  • RPM (Requests Per Minute): Số lần gọi API trong 1 phút
  • TPM (Tokens Per Minute): Số token xử lý trong 1 phút
  • RPD (Requests Per Day): Giới hạn theo ngày với tier thấp

Con số thực tế: free tier chỉ có 3 RPM và 40k TPM. Tier 1 (đã thêm payment method) khá hơn — 3,500 RPM — nhưng vẫn dễ bị hit khi traffic đột biến. Lúc mình bị incident, chỉ cần 5–6 user refresh liên tục là đủ để cháy quota cả hệ thống.

Token overflow

Mỗi model có context window giới hạn. GPT-3.5-turbo có 16k token, GPT-4o có 128k token. Nghe nhiều, nhưng một cuộc trò chuyện 20 tin nhắn đã dễ ngốn 3,000–5,000 token. Cứ nhét toàn bộ lịch sử vào mỗi request mà không cắt tỉa, chi phí phình to nhanh lắm — và dễ gặp lỗi context_length_exceeded.

Timeout và latency không ổn định

Timeout là thứ ít ai nghĩ đến lúc mới tích hợp. ChatGPT API có latency biến động lớn — đôi khi response mất 10–30 giây, đặc biệt với GPT-4 hoặc khi server OpenAI đang tải cao. Không có timeout handling, request treo và user ngồi nhìn cái spinner mãi không thôi.

API key lộ ra ngoài

Lỗi kinh điển của người mới: hard-code API key trong frontend JavaScript. Bot scanner trên GitHub tìm và dùng key của bạn trong vòng vài tiếng sau khi commit. Không phải chuyện giả định — có người nhận bill $400–500 sau một đêm vì key bị lộ lên public repo.

Các cách giải quyết

Cách 1: Exponential backoff khi gặp rate limit

Retry với thời gian chờ tăng dần — đơn giản nhất, hiệu quả nhất:

import openai
import time
import random

def call_openai_with_retry(messages, model="gpt-3.5-turbo", max_retries=5):
    for attempt in range(max_retries):
        try:
            response = openai.chat.completions.create(
                model=model,
                messages=messages,
                timeout=30
            )
            return response.choices[0].message.content
        except openai.RateLimitError:
            if attempt == max_retries - 1:
                raise
            wait_time = (2 ** attempt) + random.uniform(0, 1)
            print(f"Rate limited, waiting {wait_time:.1f}s...")
            time.sleep(wait_time)
        except openai.APITimeoutError:
            if attempt == max_retries - 1:
                raise
            time.sleep(2)

Công thức chờ: 1s → 2s → 4s → 8s → 16s. Đủ để server OpenAI hồi phục mà không làm tình hình tệ hơn bằng cách spam thêm request lúc đang quá tải.

Cách 2: Trim conversation history để kiểm soát token

Thay vì gửi toàn bộ lịch sử, giữ lại N tin nhắn gần nhất:

def trim_messages(messages, max_tokens=4000):
    """Giữ system message + tin nhắn gần nhất trong ngưỡng token"""
    system_msgs = [m for m in messages if m["role"] == "system"]
    chat_msgs = [m for m in messages if m["role"] != "system"]

    # Ước tính token: 4 ký tự ≈ 1 token
    def estimated_tokens(msgs):
        return sum(len(m["content"]) for m in msgs) // 4

    while estimated_tokens(chat_msgs) > max_tokens and len(chat_msgs) > 2:
        chat_msgs = chat_msgs[2:]  # Xóa cặp user+assistant cũ nhất

    return system_msgs + chat_msgs

Cách 3: Streaming để cải thiện trải nghiệm người dùng

Thay vì chờ response hoàn chỉnh (có thể mất 10–20s), dùng streaming để hiển thị từng từ ngay khi nhận được:

# Backend (FastAPI)
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import openai

app = FastAPI()
client = openai.OpenAI()

@app.post("/chat")
async def chat_stream(user_message: str):
    async def generate():
        stream = client.chat.completions.create(
            model="gpt-3.5-turbo",
            messages=[{"role": "user", "content": user_message}],
            stream=True
        )
        for chunk in stream:
            delta = chunk.choices[0].delta.content
            if delta:
                yield f"data: {delta}\n\n"
        yield "data: [DONE]\n\n"

    return StreamingResponse(generate(), media_type="text/event-stream")
// Frontend nhận stream
async function chatWithStream(message) {
  const response = await fetch('/chat', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ user_message: message })
  });

  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let buffer = '';

  while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    const lines = decoder.decode(value).split('\n');
    for (const line of lines) {
      if (line.startsWith('data: ')) {
        const data = line.slice(6);
        if (data === '[DONE]') return;
        buffer += data;
        document.getElementById('response').textContent = buffer;
      }
    }
  }
}

Cách tốt nhất: Kiến trúc production-ready

Bảo vệ API key — ưu tiên số 1

API key OpenAI không bao giờ được đưa vào frontend. Mọi request phải đi qua backend:

# .env (KHÔNG commit lên git)
OPENAI_API_KEY=sk-proj-...

# .gitignore
.env
import os
from dotenv import load_dotenv
import openai

load_dotenv()

api_key = os.getenv("OPENAI_API_KEY")
if not api_key:
    raise ValueError("OPENAI_API_KEY chưa được set trong environment")

client = openai.OpenAI(api_key=api_key)

Rate limiting ở tầng ứng dụng

Handle lỗi từ OpenAI chỉ là một nửa bài toán. Phần còn lại là tự giới hạn rate ở backend — tránh một user nào đó vô tình (hoặc cố tình) kéo sập quota cả hệ thống:

import redis
import time

r = redis.Redis(host='localhost', port=6379, db=0)

def check_rate_limit(user_id: str, limit: int = 10, window: int = 60) -> bool:
    """Giới hạn 10 request/phút per user"""
    key = f"rate_limit:{user_id}"
    now = time.time()
    pipe = r.pipeline()
    pipe.zremrangebyscore(key, 0, now - window)
    pipe.zadd(key, {str(now): now})
    pipe.zcard(key)
    pipe.expire(key, window)
    results = pipe.execute()
    return results[2] <= limit

Caching response để tiết kiệm chi phí

User hay hỏi những câu tương tự nhau hơn bạn nghĩ. Mình áp dụng caching trên một chatbot FAQ, cache hit rate đạt khoảng 35–40% — nghĩa là gần 1/3 request không cần đụng đến OpenAI:

import hashlib, json

def get_cache_key(messages: list) -> str:
    content = json.dumps(messages, sort_keys=True)
    return f"openai_cache:{hashlib.md5(content.encode()).hexdigest()}"

def cached_completion(messages: list, ttl: int = 3600):
    cache_key = get_cache_key(messages)
    cached = r.get(cache_key)
    if cached:
        return json.loads(cached)

    response = client.chat.completions.create(
        model="gpt-3.5-turbo",
        messages=messages
    )
    result = response.choices[0].message.content
    r.setex(cache_key, ttl, json.dumps(result))
    return result

Monitor chi phí theo thời gian thực

Không ai muốn nhận hóa đơn $200 cuối tháng mà không biết từ đâu ra. Log token usage mỗi request, xem hàng ngày:

def call_with_cost_tracking(messages: list, user_id: str):
    response = client.chat.completions.create(
        model="gpt-3.5-turbo",
        messages=messages
    )
    usage = response.usage
    # GPT-3.5-turbo: $0.5/1M input, $1.5/1M output tokens
    cost = (usage.prompt_tokens * 0.0000005) + (usage.completion_tokens * 0.0000015)
    print(f"[COST] user={user_id} | tokens={usage.total_tokens} | ${cost:.6f}")
    return response.choices[0].message.content

Checklist tối thiểu cho production

  • Frontend gọi backend của bạn — không gọi OpenAI trực tiếp
  • Backend xác thực user, kiểm tra rate limit trước khi gọi API
  • Kiểm tra cache — có thì trả về luôn, không cần gọi OpenAI
  • Gọi OpenAI với retry + exponential backoff
  • Dùng streaming nếu muốn UX mượt
  • Log token usage để monitor chi phí hàng ngày

Cái đêm 2 giờ sáng đó, sau khi fix xong, mình mới nhận ra gần như toàn bộ vấn đề đến từ việc không có rate limiting ở tầng ứng dụng. Một user tò mò spam F5 liên tục đã làm cháy quota của cả hệ thống. Từ đó về sau, Redis rate limiting là thứ đầu tiên mình thêm vào bất kỳ dự án nào gọi AI API — trước cả khi viết business logic.

Share: