在 Docker 化的 Python + React 堆疊中,整合 Clean Architecture、子行程爬蟲、SQLite FTS5 以及本地 LLM(Ollama)。
1. 執行摘要與設計目標
BMW AutoTrend Dashboard 是一個以本地為主、AI 驅動的汽車智慧平台,負責爬取、處理、分類並視覺化與 BMW 相關新聞的市場趨勢與情感分析。該系統設計為完全本地運行,透過 Ollama 本地 LLM 實例進行文章摘要與實體擷取,並搭配確定性的規則式 regex 備援引擎。
平台建置遵循以下設計目標:
- Clean Architecture:將資料擷取層、儲存機制、分析運算與 API 控制器解耦。
- 確定性備援:確保應用程式在本地 LLM 資源(Ollama)離線或資源受限時,仍能使用規則式 regex 處理器維持完整功能。
- 本地優先與高效能:使用 SQLite FTS5 提供即時全文搜尋,所有爬取資料、搜尋索引與分析檔案皆本地儲存,無外部 SaaS API 依賴。
- 即插即用擴展性:透過實作統一抽象介面,即可加入新的爬蟲配接器(如 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
管線分兩階段執行:
-
中繼資料擷取:執行
webcmd bmwblog latest -f json取得最新 10 篇文章(僅中繼資料:URL、標題、簡短摘要)。 -
完整文章擷取:針對每篇 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 Klasse、BMW M3)、對應技術標籤(如 Battery Technology、ADAS),並產生簡短、詳細與 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。編排會設定三個網路隔離的依賴容器:
-
Ollama 容器:在連接埠
11434上拉取並提供 LLM 模型服務。 -
FastAPI 後端容器:由
docker/backend.Dockerfile建置,將資料庫對應到持久化 Docker volumedb_data路徑/app/data/autotrend.db,確保容器重啟後資料仍保留。 -
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
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.