模型训完了,精度指标也刷得差不多,但一到交付环节就卡住了——同事要验收效果,不会敲命令;质检员要求批量跑一批图,你不可能手把手教他激活conda环境再写python脚本。这种时候,把YOLO训练和推理包装成一个网页版工具就成了刚需。我自己在接到这个需求时,脑子里第一反应就是:FastAPI做后端接口、SQLite做轻量存储、单机部署解决一切,技术栈里全是熟人,几乎没有学习成本。但要真把它从想法落地成一个能稳定跑起来的系统,需求设计和技术选型这两关,每一关都有讲究。这篇文章我就把整个思考和踩坑过程完整拆开,给同样想做Web版YOLO训练平台的同学一条能直接参考的路线。
1. 需求设计:先想明白谁在用、要怎么用
动手写代码之前,别急着选框架,先把需求摸透。很多人一上来就问“用什么技术栈”,实际上在单机部署这个场景下,技术选型的弹性很大,真正决定成败的是你对需求的理解是否到位。
1.1 三步拆出真实需求
第一步,搞清楚用户是谁。YOLO训练平台在单机环境下通常只有三类角色:负责训练和调参的算法工程师、需要上传图像做检测验证的业务人员、偶尔登录看一眼状态的管理者。这三类人不在同一个办公室里,但都通过浏览器访问同一个地址。算法工程师关心训练任务的启动、日志查看、模型版本管理;业务人员只关心“传一张图上去能不能检测出来”,不需要理解mAP和loss曲线;管理者只关心“训练进度到哪了,模型什么时候能下载”。一个网页版平台,本质上就是用浏览器把这三类人的诉求全部接住。
第二步,梳理核心功能模块。基于用户角色,我把需求拆成四块:
- 图像推理:上传单张图片或者批量上传,返回检测结果图、检测框坐标、类别与置信度。
- 训练任务管理:创建训练任务(选择数据集、配置epochs、batch size、imgsz等超参数)、查看任务列表、查看训练日志、启动/停止任务、下载训练产物(模型权重文件)。
- 数据集管理:上传压缩包、展示数据集信息(类别列表、图片数量、标注数量)、删除数据集。这一步在本地上做过训练的人会忽略,但实际上网页版没有数据集管理就没有训练闭环。
- 系统管理:查看GPU/CPU资源占用、磁盘剩余空间、服务运行时长等单机状态信息。这块做起来很便宜,但是在调参和排查问题时有奇效。
第三步,明确运行约束。单机部署意味着没有K8s、没有负载均衡、没有多节点训练,所有服务都在一台服务器上跑。这台服务器可能是一台带GPU的工作站,也可能是内网的一台Windows机器。并发量很低——同一时间可能只有几个人在线,真正同时跑训练任务的场景可能只有一个。数据安全要求却很高,因为数据集往往是公司的私有资产,绝对不能泄漏。这些约束决定了后端不能太复杂,但也绝不能把数据暴露在公网上。
1.2 优先级划分:先做什么,后做什么
需求排优先级的时候,我把功能分成了P0、P1、P2三档,这个划分直接影响后面项目排期。
- P0(第一版必须有):图像推理接口、训练任务创建与日志查看、数据集上传、模型文件下载。这四项构成了一个最小闭环:数据集进来→训练任务启动→模型产出→图像推理验证。
- P1(第二版迭代):训练进度百分比可视化、批量推理与结果打包下载、超参数模板保存、多模型版本对比。
- P2(看情况做):用户登录与权限区分、WebSocket实时推送日志、分布式训练支持、标注数据预览。
有一个容易被低估的需求是“停止训练任务”。很多人写训练管理只写了启动和查看,忘了用户会点错参数、会想换个数据集重训。没有停止功能的训练平台,一旦误操作就得去服务器上kill进程,这对网页版产品来说是体验上的致命伤。所以我把停止任务也归到P0,实现起来其实特别简单——子进程的terminate调用而已,但带来的体验提升非常直接。
需求设计的最终输出不是一份文档,而是画清楚“这个系统有哪些页面、每个页面上有哪些按钮、点击之后发生什么”。我习惯把这个过程叫做“在脑子里先跑通一遍流程”,走完这一步再进技术选型,思路清晰得多。
2. 技术选型:为什么偏偏是 FastAPI + SQLite
需求清楚了,技术选型就变得迎刃而解。所谓“选型”,不是选一个最好的技术,而是选一个最匹配当前场景的技术。让我一个个来拆。
2.1 框架对比:Flask、FastAPI、Django 该怎么选
Python生态里能写Web接口的后端框架,主流就是这三个。我简单列一张对比表,把关键差异摆出来:
| 维度 | Flask | FastAPI | Django |
|---|---|---|---|
| 性能 | 一般,同步框架 | 高,原生异步支持 | 一般,同步为主 |
| API文档 | 需第三方插件 | 自动生成Swagger UI | 需第三方插件 |
| 数据校验 | 手动,无内置 | Pydantic,类型安全 | DRF实现较完整 |
| 学习曲线 | 平缓 | 平缓 | 较陡 |
| 适合场景 | 小型API、模板渲染 | API服务、实时交互 | 大型全栈应用、后台管理 |
单机部署的YOLO平台,本质上是API服务后端加一个轻前端页面。Flask当然能写,但FastAPI自带的两大能力恰恰精准命中这个场景:异步处理文件上传和并发查询,以及Pydantic对请求体做自动校验。YOLO推理本身是CPU密集型操作,FastAPI的异步解决不了推理耗时问题,但Web服务的高并发接收请求场景很多:前端批量上传图片的同时还在轮询训练任务状态,这时候FastAPI处理IO密集请求的能力比Flask好得多。Django在这个场景里属于“杀鸡用牛刀”,它太完整了,自带Admin后台和ORM,但换来的是复杂度和资源占用。
我的建议很明确:如果你只需要一套能快速开发、接口文档自动生成、后续方便扩展的服务,FastAPI是三者中最合适的选择。它的自动文档功能在实际联调时省了我大量沟通成本,前端同事只需要打开/docs就能看到每个接口的参数和返回结构,不需要我再写一份几十页的接口文档。
2.2 SQLite 在单机场景下的底气
数据库选型是这个项目里最容易被人质疑的决策。刚提出SQLite,就有同事问“为什么不装一个MySQL”,我的理由其实就一句话:这个系统里根本不存在多个进程高并发写同一个表的场景,SQLite的性能短板完全够用,但它的部署和运维成本趋近于零。
来算一笔账:单机部署意味着数据库只有一个实例,连接数撑死几十个,同时写请求可能只有几秒钟发生一次。SQLite在WAL模式下能稳定支撑每秒几千次的读操作,这在浏览器轮询任务状态、查询推理记录的场景下绰绰有余。而MySQL或者PostgreSQL需要在服务器上单独装服务进程、配置账号、调权限、做备份策略,这些东西在单机场景里全是负资产。多付出的运维成本,换来的性能在这个系统里根本用不出来。
SQLite还有一个隐藏优势:备份极其简单。整库就一个文件,训练任务的关键信息(超参数、任务状态、模型路径)全在里面,我每周用cron自动把数据库文件和数据集目录一起打压缩包,拷到移动硬盘就算完成了备份。这套逻辑在MySQL里虽然也能做,但绝对没有这么轻快。
实践中有一个关键优化必须做:建表完成后立刻执行PRAGMA journal_mode=WAL;和PRAGMA busy_timeout=5000;。WAL模式允许读写并发,busy_timeout则避免多个脚本同时写库时直接报“database is locked”错误。这两个PRAGMA是SQLite在Web服务场景下的生存基础,不加你一定会踩坑。
2.3 前端与任务队列的地板选择
整个技术选型里最容易跑偏的地方,是前端和任务队列。对这两个部分,我的原则是“能省则省,能用最土的办法就别上重型框架”。
前端方面,我没有选择Vue/React加构建工具那一套,而是用FastAPI挂载静态页面 + 原生HTML/JavaScript + Bootstrap样式。原因非常简单:这个平台的页面总数不超过八个,核心交互就两个——上传图片看检测结果、管理训练任务。原生JavaScript加Fetch API足以覆盖所有交互逻辑,不需要前端工程化那一整条流水线。而且这种做法的部署产物极其干净,FastAPI把一个静态目录挂载上,整个服务就完整了。维护成本低到我可以在一个文件里改完所有页面逻辑。如果未来页面复杂度急剧膨胀,再把前端拆分出来做前后端分离完全来得及,后退路径是顺畅的。
任务队列方面,很多人会习惯性地上Celery + Redis来处理训练这种耗时任务。但我反问一句:单机环境里你根本没有多个worker节点,Celery的分布式价值完全发挥不出来,还要白养一个Redis进程。训练任务本质上就是“启动一个Python子进程去跑ultralytics”,我用subprocess.Popen直接挂后台,把训练日志重定向到文件,前端轮询读取任务状态和日志末尾内容,这套方案在单机场景里远比Celery可靠。它少了一条消息队列的链路,也就少了一个故障点,排错的时候不用怀疑“消息是不是丢了、worker是不是挂了”。业务场景还是那句话:并发低、任务少,简单方案就是最稳方案。
3. 系统架构与数据模型设计
选型定了,接下来就是画架构图和建表。这个环节直接决定代码能写多顺畅,也决定后续维护时会不会改一处破三处。
3.1 整体架构:三个核心组件协同
系统的物理组件很简单:FastAPI应用进程、SQLite数据库文件、文件系统目录。FastAPI进程负责接收HTTP请求、处理推理逻辑、管理训练子进程;SQLite存储所有任务、数据集、模型记录;文件系统存放数据集压缩包、解压后的数据、训练产物、日志文件。整个架构不需要任何中间件,三件套各司其职。
数据流的走向是这样的:用户上传数据集压缩包 → FastAPI保存到data/datasets/目录并解压 →datasets表新增记录 → 用户创建训练任务时选择该数据集 → FastAPI启动子进程跑训练,训练产出的模型权重存入data/models/目录 → 用户下载模型或上传图片到推理页面 → FastAPI加载模型推理,结果图存到data/results/并把检测记录写入inferences表。这个链路不需要从头到尾全自动化,每个环节都能在对应页面里手动触发和查看,给用户留出充足的控制权。
3.2 五张表的设计细节
数据库设计我用了五张表,不多不少。拆解如下:
- datasets:数据集记录,字段包括id、名称、压缩包存储路径、解压后目录路径、类别列表(JSON格式)、图片数量、标注数量、创建时间。压缩包和解压目录都存路径而非二进制内容,这是基本原则——数据库只负责索引和元数据,大文件永远交给文件系统。
- train_tasks:训练任务主表,字段包括id、数据集id、模型名称、超参数配置(JSON格式)、任务状态(枚举:PENDING/RUNNING/SUCCESS/FAILED/CANCELED)、GPU编号、日志文件路径、模型输出路径、启动时间、结束时间。这张表是平台的核心,我在超参字段里把所有训练参数打包成JSON存进去,好处是灵活——以后ultralytics新增参数不需要改表结构。
- models:训练产出的模型记录表,字段包括id、任务id、模型文件路径、精度指标(JSON)、模型大小、创建时间。每次训练任务跑成功,自动向这张表插一条记录。
- inferences:推理记录表,每次浏览器端发起图像检测就插一条,字段包括id、模型id、图片路径、结果图路径、检测框数量、检测耗时、创建时间。这张表看起来不起眼,但它是后续做效果统计和质量审计的重要依据。
- system_metrics:系统状态表,定期记录GPU利用率、显存占用、磁盘使用情况。Inference和训练任务跑多了以后,翻这张表能反向定位很多性能问题。
注意一个设计细节:最好不要一刀切地设计“用户表”。如果是纯内网单机使用,不做登录可以省掉一大坨逻辑;但如果这台服务器能被多人同时访问,最简单的做法是给每个用户分配一个静态访问令牌放在URL里,后端用一个FastAPI依赖函数做校验。我建议第一版不做完备的登录系统,把时间花在真正的核心功能上,后期如果安全要求提升再引入JWT不迟。
3.3 文件目录规划:路径统一管理
单机部署最怕文件散落在各处,所以我强推一个严格的文件目录规范:
app/ ├── main.py # FastAPI 入口 ├── database.py # SQLite 连接与建表 ├── inference.py # 模型加载与推理逻辑 ├── trainer.py # 训练子进程管理 ├── static/ # 前端页面资源 ├── data/ │ ├── datasets/ # 数据集压缩包和解压目录 │ ├── models/ # 模型权重文件 │ ├── results/ # 推理结果图片 │ └── logs/ # 训练日志文件所有路径我都可以在database.py里定义成常量,前端获取文件下载链接时也直接引用这些常量。这一步的收益在你归档项目时特别明显,你不需要到处找训练日志藏在了哪个角落,整个数据目录一打包就完整带走。
4. 接口设计与异步任务流程
架构和数据模型定了,接口设计就是把能力暴露出去。这个项目里最难设计的不是推理接口,而是训练任务的异步生命周期管理。我先讲推理,再讲训练,最后讲两者如何共存。
4.1 推理接口:从单图到批量
推理接口的设计要考虑前后端协作的便利性。我第一版只做了单图上传接口,前端用FormData文件上传,后端返回检测框坐标列表和生成的结果图URL。接口定义如下:
@app.post("/api/inference") async def run_inference( model_id: int = Form(...), file: UploadFile = File(...) ): img_bytes = await file.read() img = cv2.imdecode(np.frombuffer(img_bytes, np.uint8), cv2.IMREAD_COLOR) model = get_model(model_id) results = model.predict(img, conf=0.25, imgsz=640) boxes = results[0].boxes # 保存结果图、生成响应JSON、写入inferences表单图跑通后的批量推理,我优先做了压缩包上传——用户把一批图片打包成zip上传,服务端逐张推理,最后把结果图打包成zip返回下载。这个方案的实现比前端循环调用单图接口要省心得多,只需要在服务端用zipfile模块做解压和再压缩。批量推理是业务人员使用频率最高的功能,你一定要在第一版就上线,别听PM说“后续再说”。
模型加载这里有个性能优化点:同一个模型如果每次推理都重新加载,一张图要等两秒以上。我的做法是维护一个全局model_cache字典,把已加载的模型对象缓存起来,按model_id做键。推理时先查缓存,没有才加载。显存有限的情况下,这个缓存还需要设置上限,比如同时最多缓存两个模型,超过时用LRU策略淘汰。
4.2 训练任务接口:创建、查询、取消三件套
训练任务的管理接口是网页版区别于命令行脚本的核心价值所在。我设计了三个核心接口:
- POST /api/train_tasks:创建训练任务,请求体包含数据集id、模型名称、超参JSON。后端逻辑是校验数据集存在、检查GPU资源未被占用、然后拉起子进程。
- GET /api/train_tasks/{id}:查询任务详情,返回状态、日志路径、模型路径等。前端每2秒轮询一次。
- POST /api/train_tasks/{id}/cancel:取消正在运行的任务。
创建训练任务的关键代码:
@app.post("/api/train_tasks") def create_train_task(request: TrainTaskCreate): # 检查是否已有RUNNING任务 running = db.query(TrainTask).filter(TrainTask.status == "RUNNING").first() if running: raise HTTPException(400, "已有训练任务在运行,请先等待完成或取消") # 组装ultralytics训练命令 cmd = [ "python", "-m", "ultralytics", "train", f"data={dataset.yaml_path}", f"model={request.model_name}", f"epochs={request.hparams['epochs']}", f"batch={request.hparams['batch']}", f"imgsz={request.hparams['imgsz']}", f"project={MODEL_DIR}", f"name={task_id}", ] proc = subprocess.Popen(cmd, stdout=log_file, stderr=log_file) task.status = "RUNNING" task.worker_pid = proc.pid db.commit() return task注意这里我把训练跑成了python -m ultralytics的子进程,而不是直接 import ultralytics 在当前进程里调用model.train()。这是有意为之:子进程隔离了训练异常对Web服务的影响——即使训练代码抛错,FastAPI主进程也不会挂,最多是子进程退出,我们只需要把任务状态标成FAILED。直接在主进程调train,一旦内存溢出导致主进程被杀,整个网页服务就没了,这个风险谁都不愿意承担。
4.3 进度上报:不整花活,轮询最稳
训练进度的展示是网页版最能打动用户的细节。我实现得很朴素但可靠:子进程输出完整重定向到日志文件,后端计算进度时解析日志文件的关键行。ultralytics训练日志里有两种关键信息,一种是每个epoch结束打印的Epoch(epoch+1/n)格式的进度行,另一种是最后的Validation results汇总。我写了一个日志解析函数,从日志文件里正则匹配epoch句型和最近的loss值,就能得到一个不错的进度百分比和当前loss读数。
前端轮询接口每2秒拉一次任务详情,返回体里除了任务状态还有progress_percent和last_loss。这个方案的可靠之处在于日志文件即使没有刷新进度,用户看到的也是真实历史日志的尾部,不会出现“前端白屏但后台其实在跑”的困惑。WebSocket推送当然体验更好,但单机环境下轮询省下的复杂度完全淹没它那一点点体验差异——而且轮询还能在浏览器意外关闭后随时恢复状态。
5. 核心实现细节:代码与配置的完整闭环
只看接口设计和表格,很多读者还是不知道项目怎么落地。这一节我给出核心代码骨架和部署配置,方便你直接抄作业、少走弯路。
5.1 推理服务实现:缓存模型与结果图生成
推理部分的实现核心就两块:模型缓存和结果图保存。模型缓存前面提过用字典实现,这里补充结果图保存的逻辑。ultralytics的预测结果对象有一个plot()方法,它会把检测框、类别标签和置信度直接画在输入图像上并返回BGR格式的数组。我把它保存到data/results/{task_id}_{timestamp}.jpg,前端拿到这个URL就能在页面上展示。
import cv2 import numpy as np from fastapi import FastAPI, UploadFile, File, Form, HTTPException _model_cache = {} def get_model(model_id: str): if model_id not in _model_cache: # 从数据库查询模型路径后加载 model = YOLO(model_path) _model_cache[model_id] = model return _model_cache[model_id] @app.post("/api/inference") async def inference(model_id: str = Form(...), file: UploadFile = File(...)): contents = await file.read() image = cv2.imdecode(np.frombuffer(contents, np.uint8), cv2.IMREAD_COLOR) model = get_model(model_id) results = model.predict(image, conf=0.25, imgsz=640) plotted = results[0].plot() save_path = f"data/results/{uuid4().hex}.jpg" cv2.imwrite(save_path, plotted) detections = [] for box in results[0].boxes: detections.append({ "cls": int(box.cls), "conf": float(box.conf), "xyxy": box.xyxy.tolist() }) return {"detections": detections, "result_url": f"/results/{save_path}"}这段代码里有一个重要细节:model.predict需要在FastAPI的同步事件循环里执行。如果你把推理函数声明成async def,FastAPI会用事件循环异步调度,但推理的CPU计算会阻塞事件循环,导致其他请求排队,体验极差。我的做法是定义成普通函数def,FastAPI会自动把它放进线程池执行,这样Web服务的其他接口不会被推理阻塞,体验会好很多。
5.2 训练子进程管理:状态机落地
训练任务的生命周期我实现成一个简单的状态机:PENDING → RUNNING → SUCCESS/FAILED/CANCELED。创建任务时写入PENDING,然后立刻启动子进程并改成RUNNING。子进程退出后,根据exit code判断成功还是失败:0为SUCCESS,非0为FAILED,被cancel则为CANCELED。
子进程的正确管理是这里最容易出错的地方。我遇到过一个问题,直接用subprocess.Popen启动训练时,如果FastAPI服务进程异常退出,训练子进程可能变成僵尸进程继续占用GPU。解决办法是启动子进程时用start_new_session=True让它独立成进程组,服务重启后可以用shutil.which找到残留进程并kill。我还写了一个启动时的残留进程清理函数,每次Web服务启动时检查有没有旧训练进程在占GPU,有就记录下来提醒用户手动清理。
Ultralytics训练命令的超参数拼接也有一点经验教训:不要用shell字符串拼接命令,务必用列表形式传给Popen,避免参数里的特殊字符引发注入问题。epochs、batch这些参数在内部都是整数,但模型名称是用户可输入的字符串,必须走参数列表形式,这是安全红线。
5.3 服务启动配置:systemd 让服务自己活着
开发调试时直接uvicorn main:app --host 0.0.0.0 --port 8000,但真正要长期跑,必须让系统管理这个进程。我在Linux服务器上用systemd的方式:
[Unit] Description=YOLO Web Platform After=network.target [Service] User=yolo_user WorkingDirectory=/opt/yolo_web ExecStart=/opt/miniconda3/envs/yolo/bin/python -m uvicorn main:app --host 0.0.0.0 --port 8000 --workers 1 Restart=always RestartSec=5 [Install] WantedBy=multi-user.target注意这里--workers 1非常重要。很多新手看到性能瓶颈就想开多进程worker,但在这个系统里,多worker会带来两个麻烦:模型缓存分散浪费显存、SQLite并发写锁冲突变多。单进程单worker对这个规模的系统完全够用,而且状态管理简单。机器重启后systemd会自动把服务拉起来,数据库和文件目录都稳定在地磁盘上,这套部署模式我已经稳定跑了很长时间,几乎不需要人工干预。
6. 单机部署实战与踩坑记录
技术选型终于落到真实服务器上,这一步有好些坑等着你。我把踩过的坑按头疼程度排个序,全部值得你先看再动手。
6.1 从头到尾的部署步骤速览
部署流程大致如下,我按顺序列出来:
- 准备运行环境:安装Miniconda或Miniforge,创建名为yolo的Python 3.10虚拟环境,安装PyTorch(根据CUDA版本选对应轮子)、ultralytics、fastapi、uvicorn、sqlite3不需要额外装。
- 验证GPU可用:在yolo环境里跑一段测试代码确认
torch.cuda.is_available()返回True。这里有个坑,很多服务器的显卡驱动是新装的,但CUDA toolkit版本和PyTorch不匹配,会导致import torch直接报错,必须提前排掉。 - 安装项目依赖:把项目克隆到 /opt/yolo_web,执行前端静态文件放置、数据库初始化脚本。数据库初始化我写成了一条命令:
python database.py就会自动建表。 - 准备测试模型:下载一个YOLO预训练权重放在
data/models/下,先测试推理接口能通。如果预训练权重都跑不通,训练接口必然也跑不通,别浪费时间去查后面的问题。 - 启动服务并验证:systemd启动后访问
http://服务器IP:8000,确认页面加载、推理接口返回正常。这一步要在服务器本机先用curl验证API,再在局域网其他机器上验证网页打开,两步分开能精准定位是服务问题还是网络问题。
6.2 我踩过的五个平台级大坑
第一个坑是SQLite的锁等待问题。刚上线时,前端每2秒轮询一次任务详情,同时训练子进程也在写日志,偶尔还夹杂一个数据集上传写入,某个瞬间多个写请求撞在一起,就报了“database is locked”。解决办法我提过,用WAL模式加busy_timeout,但还有一个细节:所有写操作尽量走同一个连接。FastAPI里我实现了每次请求新建连接,操作用完后立刻关闭,避免长时间持锁。后来进一步把写操作合并成按顺序执行——推理记录和任务状态更新合并到一个事务里,锁冲突的频率降到了零。
第二个坑是模型文件版本混乱。训练任务跑完产出best.pt和last.pt两个权重,如果不注意重命名,下次训练任务会把文件覆盖,历史模型就丢了一个版本。我现在的做法是每次训练完成后,将产出文件立即复制成task_{id}_best.pt这种带任务ID的固定名称,源文件保留不动。这条规则让模型下载接口的实现变得异常简单,数据库里存的就是带ID的文件名,直接拼路径即可。
第三个坑是显存不够导致的推理失败。训练任务占用GPU显存后,推理模型如果也尝试加载进GPU就会OOM。我的解决策略是:默认让推理模型跑CPU。推理请求本来就不频繁,单张图CPU推理耗时也就几百毫秒到一两秒,完全可接受;训练子进程独占GPU,互不干扰。这套策略让我彻底绕开了复杂显存调度,系统运行特别稳。如果你实在希望推理也走GPU,可以在推理接口里捕获OOM异常后自动降级为CPU推理。
第四个坑是训练日志中文乱码。ultralytics在Windows服务器上跑时,日志输出可能带上非UTF-8编码的内容,导致日志解析函数读出的字符串乱码,进度解析直接失败。解决办法是启动子进程时强制设置环境变量PYTHONIOENCODING=utf-8,并把日志文件以encoding='utf-8'方式打开。这个坑花费了我一下午排查,后来在Linux上完全没碰到,但也说明了跨平台部署时编码问题必须提前处理。
第五个坑是端口被其他服务占用。服务器上跑着别的Web服务,或者防火墙规则限制了8000端口。我现在的启动脚本里会先执行ss -tlnp | grep 8000查看端口占用情况,再用firewall-cmd --list-ports检查端口放行。如果端口冲突,就换个端口并同步更新Frontend里的API地址变量。
7. 常见问题速查表与避坑建议
项目跑了一段时间后,我把运维中常遇到的问题整理成了一张表,贴在内网知识库里,后来同事自己就能处理一大半问题。
| 问题现象 | 最可能原因 | 排查方法 | 解决办法 |
|---|---|---|---|
| 页面能开,图片上传后转圈报错 | 服务器磁盘空间满 | df -h检查 | 清理data/results历史结果图 |
| 创建训练任务报“GPU被占用” | 上一次训练子进程没杀干净 | ps aux | grep ultralytics | kill 残留进程后重试 |
| 训练日志不刷新或者进度卡住 | 日志文件被截断或编码问题 | 手动tail -f日志文件 | 确认启动环境变量 PYTHONIOENCODING=utf-8 |
| 推理接口第一次调用非常慢 | 模型冷启动加载 | 查看日志确认是加载耗时 | 预热模型:启动时后台自动加载常用模型 |
| 倒腾模型后检测结果明显差 | 前端调用的模型id过期 | 查看模型表路径是否指向旧文件 | 训练后更新模型表记录,清理旧权重 |
这里我想特别强调“磁盘空间”这个隐性杀手。单机部署的服务器通常没人定时清理推理结果图和过期数据集,跑上两三个月磁盘就会悄悄满掉,然后各种奇奇怪怪的问题接连出现。我的做法是写一个定时清理脚本,每天凌晨删除超过7天的推理结果图和超过30天的临时上传文件,任务日志保留最近50条。这个小脚本的成本不到十行,但避免了运维事故。
还有一条运维经验值推荐大家用:每次模型训练开始前,自动把当前磁盘可用空间写入训练任务的备注字段。这样如果训练中途磁盘满导致失败,你能立刻判断是磁盘问题而不是算法问题。类似这种面向真实故障场景的小设计,才是项目能稳定跑下去的关键。
8. 单机部署的扩展思路与个人体会
这套方案做完后,我复盘过很多次,越觉得“简单”本身就是它的核心价值。很多团队一提到Web化就想到上K8s、走微服务、配Redis队列,但单机部署场景里,一个FastAPI进程、一个SQLite文件、两个目录,就把训练、推理、管理三件事全部覆盖了。烂大街的组件组合在一起,用克制的方式解决了真实问题,这就是我在这类项目中偏爱的风格。
如果你后续想把系统做得更强,有两条扩展路径比较自然:一是把前端从原生HTML升级成Vue/React做更复杂的交互视觉,后端接口基本不用改,因为FastAPI天然适合前后端分离;二是如果团队开始多人协作训练多模型,可以考虑把SQLite迁移成PostgreSQL,训练任务管理部分的数据结构也可以维持不动,改动只集中在database层。这套架构像积木一样,每一块都可以被单独替换,不用推翻重来。
最后分享一个我个人的经验:做这种“工具类”产品,别追求技术上的炫技,把精力放在拆解需求链路和打磨异常处理上。用户打开页面点几个按钮,就能完成之前要在命令行里敲十几条命令才能做的事,这一眼可见的价值感比任何架构图都更有说服力。技术选型没有标准答案,但对单机、低并发、重任务型Web工具的YOLO训练平台来说,FastAPI和SQLite的组合已经在我这里验证过足够久:它不花哨,但它非常耐用。