從零打造私有化 Local LLM 模型管理系統:React + Node.js + Docker

Github : https://github.com/fishbob889/local-llm-manager

隨著開源大型語言模型 (LLM) 的爆發,硬碟裡堆滿了各式各樣的 .safetensorsGGUF 檔案。原本使用指令行 (huggingface-cli) 下載模型雖然快速,但隨著模型數量增加,管理變得一團混亂:不知道哪個資料夾是做什麼的、檔案多大、何時下載的。

所以,開發一個 Local LLM Manager。這是一個輕量級、無需資料庫 (No-Database)、前後端分離的 Web 管理介面,專門用來管理與下載 Hugging Face 模型。

專案重點:

  • 視覺化模型庫 (Library):以卡片式介面瀏覽本地模型,支援依大小、日期、名稱排序。

  • 即時下載監控:整合 Python 下載腳本,透過 WebSocket 將進度條即時串流到網頁前端(駭客風格終端機介面)。

  • 批次下載 (Batch Download):支援一次貼上多個 Model ID,自動排程佇列下載。

  • 中繼資料管理:可直接在網頁上編輯模型說明 (description.txt),資料隨檔案夾帶著走。

  • 暖色系 UI 設計:採用 Tailwind CSS 打造舒適的 Latte/Paper 風格。

  • 安全部署:整合 Nginx Proxy Manager (NPM) 與 Basic Auth 保護。


系統架構

為了保持系統輕量且易於遷移,採用了 「檔案即資料庫 (File-system as Database)」 的設計哲學:

  • Frontend: React (Vite) + Tailwind CSS + Lucide Icons

  • Backend: Node.js (Express) + WebSocket (ws library)

  • Worker: Python (huggingface_hub SDK) 負責實際下載任務

  • Infrastructure: Docker Compose (Multi-stage build) + Nginx Proxy Manager

架構圖

  1. 使用者 透過瀏覽器存取 React 前端。

  2. 前端 發送 API 請求給 Node.js 後端。

  3. 後端 掃描 /data/models 掛載目錄,即時計算資料夾大小與讀取說明檔。

  4. 下載時,Node.js 呼叫 Python 子程序 (Spawn Process)。

  5. Python 輸出 Log,Node.js 透過 WebSocket 將 Log 廣播回前端顯示。


開發過程中的技術挑戰與解法

過程中,遇到了幾個有趣的技術坑,以下是解決方案:

1. WebSocket 訊息風暴 (Message Storm)

問題:Hugging Face 的下載進度條更新頻率極高(每秒數十次),直接轉發給前端會導致 Nginx 緩衝區溢位或瀏覽器卡頓,最終導致連線中斷 (DISCONNECTED)。

解法:在 Node.js 後端實作 「節流機制 (Throttling)」。 我們設定一個 500ms 的緩衝時間,只有當錯誤發生或距離上次廣播超過 500ms 時,才發送進度更新。這樣既保留了進度感,又保護了連線穩定性。

JavaScript

// Backend 節流邏輯片段
if (isError || (now - lastBroadcastTime > 500)) {
  lastBroadcastTime = now;
  broadcast({ ... });
}

2. Nginx 反向代理的中斷問題

問題:即使後端修好了,下載大模型時(如 70GB 的 Llama-3),連線依然會莫名中斷。

解法:這是 Nginx 的設定問題。必須在 Nginx Proxy Manager 中明確開啟 WebSocket 支援,並延長超時設定:

Nginx

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s; # 關鍵:防止長連線被切斷

3. React 批次下載的競態條件 (Race Condition)

問題:在實作「批次下載」功能時,使用者貼上清單後立刻點擊開始,卻發現佇列是空的。原因是 React 的 useState 是非同步更新的,狀態還沒寫入,程式碼就先執行了。

解法:不依賴 State 的更新結果,而是直接將 e.target.value (使用者的原始輸入) 傳遞給解析函式,確保資料的一致性。

4. 排序功能的陷阱

問題:在前端進行「按大小排序」時,結果是錯的。原因是 API 回傳的是格式化後的字串(如 “10 GB”, “2 MB”),字串排序會導致 “10 GB” 排在 “2 MB” 前面。

解法:後端 API 除了回傳顯示用的字串,同時回傳原始 Bytes 數字 (sizeBytes),前端使用該數字進行精確排序。


部署與安全性

由於這是私有服務,我不希望將其暴露在公網而不加保護。與其在程式碼內寫複雜的登入系統,我選擇利用 Nginx Proxy ManagerAccess Lists 功能。

這是一個優雅的「無程式碼」解決方案:在 Nginx 層級攔截請求,彈出瀏覽器原生的帳號密碼視窗 (Basic Auth)。只有驗證通過的請求才會轉發給我的 Docker 容器,確保了絕對的安全。

結語

這個專案雖然不大,但涵蓋了 Full Stack 開發的精髓:從 Docker 環境建置、後端串流處理、前端狀態管理到反向代理設定。

如果你也有整理本地 LLM 的困擾,不妨試試看這個架構!

Tech Stack: React, Node.js, Python, Docker, Nginx GitHub: (GitHub連結)

發佈留言

65 − = 63
Powered by MathCaptcha

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料