Tích hợp ChatGPT API vào ứng dụng web: Streaming, Conversation History và Deploy thực tế

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

Quick Start: Có endpoint chat trong 5 phút

Mình sẽ cho bạn thấy cách nhanh nhất để dựng một backend nhận prompt và trả về câu trả lời từ ChatGPT — trước khi giải thích tại sao cần làm phức tạp hơn.

Cài thư viện:

pip install flask openai python-dotenv flask-cors

Tạo file .env:

OPENAI_API_KEY=sk-proj-xxxxxx

File app.py tối giản:

from flask import Flask, request, jsonify
from flask_cors import CORS
from openai import OpenAI
from dotenv import load_dotenv

load_dotenv()
app = Flask(__name__)
CORS(app)
client = OpenAI()

@app.route("/chat", methods=["POST"])
def chat():
    user_message = request.json.get("message", "")
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": user_message}]
    )
    return jsonify({"reply": response.choices[0].message.content})

if __name__ == "__main__":
    app.run(debug=True)

Test nhanh bằng curl:

curl -X POST http://localhost:5000/chat \
  -H "Content-Type: application/json" \
  -d '{"message": "Giải thích Docker là gì?"}'

Chạy được rồi. Nhưng đưa lên production theo cách này thì có ít nhất 3 vấn đề nghiêm trọng.

3 vấn đề thực tế cần giải quyết

Vấn đề 1: Response chậm — UX tệ

ChatGPT thường mất 3–10 giây để generate xong một câu trả lời dài. User nhìn màn hình trắng, không có feedback gì — cảm giác như app bị treo. Streaming giải quyết chuyện này: nhận từng chunk text ngay khi model generate và hiển thị dần, y hệt giao diện ChatGPT.com.

Vấn đề 2: Chatbot không nhớ context

OpenAI API hoàn toàn stateless — mỗi request độc lập, không liên quan gì đến request trước. Muốn chatbot “nhớ” được hội thoại, bạn phải gửi toàn bộ lịch sử conversation lên mỗi lần gọi API.

Vấn đề 3: API key bị lộ nếu gọi từ frontend

Gọi thẳng OpenAI từ JavaScript trên browser đồng nghĩa với việc bất kỳ ai mở DevTools đều thấy API key của bạn. Mọi request phải đi qua backend — đây là rule số 1 khi làm bất cứ thứ gì với AI API.

Giải pháp hoàn chỉnh: Streaming + Conversation History

Backend dùng Server-Sent Events (SSE) — cơ chế cho phép server push text về browser từng mảnh một thay vì chờ xong mới gửi. Mình áp dụng cách này trên production được vài tháng; user thấy text xuất hiện dần thay vì nhìn màn hình trắng 5–8 giây, dù tổng thời gian model generate không thay đổi.

Backend Flask với streaming

from flask import Flask, request, jsonify, Response, stream_with_context
from flask_cors import CORS
from openai import OpenAI
from dotenv import load_dotenv
import json

load_dotenv()
app = Flask(__name__)
CORS(app)
client = OpenAI()

# Lưu conversation history trong memory
# Production nên dùng Redis hoặc database
conversations = {}

SYSTEM_PROMPT = """Bạn là chuyên gia IT, chuyên về Linux và DevOps.
Trả lời bằng tiếng Việt, ngắn gọn, có ví dụ code khi cần.
Nếu không chắc, nói thẳng thay vì bịa đặt."""

MAX_HISTORY = 20  # Giữ tối đa 20 tin nhắn

def trim_history(history):
    system_msg = [m for m in history if m["role"] == "system"]
    other_msgs = [m for m in history if m["role"] != "system"]
    if len(other_msgs) > MAX_HISTORY:
        other_msgs = other_msgs[-MAX_HISTORY:]
    return system_msg + other_msgs

