YOLO 训练做成网页版:FastAPI + SQLite 单机部署的需求设计与技术选型(系列第 2 篇)
公司内部做工业视觉检测,数据集散落各台机器、训练要开命令行敲yolo、训完的 .pt 文件满天飞——现成的 CVAT、ClearML 这类平台要么不管训练要么部署太重。这篇文章讲我怎么用 FastAPI + SQLite + Vue 3 在一台 Windows 机器上搭出"数据 → 标注 → 训练 → 模型"全链路的网页版训练平台,重点是动手之前的需求砍法和技术选型理由。适合想在公司内网给小团队搭内部工具、不想碰 Docker 和 K8s 的个人开发者。
背景:为什么不用现成的
痛点很具体:
- 数据集散落在各台机器的文件夹里,"这个模型用的哪版数据"没人说得清
- 标注靠 LabelImg 单机标,多人协作靠拷文件夹
- 训练要开命令行敲 yolo 命令,非算法同事根本没法自助
- 训练完的 .pt 文件满天飞,哪个是最新的、指标多少,全靠文件名和记忆
先看了一圈现成方案:CVAT 和 LabelStudio 标注很强但不管训练;各种 MLOps 平台(ClearML、Weights & Biases)管训练但对标注和数据集快照的支持要么没有要么很贵;而且它们几乎都要 Docker + 数据库 + 一堆服务,给公司内部的 Windows 机器部署简直是灾难。
结论:自己写一个,把"数据 → 标注 → 训练 → 模型"整条链路塞进一个程序里。
需求:就做这七件事
动手前把需求砍到最小集合,每条都有明确的边界:
- 素材库:导入 YOLO/VOC 格式的 zip,自动识别格式,VOC 内部统一转 YOLO 存储
- 在线标注:浏览器里画框/移动/删除/改类别,支持模型预标注(带 conf 阈值滑杆)
- 数据集版本快照:随时"保存版本",可回滚查看;训练任务必须引用版本,保证可追溯
- 在线训练:选数据集版本 + 填几个参数就能发起,实时曲线,串行队列(一次只跑一个),中断可续训
- 模型版本管理:模型是"系列",每次训练完自动入库一个新版本(.pt + 指标 + 来源 + 操作人)
- 试模型:网页上传图片,选模型版本,出检测框
- 登录与两级权限:admin 管账号和删除,member 干其余所有事
其中第 3 条是整个系统的灵魂,后面单独讲。
技术选型:每一项都是"够用就好"
后端:FastAPI + SQLite,单进程托管一切
浏览器 ←→ FastAPI(单进程) ├── /api/* 业务接口 ├── /* 前端打包后的静态页 └── /data/* 图片等静态文件不装数据库、不装 nginx、不要 Docker。理由:
- 元数据量级小:50 万张图的标注元数据也就 1GB 左右,SQLite 绰绰有余(理论上限 281TB),而且单文件意味着备份 = 拷目录
- 并发低:公司内部同时使用的人一只手数得过来,SQLite 开了 WAL(读不阻塞写)之后,写锁完全不是瓶颈
- 部署是最大约束:目标机器是公司的 Windows 台式机,维护者不是专业运维。每多一个组件(PostgreSQL、nginx、Redis),部署失败率和后续维护成本就翻一倍
这个决定事后看非常正确:整个系统在新机器上的安装就是"建 venv → pip install → 双击启动"。
前端:Vue 3 + Vite + Pinia + ECharts,自绘 CSS
没套 Element Plus / Quasar 这类 UI 库,原因有两个:一是页面一共就七个(登录/数据集/数据集详情/标注/训练/模型库/试模型),自己写卡片和按钮的 CSS 量比引入 UI 库再定制样式的量还小;二是想要浅色极简的风格,UI 库的默认主题改起来反而费劲。ECharts 只用来做训练实时曲线。
训练:subprocess 调 yolo CLI,而不是在进程内 import
这是选型里最重要的一个技术决定。训练不在 FastAPI 进程内跑,而是 spawn 一个独立的yoloCLI 子进程:
- 崩溃隔离:训练 OOM、CUDA 报错、超时,死的只是子进程,Web 服务毫发无损
- 停止简单:psutil 杀掉整棵进程树就行,不用管 PyTorch 内部的线程清理
- 日志天然解耦:子进程输出写日志文件,接口按 offset 增量读,服务重启日志不丢
- 断点续训白送:ultralytics 原生支持从 last.pt 恢复,接上就行
核心代码剥掉项目里的数据库和队列逻辑后就这么点,可以直接抄:
importosimportsubprocessimportpsutildefstart_train(data_yaml,weights,epochs=100,batch=16,imgsz=640):cmd=["yolo","detect","train",f"data={data_yaml}",f"model={weights}",f"epochs={epochs}",f"batch={batch}",f"imgsz={imgsz}"]returnsubprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.STDOUT,text=True,encoding="utf-8",# 子进程输出含中文类名,默认 GBK 解码会崩errors="replace",bufsize=1,env={**os.environ,"PYTHONIOENCODING":"utf-8"},)defstop_train(pid):proc=psutil.Process(pid)procs=proc.children(recursive=True)+[proc]forpinprocs:p.terminate()_,alive=psutil.wait_procs(procs,timeout=1)forpinalive:# Windows 上 terminate 杀不干净,要补 killp.kill()代价是要自己解析子进程输出(进度条用\r刷新、results.csv 列名随版本变化等),怎么解析、怎么画实时曲线、怎么做断点续训,是系列第 3 篇的全部内容。
认证:JWT + 两级角色,不做细粒度权限
python-jose 签 JWT,passlib/bcrypt 存密码哈希,角色只有 admin / member 两级。明确不做:按数据集授权的细粒度权限、审计日志、强制改密、SSO。内部小团队工具,这些全是过度设计——PLAN.md 里专门有一节"明确不做",和需求清单一样重要。
三个关键设计决策
1. 训练必须引用数据集版本,而不是"当前数据"
标注是会持续修改的。如果训练直接读当前数据,那"这个模型到底是哪版标注训出来的"永远说不清——三周后有人改了 200 张图的标注,你对比两个模型的指标时将无从下手。
所以:数据集可以随时"保存版本"(快照当时的全部标注和类别表),发起训练时下拉框里只能选版本。训练读的是快照内容,之后怎么改标注都不影响已发起的任务,旧版本还能回滚查看、对比、恢复。
这一条让系统从"标注工具"升级成了"可复现的训练平台",成本只是多一张版本表。
2. 类别跟着数据集走,且顺序永不可变
标注里存的是 class id,id 的含义由类别表顺序决定。所以类别表一旦确定只能追加不能重排,标注页新增类别也是追加到尾部。多数据集合并训练时,先校验类别名一致性,合并后统一重写所有标签的 class id。(这条的教训意义系列第 1 篇详细写过。)
3. 训练参数只暴露四个
模型规格(n/s/m)、epochs、batch、imgsz——就这四个,其余全部用 ultralytics 默认值。不是偷懒,是刻意的:内部工具的用户大部分不是算法工程师,参数越多,乱调的可能性越大,出了问题越难排查。真有特殊需求,改代码里的默认值比加 UI 控件便宜。
部署形态:Windows 优先,装给非技术同事用
主环境是公司的 Windows 机器,Ubuntu 只是备选。为了这一条做了一串配套决定:
- 后端全部 pathlib,禁手拼路径、禁
os.system,从第一天就杜绝 POSIX-only 调用 - Windows 下训练进程是 spawn 模式,杀进程用 psutil 杀整棵树(
terminate在 Windows 上杀不干净) - zip 解压做 cp437→GBK 兜底(同事在 Windows 上打的包里全是中文文件名)
- 所有程序生成的路径保持纯 ASCII,避开中文用户名/中文路径的深坑
start.bat第一行chcp 65001,控制台中文不乱码
最后还写了一个图形安装向导(installer.py):自动检测 Python、磁盘、显卡(按 CUDA 版本自动选 GPU 版 torch),选完安装目录自动复制程序、建虚拟环境、装依赖、建桌面快捷方式。双击桌面图标启动服务、自动开浏览器,关窗即停服务。不会敲命令的同事也能自己装、自己用——这才是内部工具的真正验收标准。
落地效果:一组真实数字
不吹"效果显著",就摆数字(均为 2026 年 10 月从仓库统计):
- 代码量:后端 Python 约 7800 行,前端 Vue/JS 约 9600 行——一个人业余时间能维护的上限差不多就这规模
- 页面数:7 个页面,覆盖了上面全部 7 条需求,没有一个是"先放着以后再做"的半成品
- 测试:13 个 pytest 用例,只覆盖认证、数据集、训练三条核心路径。测试数量偏少,这是事实,原因是业余项目优先保功能落地;好在核心路径有兜底,改代码时心里有底
- 训练参数:用户能碰的只有 4 个,默认值来自 ultralytics
- 部署:新机器安装 = 图形向导点几下,不用敲一行命令
这套选型什么时候不适用
选型没有银弹,这套方案在以下场景会直接失效,提前说清楚:
- 多人同时训练:串行队列一次只跑一个任务。要并行训练得引入任务队列(Celery/Dramatiq)+ 多 worker,SQLite 也得换成 PostgreSQL,整套"单进程单文件"的简洁性就没了
- 多机多卡调度、分布式训练:subprocess 只能管本机进程,跨机器调度去看 ClearML、Kubeflow 这类正经 MLOps 平台
- 公网部署、陌生人注册:两级角色 + 无审计日志在内部小团队够用,放到公网上从合规到安全都不够
- 几十人并发使用:WAL 也救不了高频并发写,而且单进程托管静态图片文件会先成为瓶颈(元数据本身反而不是问题)
- 团队已有专职运维和容器基建:那"免运维"这个最大卖点对你没有价值,直接上标准技术栈更划算
一句话:这套选型的价值密度全部集中在"一台 Windows 机器、几个人用、维护者不是运维"这个场景里,出了这个圈,请重新选型。
明确不做的(和需求清单同等重要)
写进 PLAN.md 的"二期也不做"清单:
- Docker 化 —— 单机工具,引入 Docker 是纯负担
- nginx —— FastAPI 自己托管静态文件够了
- 细粒度权限、审计日志、强制改密 —— 内部小团队用不上
- 多项目工作区、数据挖掘/难例挖掘 —— 超出现阶段的实际痛点
内部工具最大的死因不是功能少,是过度设计拖到烂尾。每一条"不做"都是在保护这个项目能被做完。
小结
- 内部工具选型,在"够用"和"好维护"之间,永远选好维护。
- FastAPI 单进程 + SQLite 单文件,对"几个人用、一台机器"的系统不是妥协而是最优解——备份 = 拷目录。
- 训练必须引用数据集版本快照,这一条把标注工具升级成可复现的训练平台,成本只有一张版本表。
- 给非算法用户用的工具,暴露的参数越少越好;改默认值永远比加 UI 控件便宜。
- "明确不做"清单和需求清单一样重要,每一条"不做"都是在保护项目能被做完。
FAQ
Q1:SQLite 多人同时用不会锁库吗?
开 WAL 模式(一行PRAGMA journal_mode=WAL),读不阻塞写。这个系统写操作本来就少(保存标注、任务入库),最重的写——训练进度——又被串行队列天然串行化了。真正扛不住的是几十人并发,那是这套选型明确放弃的场景。
Q2:为什么不用 Docker 部署?
目标机器是公司的 Windows 台式机,维护者不是运维。Windows 上跑 Docker 要先过 Docker Desktop + WSL2 这道坎,光装环境就能劝退一半人,和"双击即用"的目标直接冲突。有容器基建的团队本来也不在这篇文章的适用场景里。
Q3:subprocess 调 yolo CLI,网页上的实时曲线数据从哪来?
不解析终端进度条,读文件:训练时 ultralytics 会在 runs 目录持续写results.csv,后端轮询这个文件算进度、喂给 ECharts 画曲线。完整实现(含\r进度条、断点续训)在系列第 3 篇。
Q4:只有一个用户,也要做 JWT 登录吗?
多人共用一台机器就要,操作人字段后面排查问题全靠它。真·单用户可以把登录中间件摘掉,但建议留着——内部工具的"只有我一个人用"通常是暂时的。
Q5:训练参数只暴露四个,同事要调别的怎么办?
改代码里的默认值,重新发布。一次改动几行;加一个 UI 参数要考虑校验、提示、持久化、文档,成本高一个量级,还增加被乱调的风险。这个权衡在内部工具里几乎总是改代码赢。
技术栈:FastAPI · SQLite · Vue 3 · ultralytics · PyInstaller
系列导航
- 系列第 1 篇:开发总览——23 个实践教训
- 系列第 2 篇:需求设计与技术选型(本篇)
- 系列第 3 篇:subprocess 训练进程管理
- 系列第 4 篇:标注数据一致性的 4 个设计
- 系列第 5 篇:Windows 双击即用与 PyInstaller 打包
- 系列第 6 篇:业余时间做内部工具不烂尾的心得
有问题欢迎评论区交流。