在 Docker 化的 Python + React 堆疊中,整合 Clean Architecture、子行程爬蟲、SQLite FTS5 以及本地 LLM(Ollama)。


1. 執行摘要與設計目標

BMW AutoTrend Dashboard 是一個以本地為主、AI 驅動的汽車智慧平台,負責爬取、處理、分類並視覺化與 BMW 相關新聞的市場趨勢與情感分析。該系統設計為完全本地運行,透過 Ollama 本地 LLM 實例進行文章摘要與實體擷取,並搭配確定性的規則式 regex 備援引擎。

平台建置遵循以下設計目標:

  1. Clean Architecture:將資料擷取層、儲存機制、分析運算與 API 控制器解耦。
  2. 確定性備援:確保應用程式在本地 LLM 資源(Ollama)離線或資源受限時,仍能使用規則式 regex 處理器維持完整功能。
  3. 本地優先與高效能:使用 SQLite FTS5 提供即時全文搜尋,所有爬取資料、搜尋索引與分析檔案皆本地儲存,無外部 SaaS API 依賴。
  4. 即插即用擴展性:透過實作統一抽象介面,即可加入新的爬蟲配接器(如 Autoblog、MotorTrend)。

2. Clean System Architecture

應用程式遵循 Clean Architecture 原則,維持嚴格的單向資料流,並將業務邏輯與外部依賴隔離。

graph TD
    subgraph Frontend [React SPA - Served via Nginx]
        UI[Interactive Dashboard Pages]
        RC[Recharts Visualizations]
        FTS_UI[Spotlight Search UI]
    end

    subgraph Backend [FastAPI Application]
        API[FastAPI Endpoints]
        SCH[APScheduler Ingestion & Snapshots]
        ING[Ingestion Pipeline]
        ANA[Analytics Engine]
        AIP[AI Processing Pipeline]
    end

    subgraph CLI Bridge [Subprocess Execution]
        WBC[Webcmd CLI + BMWBLOG Plugin]
    end

    subgraph Storage [Local Storage]
        DB[(SQLite Database)]
        FTS[(FTS5 Search Index)]
    end

    subgraph Local LLM [AI Inference]
        OLL[Ollama Server]
    end

    UI -->|Queries| API
    API -->|Reads/Writes| DB
    ING -->|Executes| WBC
    WBC -->|Scrapes Web Data| BMWBLOG[BMWBLOG Site]
    ING -->|Sends content for classification| AIP
    AIP -->|Requests JSON| OLL
    AIP -->|Regex Fallback| AIP
    SCH -->|Triggers| ING
    SCH -->|Runs daily| ANA
    ANA -->|Computes stats| DB
    FTS_UI -->|Queries FTS| API
    API -->|FTS Match Query| FTS

Enter fullscreen mode Exit fullscreen mode

  • 前端層:使用 Vite 建置的 React + TypeScript SPA,透過標準化 JSON REST 端點與後端互動。
  • API 控制器:FastAPI 處理器使用 Pydantic 驗證結構描述、查詢儲存並觸發背景任務。
  • 核心業務邏輯
    • Ingestion Pipeline 負責協調爬蟲、重複檢查與資料寫入。
    • AI Processing Pipeline 執行文字分析、摘要與標籤擷取。
    • Analytics Engine 聚合指標並維護每日快照。
  • 資料提供者:以子行程包裝 Node.js CLI 工具(webcmd)的執行。
  • 資料庫層:透過 SQLAlchemy ORM 管理 SQLite,並搭配原始 SQL 資料庫觸發器強化功能。

3. 資料擷取管線與子行程 WebCMD 爬蟲

資料擷取從 [providers/base.py] 中的抽象基底類別 NewsProvider 開始。具體實作如 providers/bmwblog.py 中的 BMWBlogProvider,負責從出版商爬取資料。

後端並未自行撰寫易碎且難以維護的自訂網頁爬蟲,而是使用底層的 @agentrhq/webcmd CLI 工具。BMWBlogProvider 透過子行程執行 webcmd 取得結構化文章摘要。

子行程執行機制

# snippet from backend/providers/bmwblog.py
def _run_webcmd(self, args: List[str]) -> str:
    cmd = ["webcmd"] + args
    try:
        # We use shell=True on Windows because webcmd is a script (.ps1 or .cmd)
        is_windows = os.name == 'nt'
        result = subprocess.run(
            cmd,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            shell=is_windows,
            check=True
        )
        return result.stdout
    except subprocess.CalledProcessError as e:
        logger.error(f"webcmd execution failed: {result.stderr}")
        raise Exception(f"webcmd error: {result.stderr}")

Enter fullscreen mode Exit fullscreen mode

管線分兩階段執行:

  1. 中繼資料擷取:執行 webcmd bmwblog latest -f json 取得最新 10 篇文章(僅中繼資料:URL、標題、簡短摘要)。
  2. 完整文章擷取:針對每篇 SQLite 索引中判定為「新」的 URL,執行 webcmd bmwblog article <url> -f json 取得完整本文、分類、作者,並從原始 HTML 抓取 OpenGraph 圖片(og:image)。

4. 混合式 AI 分類管線(Ollama 與 Regex 備援)

完整文章文字擷取後,會分派至 ai/processor.py。處理器會先動態檢查 Ollama 是否可用,再決定使用哪種評估引擎:

graph TD
    A[New Article Ingested] --> B{Is Ollama Server Online?}
    B -->|Yes| C[Call Ollama Llama 3.2 API]
    C --> D{Parsing JSON Successful?}
    D -->|Yes| E[Save AI Classification to Database]
    D -->|No| F[Fallback to Regex Rule Engine]
    B -->|No| F
    F --> G[Extract Entities, Sentiment, & Summaries via Rules]
    G --> E

Enter fullscreen mode Exit fullscreen mode

1. Ollama LLM 管線

若 Ollama 伺服器在線並執行 llama3.2,後端會對 /api/generate 發出 POST 請求,並附上精心設計的提示詞。為確保整合結果具有確定性,請求會強制要求結構化 JSON 輸出:

payload = {
    "model": settings.OLLAMA_MODEL,
    "prompt": prompt,
    "format": "json",
    "stream": False,
    "options": { "temperature": 0.1 }
}

Enter fullscreen mode Exit fullscreen mode

模型會分類情感(正面、中性、負面)、擷取相關車型(如 Neue KlasseBMW M3)、對應技術標籤(如 Battery TechnologyADAS),並產生簡短、詳細與 TL;DR 摘要。

2. 規則式 Regex 備援管線

若 Ollama 離線(或無法回傳有效 JSON),則執行 _analyze_with_rules。它會使用預先編譯的正規表示式來比對關鍵字與標籤:

TECHNOLOGY_TAGS = {
    "Electric Vehicles": r"\b(ev|evs|electric|zero-emission|zero emission|battery electric|bev)\b",
    "Battery Technology": r"\b(battery|batteries|solid-state|cell|cells|rimac)\b",
    "Autonomous Driving": r"\b(autonomous|self-driving|driverless|autopilot)\b",
    # ...
}

# Sentiment heuristic based on term tallying
pos_count = sum(len(re.findall(rf"\b{word}\b", combined_text)) for word in SENTIMENT_POSITIVE)
neg_count = sum(len(re.findall(rf"\b{word}\b", combined_text)) for word in SENTIMENT_NEGATIVE)

Enter fullscreen mode Exit fullscreen mode

這種混合模式可確保資料庫欄位填入有效分類與標籤,且搜尋功能不會因硬體限制而受影響。


5. 使用資料庫觸發器的高效能 SQLite FTS5 搜尋索引

為提供高速、本地優先的搜尋,專案捨棄緩慢的 LIKE %query% SQL 操作,改用 SQLite 原生 FTS5(全文搜尋) 擴充功能。

資料庫結構初始化與觸發器

database/connection.py 中,應用程式使用 SQLAlchemy 啟動資料庫,並透過連線事件鉤子強制啟用外鍵約束(PRAGMA foreign_keys=ON),以及手動建立 FTS5 虛擬表與同步觸發器:

-- FTS5 Virtual Table Configuration
CREATE VIRTUAL TABLE articles_fts USING fts5(
    title, 
    excerpt, 
    content, 
    content='articles'
);

-- Synchronization Triggers (Insert, Delete, Update)
CREATE TRIGGER articles_ai AFTER INSERT ON articles BEGIN
    INSERT INTO articles_fts(rowid, title, excerpt, content)
    VALUES (new.id, new.title, new.excerpt, new.content);
END;

Enter fullscreen mode Exit fullscreen mode

這些觸發器將搜尋索引工作直接交由 SQLite 引擎處理。當透過 SQLAlchemy 提交新文章時,SQLite 會自動將標題、摘要與內容索引到 FTS5 陰影表中。

相關性排序搜尋端點

當使用者在儀表板搜尋時,後端會執行 BM25 相關性排序查詢:

# Snippet from backend/api/routes.py
query_str = """
    SELECT rowid FROM articles_fts 
    WHERE articles_fts MATCH :query
    ORDER BY bm25(articles_fts)
"""
result = db.execute(text(query_str), {"query": search_term})

Enter fullscreen mode Exit fullscreen mode

此查詢可在毫秒級完成,讓前端實現即時「隨打即搜」的體驗。


6. 動態分析引擎與滾動趨勢運算

analytics/engine.py 中的 Analytics Engine 負責執行滾動視窗計算,以判斷哪些汽車趨勢正在獲得關注。

引擎並非僅追蹤基本計數,而是透過比較車型與技術在最近 7 天與前一個 7 天視窗內的提及次數,計算 每週成長率

# 7-day boundaries
seven_days_ago = today_start - timedelta(days=7)
fourteen_days_ago = today_start - timedelta(days=14)

# Mentions in last 7 days (recent) vs 7-14 days ago (previous)
# Growth calculation:
if prev_cnt == 0:
    growth_pct = 100.0 if recent_cnt > 0 else 0.0
else:
    growth_pct = ((recent_cnt - prev_cnt) / prev_cnt) * 100.0

Enter fullscreen mode Exit fullscreen mode

結果依 growth_rate DESC 排序,以填入 UI 中的「Trending Topics」小工具。

歷史快照

為避免每次使用者載入頁面都執行大量 CPU 聚合運算,應用程式使用 APScheduler 在接近午夜時執行 generate_daily_snapshot。此函式將計算後的統計資料序列化為 JSON,並儲存至 analytics_snapshots 資料表,讓前端能快速渲染 30 天歷史時間軸。


7. 前端架構(React + Vite + Recharts)

前端是建置於現代 React + TypeScript + Vite 架構上的單頁應用程式。

UI 功能

  • 玻璃擬態主題:基於 CSS 變數、自訂 Outfit 字型與發光邊框打造的高級暗色模式介面。
  • 元件佈局:側邊欄與主面板於 layouts/DashboardLayout.tsx 中定義,提供響應式導航。
  • 視覺化:使用 Recharts 顯示動態資料:
    • 每日文章量的面積圖。
    • 30 天內正面/中性/負面情感趨勢的時間軸。
    • 車型與技術標籤熱門度的長條圖。
  • 即時搜尋:擷取按鍵輸入,透過 API 查詢 SQLite FTS5 索引,並以毫秒級延遲高亮顯示匹配結果。

8. Docker 編排與 DevSecOps 洞察

平台設計為單一指令啟動:docker compose up --build。編排會設定三個網路隔離的依賴容器:

  1. Ollama 容器:在連接埠 11434 上拉取並提供 LLM 模型服務。
  2. FastAPI 後端容器:由 docker/backend.Dockerfile 建置,將資料庫對應到持久化 Docker volume db_data 路徑 /app/data/autotrend.db,確保容器重啟後資料仍保留。
  3. Nginx 前端容器:多階段建置,編譯 React 應用程式,並使用 Nginx 在連接埠 80 提供靜態資源,同時反向代理至後端。

技術 Docker 提示:Node.js 子行程需求

由於 Python 後端會以子行程方式呼叫 webcmd(npm 函式庫),後端 Dockerfile 必須支援多執行階段環境。在 python slim 容器內安裝 Node.js、npm 並全域安裝 @agentrhq/webcmd,可確保資料擷取管線順利執行:

FROM python:3.12-slim
WORKDIR /app

# Install Node.js, NPM, and global webcmd CLI scraper
RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential curl gnupg \
    && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
    && apt-get install -y nodejs \
    && npm install -g @agentrhq/webcmd \
    && webcmd plugin install github:agentrhq/webcmd/bmwblog \
    && rm -rf /var/lib/apt/lists/*

Enter fullscreen mode Exit fullscreen mode


9. 擴展性與未來發展

AutoTrend Dashboard 的設計易於擴展:

  • 新增新聞來源:建立實作 NewsProvider 抽象基底類別的新類別(如 AutoblogProvider),對應爬蟲外掛指令(如 webcmd autoblog latest),並在 run_ingestion_pipeline 中註冊。
  • 外部 LLM:可透過 [ai/client.py] 中的 AI 用戶端,使用環境變數設定 API 金鑰,擴展支援遠端雲端模型(如 Gemini API 或 OpenAI API)。
  • 進階情感啟發式:規則式引擎可升級為支援 VADER 或 Hugging Face 的 distilbert-base-uncased-finetuned-sst-2-english 等 Transformer 模型,以進行深度離線情感擷取。

為 BMW AutoTrend Dashboard 程式碼庫撰寫的技術文件。GitHub 儲存庫位於 https://github.com/scha54/BMW-AutoTrend-Dashboard