@app.route("/chat/stream", methods=["POST"])
def chat_stream():
    data = request.json
    session_id = data.get("session_id", "default")
    user_message = data.get("message", "")

    if session_id not in conversations:
        conversations[session_id] = [{"role": "system", "content": SYSTEM_PROMPT}]

    conversations[session_id].append({"role": "user", "content": user_message})
    conversations[session_id] = trim_history(conversations[session_id])

    def generate():
        full_response = ""
        stream = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=conversations[session_id],
            stream=True,
            max_tokens=1000
        )
        for chunk in stream:
            if chunk.choices[0].delta.content:
                text = chunk.choices[0].delta.content
                full_response += text
                yield f"data: {json.dumps({'text': text})}\n\n"

        conversations[session_id].append({
            "role": "assistant",
            "content": full_response
        })
        yield f"data: {json.dumps({'done': True})}\n\n"

    return Response(
        stream_with_context(generate()),
        mimetype="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "X-Accel-Buffering": "no"  # Bắt buộc nếu dùng Nginx
        }
    )

@app.route("/chat/reset", methods=["POST"])
def reset():
    session_id = request.json.get("session_id", "default")
    conversations.pop(session_id, None)
    return jsonify({"status": "ok"})

Frontend JavaScript: Nhận stream và hiển thị realtime

Dùng Fetch API với ReadableStream — không cần thư viện gì thêm:

const sessionId = Math.random().toString(36).substr(2, 9);

async function sendMessage(message) {
    const assistantDiv = createMessageDiv("assistant", "");

    const response = await fetch("/chat/stream", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ session_id: sessionId, message })
    });

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

    while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        const lines = decoder.decode(value).split("\n");
        for (const line of lines) {
            if (!line.startsWith("data: ")) continue;
            try {
                const data = JSON.parse(line.slice(6));
                if (data.text) {
                    assistantDiv.textContent += data.text;
                }
            } catch (e) {}
        }
    }
}

Nâng cao: Rate limiting và kiểm soát chi phí

Bài học đắt giá: thiếu rate limiting, một con bot hoặc user mất kiểm soát có thể đẩy bill OpenAI lên $30–50 chỉ trong một đêm. Cài thêm flask-limiter:

pip install flask-limiter
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
import openai

limiter = Limiter(
    app=app,
    key_func=get_remote_address,
    default_limits=["200 per day", "50 per hour"]
)

@app.route("/chat/stream", methods=["POST"])
@limiter.limit("10 per minute")
def chat_stream():
    try:
        # ... code như trên
        pass
    except openai.RateLimitError:
        return jsonify({"error": "Hệ thống đang bận, thử lại sau 30 giây"}), 429
    except openai.APIConnectionError:
        return jsonify({"error": "Không kết nối được OpenAI API"}), 503
    except openai.AuthenticationError:
        return jsonify({"error": "API key không hợp lệ"}), 401

Tips thực tế khi deploy lên production

Nginx cần config đặc biệt cho SSE

Nginx mặc định buffer toàn bộ response trước khi gửi về client. Với SSE, điều này khiến stream bị “đứng” — text chỉ hiện ra một lần khi request kết thúc, phá vỡ toàn bộ hiệu ứng streaming. Phải tắt buffering:

location /chat/ {
    proxy_pass http://127.0.0.1:5000;
    proxy_buffering off;        # Bắt buộc cho SSE
    proxy_cache off;
    proxy_read_timeout 300s;    # Đủ dài cho response dài
    add_header X-Accel-Buffering no;
}

Chọn model phù hợp với use case

  • gpt-4o-mini — Nhanh, rẻ (khoảng 30× rẻ hơn gpt-4o), đủ cho chatbot hỗ trợ thông thường
  • gpt-4o — Dùng khi cần phân tích phức tạp hoặc code generation chất lượng cao
  • gpt-4o với structured outputs — Khi cần JSON response có schema cố định, không bị hallucinate format

System prompt quyết định chất lượng

System prompt rỗng là mistake phổ biến nhất. Viết rõ vai trò, phong cách trả lời, và giới hạn chủ đề — chatbot sẽ ít bịa hơn và trả lời đúng tone hơn rõ rệt. Mỗi khi chatbot lạc đề hoặc sai giọng, chỗ đầu tiên mình kiểm tra luôn là system prompt.

Không lưu conversation history mãi mãi

Conversation history dài = token nhiều = tốn tiền hơn mỗi request. Hàm trim_history() ở trên giữ 20 tin nhắn gần nhất — tương đương khoảng 10.000–15.000 token tùy nội dung, đủ để duy trì ngữ cảnh mà không ăn hết budget. Nếu cần nhớ thông tin dài hạn hơn (ví dụ: tên user, preferences), tóm tắt và inject vào system prompt thay vì giữ nguyên toàn bộ lịch sử.

Share: