在 Docker 化的 Python + React 技术栈中编排 Clean Architecture、子进程爬虫、SQLite FTS5 和本地 LLM(Ollama)。


1. 执行摘要与设计目标

BMW AutoTrend Dashboard 是一个本地优先、AI 驱动的汽车情报平台,负责抓取、处理、分类并可视化 BMW 相关新闻的市场趋势与情感。该系统设计为完全本地运行,通过 Ollama 本地 LLM 实例进行文章摘要与实体抽取,并由确定性的基于规则的正则回退引擎提供备份。

该平台构建时遵循以下设计目标:

  1. Clean Architecture:将数据抽取层、存储机制、分析计算与 API 控制器解耦。
  2. 确定性回退:即使本地 LLM 资源(Ollama)离线或资源受限,应用仍可使用基于规则的正则处理器完全正常运行。
  3. 本地优先与高性能:使用 SQLite FTS5 实现即时全文搜索,所有抓取数据、搜索索引与分析文件均本地存储,无外部 SaaS API 依赖。
  4. 即插即用扩展性:通过实现统一的抽象接口,可插入新的抓取适配器(如 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

流水线分两个阶段执行:

  1. 元数据获取:执行 webcmd bmwblog latest -f json 获取最新 10 篇文章(仅元数据:URL、标题、简短摘要)。
  2. 深度文章入库:对于每篇 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 KlasseBMW M3),映射技术标签(如 Battery TechnologyADAS),并生成简短、详细及 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。编排配置了三个网络隔离的依赖容器:

  1. Ollama 容器:在端口 11434 上拉取并提供 LLM 模型服务。
  2. FastAPI 后端容器:由 docker/backend.Dockerfile 构建。它将数据库映射到持久化 Docker 卷 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