Xây dựng Web Dashboard và Tích hợp Mô hình AI với Python Streamlit: Bài học từ 6 Tháng Production

Python tutorial - IT technology blog
Python tutorial - IT technology blog

Vấn đề thực tế: Script phân tích và model AI bị ‘kẹt’ trong Jupyter Notebook

Khoảng nửa năm trước, team Data bên mình nhận một bài toán gấp từ phòng Vận hành. Họ cần một công cụ nội bộ để theo dõi doanh thu theo ngày, đồng thời chạy thử nghiệm model phân loại phản hồi khách hàng (Sentiment Analysis). Ban đầu, mọi việc trên Jupyter Notebook diễn ra rất mượt. Dữ liệu kéo về chuẩn xác. Pandas lọc gọn gàng. Biểu đồ Matplotlib vẽ ra nhìn rất ưng mắt.

Nhưng rắc rối ập đến ngay khi bàn giao notebook cho team Operations và Marketing dùng thử. Họ không quen mở terminal. Họ thường xuyên làm crash kernel vì nhấn nhầm phím tắt Shift + Enter liên tục.

Cứ mỗi lần sếp cần đổi bộ lọc ngày hay thử một câu text mới, mình lại phải ngồi cạnh gõ code hộ. Script phân tích dù xịn đến đâu, nếu không có giao diện trực quan thì cũng chỉ nằm đắp chiếu trên máy của lập trình viên.

Vì sao đưa script Python lên web lại khó đến vậy?

Khi bắt tay vào giải quyết, mình nhận ra rào cản lớn nhất nằm ở khoảng cách giữa data logic và frontend:

  • Quá nhiều công nghệ phải học: Để đưa một hàm Python lên trình duyệt, bạn thường phải viết REST API bằng FastAPI, rồi loay hoay với HTML, CSS, JavaScript hoặc React. Với một data analyst hay người mới học Python, khối lượng kiến thức này là một gánh nặng lớn.
  • Quản lý trạng thái (State) đau đầu: Người dùng kéo một thanh slider hay chọn dropdown? Bạn phải tự viết JavaScript bắt sự kiện (Event Listener), gửi fetch request rồi cập nhật lại DOM. Chỉ riêng việc đồng bộ giao diện đã ngốn hàng ngày trời.
  • Áp lực tài nguyên máy chủ: File checkpoint của model AI nặng tới 260MB. Nếu mỗi request của user đều reload model từ ổ cứng và query lại 500.000 dòng dữ liệu, server 8GB RAM sẽ sập ngay lập tức vì tràn bộ nhớ.

3 giải pháp đã thử nghiệm trước khi chốt Streamlit

Trước khi tìm ra phương án tối ưu, mình đã cùng team thử qua ba hướng tiếp cận khác nhau trên môi trường nội bộ:

1. FastAPI kết hợp HTML/Bootstrap thuần

FastAPI xử lý backend rất nhanh và chuẩn chỉ. Swagger UI tự sinh cũng tiện cho việc test API. Tuy nhiên, nút thắt lại nằm ở frontend.

Khi sếp yêu cầu thêm 4 bộ lọc tương tác và 3 biểu đồ biến động theo giờ, mình mất trọn 3 ngày chỉ để viết JavaScript fetch API và cấu hình Chart.js. Tốc độ bàn giao tính năng quá chậm. Điều đó không đáp ứng được yêu cầu linh hoạt của dự án.

2. Plotly Dash

Dash thuần Python và vẽ biểu đồ phân tích rất đẹp. Dù vậy, mô hình callback lồng nhau (nested callbacks) nhanh chóng biến code thành một mớ bòng bong khi giao diện mở rộng. Việc debug lỗi vòng lặp callback tốn nhiều thời gian và gây ức chế cho người mới.

3. Streamlit: Phương án được chọn

Streamlit giải quyết triệt để vấn đề: không cần đụng vào một dòng HTML hay JS nào. Bạn viết code theo tư duy script tuần tự từ trên xuống dưới, gọi component ở đâu thì nút bấm hay biểu đồ hiện ngay ở đó.

Điểm lưu ý duy nhất là cơ chế re-run: Streamlit sẽ chạy lại toàn bộ script mỗi khi người dùng tương tác với một widget. Ban đầu cơ chế này khiến app phản hồi rất chậm. Nhưng khi áp dụng đúng caching và session state, tốc độ tải trang đã cải thiện ngoạn mục.

Quy trình xây dựng Dashboard và tích hợp Model AI

Dưới đây là các bước triển khai chi tiết từ máy local đến khi chạy thực tế trên server.

Bước 1: Chuẩn bị môi trường

Hãy tạo môi trường ảo riêng biệt để tránh xung đột package giữa các dự án:

# Tạo và kích hoạt virtual environment
python -m venv venv
source venv/bin/activate  # Trên Windows: venv\Scripts\activate

# Cài đặt các thư viện cần thiết
pip install streamlit==1.32.0 pandas plotly transformers torch

Bước 2: Dựng layout và bảng điều khiển

Tạo file app.py với logic hiển thị dữ liệu kinh doanh:

import streamlit as st
import pandas as pd
import numpy as np
import plotly.express as px

# Cấu hình giao diện trang
st.set_page_config(
    page_title="Production Data Dashboard",
    layout="wide"
)

st.title("Dashboard Phân Tích Dữ Liệu & Thử Nghiệm AI")

# Bộ lọc trên thanh Sidebar
st.sidebar.header("Bộ lọc dữ liệu")
selected_category = st.sidebar.selectbox(
    "Chọn danh mục sản phẩm:",
    ["Tất cả", "Phần cứng", "Phần mềm", "Dịch vụ đám mây"]
)

# Đọc và cache dữ liệu giao dịch
@st.cache_data
def load_data():
    np.random.seed(42)
    return pd.DataFrame({
        "Ngay": pd.date_range(start="2026-01-01", periods=100),
        "Danh_muc": np.random.choice(["Phần cứng", "Phần mềm", "Dịch vụ đám mây"], size=100),
        "Doanh_thu": np.random.randint(10, 200, size=100),
        "So_luong": np.random.randint(1, 20, size=100)
    })

df = load_data()

# Xử lý lọc dữ liệu
filtered_df = df if selected_category == "Tất cả" else df[df["Danh_muc"] == selected_category]

# Hiển thị số liệu tổng quan
col1, col2, col3 = st.columns(3)
col1.metric("Tổng doanh thu ($k)", f"{filtered_df['Doanh_thu'].sum():,}")
col2.metric("Tổng đơn hàng", f"{filtered_df['So_luong'].sum():,}")
col3.metric("Giá trị trung bình/đơn", f"{filtered_df['Doanh_thu'].mean():.1f}")

# Biểu đồ Plotly tương tác
fig = px.line(
    filtered_df,
    x="Ngay",
    y="Doanh_thu",
    color="Danh_muc",
    title="Biến động doanh thu theo thời gian"
)
st.plotly_chart(fig, use_container_width=True)

Bước 3: Tối ưu hiệu năng bằng Caching chuẩn xác

Trong thực tế, khi bảng dữ liệu chạm mốc 200.000 dòng, mỗi lần user bấm chọn dropdown là trang web lại đơ 4.2 giây do Pandas phải tính lại toàn bộ. Sau khi gắn @st.cache_data, thời gian phản hồi giảm xuống còn 85ms.

Streamlit tách bạch hai loại cache rất rõ ràng:

  • @st.cache_data: Dùng cho dữ liệu có thể serialize (DataFrame, mảng NumPy, kết quả gọi REST API). Bất cứ phép tính toán số liệu nào tốn thời gian đều nên đặt dưới decorator này.
  • @st.cache_resource: Dùng cho các object nặng không thể serialize. Điển hình là kết nối database connection pool, TensorFlow/PyTorch session hoặc mô hình Transformer.

Bước 4: Nhúng pipeline NLP phân loại cảm xúc

Model distilbert-base-uncased-finetuned-sst-2-english có dung lượng khoảng 268MB. Nếu không dùng @st.cache_resource, mỗi lần người dùng bấm nút phân tích, server sẽ đọc lại file 268MB này vào RAM. Điều đó khiến tài nguyên cạn kiệt rất nhanh.

Đoạn code dưới đây đảm bảo model chỉ nạp vào bộ nhớ đúng một lần:

from transformers import pipeline

st.subheader("Trợ lý AI: Phân tích phản hồi khách hàng")

@st.cache_resource
def get_ai_model():
    return pipeline("sentiment-analysis", model="distilbert-base-uncased-finetuned-sst-2-english")

ai_analyzer = get_ai_model()

user_input = st.text_area(
    "Nhập phản hồi của khách hàng:",
    "The dashboard is extremely responsive and saves our team hours of manual work!"
)

if st.button("Chạy phân tích AI", type="primary"):
    if user_input.strip():
        with st.spinner("AI đang xử lý..."):
            result = ai_analyzer(user_input)[0]
            label = result['label']
            score = result['score']
            
            if label == "POSITIVE":
                st.success(f"Kết quả: Tích cực (Độ tin cậy: {score:.2%})")
            else:
                st.error(f"Kết quả: Tiêu cực (Độ tin cậy: {score:.2%})")
    else:
        st.warning("Vui lòng nhập nội dung đánh giá trước khi phân tích.")

Bước 5: Khởi chạy và kiểm tra ứng dụng

Để chạy ứng dụng ở máy local, gõ lệnh sau vào terminal:

streamlit run app.py

Mặc định ứng dụng sẽ lắng nghe tại cổng http://localhost:8501. Bạn chỉ cần sửa code và bấm Ctrl + S. Trình duyệt sẽ tự động cập nhật giao diện mới ngay lập tức mà không cần khởi động lại tiến trình.

3 bài học xương máu khi đưa Streamlit lên Production

Sau 6 tháng chạy thực tế trên server Ubuntu (4 vCPU, 8GB RAM) cho hơn 30 nhân viên nội bộ truy cập mỗi ngày, mình rút ra ba nguyên tắc quan trọng:

  1. Tuyệt đối không dùng biến toàn cục (global variables): Mọi session của người dùng trên Streamlit đều dùng chung bộ nhớ tiến trình Python. Hãy dùng st.session_state để lưu trạng thái riêng cho từng người dùng, tránh tình trạng dữ liệu người này đè lên người kia.
  2. Khống chế RAM khi chạy model AI: Khi nhiều user cùng bấm gửi text phân tích một lúc, CPU và RAM sẽ tăng đột biến. Bạn nên giới hạn độ dài chuỗi đầu vào (ví dụ tối đa 512 tokens) hoặc lượng tử hóa model sang định dạng ONNX/8-bit để tiết kiệm 50% dung lượng RAM.
  3. Quản lý tiến trình bằng Docker hoặc systemd: Không bao giờ chạy lệnh streamlit run qua terminal rồi để đó. Hãy cấu hình systemd service hoặc đóng gói ứng dụng vào Docker container. Khi ứng dụng gặp lỗi crash hoặc server khởi động lại, dịch vụ sẽ tự động phục hồi mà không cần can thiệp thủ công.
Share: