在 Docker 化的 Python + React 技术栈中编排 Clean Architecture、子进程爬虫、SQLite FTS5 和本地 LLM(Ollama)。
1. 执行摘要与设计目标
BMW AutoTrend Dashboard 是一个本地优先、AI 驱动的汽车情报平台,负责抓取、处理、分类并可视化 BMW 相关新闻的市场趋势与情感。该系统设计为完全本地运行,通过 Ollama 本地 LLM 实例进行文章摘要与实体抽取,并由确定性的基于规则的正则回退引擎提供备份。
该平台构建时遵循以下设计目标:
- Clean Architecture:将数据抽取层、存储机制、分析计算与 API 控制器解耦。
- 确定性回退:即使本地 LLM 资源(Ollama)离线或资源受限,应用仍可使用基于规则的正则处理器完全正常运行。
- 本地优先与高性能:使用 SQLite FTS5 实现即时全文搜索,所有抓取数据、搜索索引与分析文件均本地存储,无外部 SaaS API 依赖。
- 即插即用扩展性:通过实现统一的抽象接口,可插入新的抓取适配器(如 Autoblog、MotorTrend)。
2. Clean 系统架构
应用遵循 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、标题、简短摘要)。 -
深度文章入库:对于每篇 新 URL(通过 SQLite 索引判断),执行
webcmd bmwblog article <url> -f json提取正文、分类、作者,并从原始 HTML 中抓取 OpenGraph 图片(og:image)。
4. 混合 AI 分类流水线(Ollama 与正则回退)
全文入库后,内容会被派发到 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 发送精心构造的提示。为确保确定性集成,请求强制结构化 JSON 输出:
payload = {
"model": settings.OLLAMA_MODEL,
"prompt": prompt,
"format": "json",
"stream": False,
"options": { "temperature": 0.1 }
}
Enter fullscreen mode Exit fullscreen mode
模型将分类情感(Positive、Neutral、Negative),提取相关车型(如 Neue Klasse、BMW M3),映射技术标签(如 Battery Technology、ADAS),并生成简短、详细及 TL;DR 摘要。
2. 基于规则的正则回退流水线
若 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 中的“趋势主题”组件。
历史快照
为避免每次加载页面时执行高 CPU 聚合,应用使用 APScheduler 在接近午夜时执行 generate_daily_snapshot。该函数将计算后的统计序列化为 JSON 并存入 analytics_snapshots 表,从而实现 30 天历史时间线的快速渲染。
7. 前端架构(React + Vite + Recharts)
前端是基于现代 React + TypeScript + Vite 架构构建的单页应用。
UI 特性
- 玻璃态主题:基于 CSS 变量、自定义 Outfit 字体与发光边框构建的高级暗色模式界面。
-
组件布局:侧边栏与主面板在
layouts/DashboardLayout.tsx中组织,以实现响应式导航。 -
可视化:使用
Recharts展示动态数据:- 每日文章量的面积图。
- Positive/Neutral/Negative 趋势的 30 天情感时间线。
- 车型与技术标签流行度的柱状图。
- 实时搜索:捕获按键,调用 API 查询 SQLite FTS5 索引,并以亚毫秒级延迟高亮匹配结果。
8. Docker 编排与 DevSecOps 洞见
平台设计为仅需一条命令即可启动:docker compose up --build。编排配置了三个网络隔离的依赖容器:
-
Ollama 容器:在端口
11434上拉取并提供 LLM 模型服务。 -
FastAPI 后端容器:由
docker/backend.Dockerfile构建。它将数据库映射到持久化 Docker 卷db_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.