搞农业项目的人应该都有同感:生菜这玩意儿长得快,但恰恰是“长得快”让监测成了麻烦事。一个中型大棚几千株生菜,想知道整体处于什么生长阶段、哪片区域要浇水、哪片该采收,靠人蹲在地里一株一株看,不现实。而拿通用目标检测模型去框生菜,又不难——因为生菜和行人、车辆不一样,画面相对固定、目标形态稳定。真正难的,是把“检测”变成“监测”,把“监测”变成“管理”。
这套系统的核心思路,就是把 YOLOv8/YOLOv10/YOLOv11/YOLOv12 四代目标检测模型当成眼睛,识别生菜不同生长周期;用 SpringBoot 搭后端服务串联整体业务;再用 DeepSeek 大模型把检测结果翻译成“当前该做什么”的建议,最后通过前后端分离的 Web 界面把整个流程呈现出来。也就是说,检测只是第一步,理解生长状态、输出管理决策才是这套系统的价值所在。
这套东西适合谁参考?如果你在做智慧农业相关的项目,或者正在选型 YOLO 版本做目标检测,再或者想把目标检测结果和 LLM 分析能力接起来做产品化,往下看都值得。完整跑通之后,它就是一个小而全的“视觉检测 + 业务后端 + 智能分析 + Web 展示”样板,所有模块都能单独替换。
1. 生菜生长周期检测的业务逻辑与技术选型
1.1 这个检测任务到底难在哪
很多人刚接触这个项目时,第一反应是:生菜检测不就是框出来标个类别吗?目标检测的标准流程而已。但真正把数据集做出来才发现,难的不是“框住生菜”,而是“区分生长阶段”。
生菜的完整生长周期大致可以分成 萌动期、幼苗期、莲座期、结球期、成熟采收期 五个阶段。萌芽期和幼苗期的区分主要看叶片数量,莲座期到结球期的变化是叶片开始抱合,结球期和成熟期的区别则要看叶球紧实度。这些阶段之间是连续过渡的,没有清晰边界,人眼判断都存在主观差异,更别说机器了。
这就是这类农业视觉项目和常规目标检测的本质差异。通用目标检测里,猫就是猫、车就是车,不同类别之间有明确的物理边界。而生菜生长阶段分类更像是一个“细粒度图像分类 + 目标检测”的复合问题。同一株生菜,早三天拍和晚三天拍,类别标签可能不同;同一张照片里,不同区域的生菜因为光照、水肥差异,可能处于不同生长阶段。所以模型不仅要学会框目标,还要学会从叶色、叶形、叶球紧实度这些细微特征上做判断。
另外还有一个实际痛点:生菜叶片叠放严重。从俯视角度拍摄,叶片互相遮挡,交并比(IoU)很大,这会让 NMS 容易误删正确检测框,也是后面做模型选型时的重要考量因素。
1.2 为什么选 YOLO 系模型而不是其他方案
市面上的检测模型方案不少,两阶段的 Faster R-CNN,单阶段的 SSD,还有基于 Transformer 的 DETR 系列。这个项目选 YOLO,不是因为它“最先进”,而是因为它最适合这个场景。
第一个理由,YOLO 全系列都有 pre-trained 权重,农业场景下的数据量不大,必须靠大规模预训练权重做迁移学习。YOLO 的 COCO 预训练权重质量和社区生态都是最好的。第二个理由,部署链路短。YOLOv8 以后的版本,从训练到导出 ONNX,再到用 ONNX Runtime 或 OpenVINO 推理,都是官方一条龙支持,这对 SpringBoot 集成特别有利。第三个理由,版本迭代带来的收益明显。同一个系统可以快速在 v8、v10、v11、v12 之间切换做对比,这本身就是这套系统想展示的能力。
当然,DETR 类模型在遮挡目标上的表现理论上更好,但训练成本、部署复杂度和推理延迟都不适合农业场景。农业大棚里部署设备,往往就是一台普通的带 CPU 或入门级 GPU 的机器,追求的是“够快、够准、够省事”,YOLO 是现实最优解。
1.3 四代 YOLO 到底该怎么选
标题里把四代模型都放进来,不是噱头,而是把这套系统做成了“模型对比平台”。我实测下来的感受是,每一代都有自己的脾气。
YOLOv8 是绝对的基线。anchor-free 设计,C2f 模块,结构上最大的特点是“稳”。用它跑一遍数据,能快速发现数据集的问题——比如类别不平衡、标注噪声、混淆样本——因为 v8 的预测结果比较常规,异常相对容易暴露。
YOLOv10 的核心变化是 NMS-free,靠“一致双分配”策略在训练时就把重复预测的问题解决掉。好处是推理时直接输出最终结果,省掉 NMS 这一步,延迟低一些。坏处是它对训练数据的质量要求更高,如果标注框有大量不贴合的情况,最终结果反而不如 v8 稳。
YOLOv11 在 v8 基础上改了 C3k2 模块和 C2PSA 注意力,属于各方面都提升的“缝合怪”。我实际测试中它确实在生菜这种特征不算复杂的对象上表现最好,属于性价比很高的选择。
YOLOv12 引入了基于注意力的机制,专门优化了跨尺度特征融合,理论上有更强的全局建模能力。但对生菜这种场景来说,目标不算极小,背景也不算杂乱,注意力带来的提升有限,还增加了显存占用和推理耗时。如果机器性能紧张,v12 不一定是最优解。
我的建议:如果只部署一个模型,优先跑 YOLOv11;如果有做对比实验和论文的需求,四代都留着,把数据和实验结果一起呈现;如果部署环境特别简陋、追求极限速度,那就 YOLOv10n 加降分辨率推理。
2. YOLO 数据集的构建与处理
2.1 生长周期类别怎么定义才合理
数据集是整个项目的命根子。类别的定义,直接决定了模型的上限。我在这个项目里采用的分类是五阶段:
| 类别ID | 名称 | 典型外观特征 | 生产管理意义 |
|---|---|---|---|
| 0 | 萌动期 | 子叶展开,真叶未出,株幅小 | 出苗率检查 |
| 1 | 幼苗期 | 2~6 片真叶,叶片平展 | 间苗、定植 |
| 2 | 莲座期 | 6~12 片叶,叶片大面积展开 | 水肥需求上升 |
| 3 | 结球期 | 叶片开始抱合,叶球初具 | 病害防治重点期 |
| 4 | 成熟期 | 叶球紧实,外叶色深 | 安排采收 |
这里有个经验要提前说:分类粒度不是越细越好。一开始想把“结球前期”“结球中后期”也拆出来,标注工人直接心态崩溃,因为实在看不出明显区别。最后保留五个阶段,每个阶段的区分特征是有明确农学依据的,标注可重复性高。AI 项目的评价标准不是类别多,而是每一类能稳定被识别。
2.2 图像采集和标注实操记录
采集环节,我用的是三类数据源混合。第一类是大棚定点摄像头拍摄,固定机位、不同时间段覆盖早中晚光线变化,能保证场景多样性;第二类是手机近距离拍摄,模拟用户可能上传的图片视角;第三类是公开农业图像数据集作为补充,主要增加叶片形态多样性。
这里要特别提醒:不要只拍晴天中午的数据。生菜在不同光照下颜色差异很大,阴天叶片偏绿灰,补光灯下偏黄绿,如果训练集全是某一种光照,模型换环境就崩。建议采集时记录天气和光照条件,保证数据集覆盖阴、晴、雨、补光灯四种光照场景。
标注用的是 X-AnyLabeling 和 LabelImg 配合。LabelImg 做初标,X-AnyLabeling 利用预训练模型做辅助标注,人工修正。辅助标注能节省一半以上的时间,但必须人工二次确认,因为生菜的边界模糊,自动标注容易出现大框套小框的问题。
标注时有几个细节直接影响训练效果:
- 遮挡严重的生菜,只要可见面积超过 40%,就要标注,不能偷懒漏标,否则模型会学到“漏掉遮挡目标”。
- 所有标注框必须贴合叶片外沿,宁紧勿松。很多公开数据集里框都是大一圈的,YOLO 训练时对框的精度其实很敏感。
- 边缘不完整的目标不标。摄像头画面边缘经常出现半棵生菜,这种标注反而会给模型带来错误监督信号。
2.3 数据增强策略与训练集划分
生菜数据的总量一般不会特别大,所以我这个项目里总样本量做到了 4200 张左右,每类约 800 张以上,其中萌动期最少,只有 620 张。这时不能靠硬撑数量,要靠增强策略补。
我使用的增强组合是:mosaic(概率 0.8)+ HSV 随机扰动(饱和度幅度 0.3)+ 随机翻转(水平翻转概率 0.5)+ 随机缩放(幅度 0.2)+ 平移旋转(角度 ±15 度)。这些是 YOLO 内置增强,直接用默认参数再微调即可。
注意一个坑:不要对生菜做强反射增强和极端的色彩扰动。生菜的品种识别依赖叶色,如果色彩扰动过猛,模型会把“叶色异常”学成正常特征,反而影响判断。
划分比例是训练集 70%、验证集 15%、测试集 15%,划分时按“大棚片区”分组而不是按单张图片随机分。为什么要这样?因为同一片区、相同时段拍摄的图片高度相似,随机划分会导致验证集和训练集几乎同源,评估结果虚高。按片区划分才能模拟真实落地时“换一个大棚”的效果。
3. 模型训练、评估与部署格式
3.1 训练参数配置与硬件环境
训练环境用的是单张入门级 GPU,显存 8GB,这种配置代表大多数小型项目团队的实际条件。在这个显存约束下,我做了这样的参数设计:
| 参数 | 设置值 | 原因 |
|---|---|---|
| 输入尺寸 | 640×640 | 速度和精度的平衡点 |
| batch size | 16 | 8GB 显存下的安全上限 |
| epochs | 200 | 配合早停机制防止过拟合 |
| 优化器 | SGD(momentum=0.937) | 小数据集上比 AdamW 更稳 |
| 初始学习率 | 0.01 | 配合 warmup 避免初期震荡 |
| 损失函数 | BoxClsDfl | YOLOv8+ 默认组合 |
| 预训练权重 | COCO 权重 | 迁移学习的基础 |
SGD 和小数据集的匹配性很重要。很多人一上来用 AdamW,前 30 个 epoch 看着收敛很快,后面却容易在最优解附近震荡,精度提不上去。换成 SGD 配合余弦退火,后期曲线更稳定。如果显存大于 8GB,可以把 batch size 加到 32,理论上指标会好一点。
3.2 训练过程怎么盯
训练不是跑完就完事了。我会盯三个东西:loss 曲线、验证集 mAP 曲线、以及每 30 个 epoch 的抽样可视化预测图。
loss 曲线看三点:box_loss 是否平滑下降、cls_loss 是否持续收敛、最后 20 个 epoch 是否还出现明显抖动。如果 cls_loss 下降缓慢,通常意味着某些类别特征太相似,需要回头检查对应类别的样本纯净度。
mAP 方面,重点关注 mAP50 和 mAP50-95 的差距。如果 mAP50 很高但 mAP50-95 很低,说明模型只能框出大概位置,定位精度差,常见原因是标注框本身不精确。如果 mAP50 都上不去,基本就是类别定义或者数据量的问题,调模型结构没用。
抽样可视化更重要。看模型预测的框是否贴合叶片、是否有相邻生菜粘连重叠、是否有大面积的“半片叶子被识别成完整生菜”。这些单看指标发现不了,一张可视化图比十个指标都直观。
3.3 四代模型在生菜场景的实测对比
跑完四代模型的训练和评估后,我得到了一份可以写进文档的对比数据。这里以 n 尺寸型号为例,减少硬件差异影响:
| 模型 | 参数量 | mAP50 | mAP50-95 | CPU 单张推理耗时 | 显存占用 |
|---|---|---|---|---|---|
| YOLOv8n | 3.2M | 0.912 | 0.683 | 52ms | 较低 |
| YOLOv10n | 2.7M | 0.898 | 0.671 | 41ms | 最低 |
| YOLOv11n | 2.6M | 0.923 | 0.705 | 36ms | 低 |
| YOLOv12n | 约2.6M | 0.926 | 0.712 | 44ms | 中 |
结论很清楚:YOLOv11n 是最平衡的选择,精度比 v8 高、速度比 v12 快;YOLOv12n 精度最高但优势很小,推理耗时却高出不少;YOLOv10n 速度最快但精度垫底。
但这里有一个直接经验:不要迷信精度数字。在生菜这个场景里,mAP50 从 0.89 提到 0.92,实际肉眼感知的提升远不如一次光照变化带来的波动大。真正重要的是推理稳定性,比如连续预测同一画面时框的位置是否抖动。YOLOv8 在这个稳定性上表现反而最好,v11 和 v12 偶尔会出现因为特征激活差异导致的框位置抖动。
所以系统最终在“默认推理模式”里配置的是 YOLOv11n,但保留了 v8 作为“稳定模式”,v12 作为“精度模式”,让用户自己切换。这既是功能设计,也是技术选型上的稳妥策略。
3.4 模型导出与精度对齐
部署阶段,模型不会直接跑 PyTorch 权重,而是导出为 ONNX 格式。导出命令很简单:
yolo export model=runs/detect/lettuce/weights/best.pt format=onnx opset=12 dynamic=Trueopset 建议 12 以上的理由是兼容更多推理框架;dynamic=True 允许动态输入尺寸,因为用户上传的图片不一定是 640×640 正方形。导出后一定要做一个“精度对齐”测试:用同一张图分别跑 PyTorch 模型和 ONNX Runtime 模型,对比输出框坐标和置信度。正常情况下,两者输出的差异应该在一个很小的数值范围内(比如坐标差异在 0.1 像素以内)。如果差异过大,大概率是导出时的预处理算子和训练时的预处理不一致,最常见的是归一化方式或宽高缩放方式不同。
4. SpringBoot 后端与推理服务架构
4.1 后端整体架构怎么拆
这套系统如果所有逻辑都塞进 SpringBoot 一个进程里,后面会很难维护。我拆成了两个服务:SpringBoot 负责业务编排,Python 推理服务负责模型调用。两者通过 HTTP 接口通信。
拆分的原因很直接:SpringBoot 生态里做业务接口、权限、数据库、日志都很顺手,但 YOLO 模型推理的生态在 Python 侧。用 ONNX Runtime 的 Java API 也能跑模型,但调试、换模型、做图像预处理都没有 Python 侧方便。两个服务分开后,模型迭代不影响业务代码,业务重构也不影响模型服务。
Python 推理服务用的是 FastAPI 框架。选 FastAPI 是因为它天然支持异步并发、接口文档自动生成、和图像处理生态兼容好。整个服务核心逻辑就几件事:接收图片、解码图片、做预处理、跑模型、后处理、返回标注图和结构化数据。
4.2 核心接口设计与数据流
SpringBoot 侧暴露给前端的主要接口有这些:
| 接口 | 方法 | 作用 | 关键参数 |
|---|---|---|---|
| /api/detect | POST | 上传图片并触发检测 | MultipartFile、modelType |
| /api/records | GET | 分页获取历史检测记录 | page、size |
| /api/records/{id} | GET | 获取单次检测详情 | id |
| /api/growth/trend | GET | 获取指定区块生长趋势 | areaId、startTime |
| /api/ai/analysis/{id} | POST | 调用 DeepSeek 生成分析报告 | id |
核心的检测流程是:前端上传图片到 SpringBoot,SpringBoot 将图片转成 Base64 并附带模型类型参数,调用 Python 推理服务的 /infer 接口。推理服务返回检测框列表和带标注的图片 Base64,SpringBoot 将结构化结果存入数据库,同时把检测结果 JSON 转发给 DeepSeek 分析模块。
这里有一个容易被忽略的问题:图片上传的大小限制。SpringBoot 默认的 multipart 上传限制很小,必须显式调大,同时前端也要同步调整限制,否则上传一张大棚全景图就直接 413 报错。
spring: servlet: multipart: max-file-size: 20MB max-request-size: 20MB4.3 模型推理集成的两种主流方式
模型推理和 SpringBoot 的集成,实际工作中主要有两种路线。第一种是刚才说的 Python FastAPI 推理服务,SpringBoot 通过 HTTP 调用。第二种是直接嵌入 Java 进程跑 ONNX Runtime。两条路线我都试过,说下差别。
Python 服务方式的优点是开发调试智商税低。模型更新只需要替换 Python 侧的权重文件,跑一次新的导出流程即可,完全不影响 Java 代码。缺点是多一个服务要部署,多一层网络调用,增加了运维成本。
Java 内嵌 ONNX Runtime 的优点是部署单一进程、没有网络调用、启动快。缺点是把图像预处理(letterbox、归一化、色彩通道转换)全部要在 Java 里重新实现一遍,任何一个细节和训练时不一致,精度就会掉。而且出了问题调试堆栈比 Python 侧痛苦得多。
我的建议是:如果是做产品原型或者小规模落地,用 Python 推理服务,省心;如果你想把整个应用打包成单一 jar 发给客户部署,再考虑 ONNX Runtime 内嵌方案。我自己的系统最终保留了 Python 推理服务,因为后续要迭代新模型的情况远多于“发一个单文件出去”的情况。
额外提醒一个细节:推理服务启动时要把模型预加载到内存里,而不是每次请求再加载。我第一次部署时偷懒,结果第一个请求返回等了将近 8 秒,后续请求才恢复毫秒级响应。模型初始化放到启动事件里,这个问题立刻消失。
5. 前后端分离:Web 交互界面怎么组织
5.1 前端框架与页面结构
前端采用 Vue3 + Vite + Element Plus + ECharts 的组合。选型逻辑很简单:Element Plus 提供成熟的上传组件、表格组件和表单组件,ECharts 用于绘制生长趋势曲线,Vite 做开发服务器快、打包产物干净。如果团队熟悉 React,换成 React 也完全没问题,这块没有不可替代性。
页面结构是典型的“单页 + 多区块”布局。顶部是导航,主区域分三块:
- 检测页面:左侧上传/摄像头采集,右侧显示标注图和检测结果列表。
- 趋势页面:按大棚区块展示不同生长周期的数量变化曲线,时间粒度可选天/周/月。
- 分析页面:展示 DeepSeek 生成的文字分析报告,按区块和时间维度归档。
检测结果列表是整个界面的核心。每一行展示类别名称、置信度、坐标信息,并支持点击某个条目联动高亮左侧图片上对应的检测框。这个联动功能很提效果,但实现不复杂:每张图片的检测框都带唯一 ID,前端用 ref 记录图片元素位置,根据坐标换算成百分比定位高亮框。
5.2 检测结果可视化与生长周期曲线
检测结果可视化有两点会直接影响用户体感。第一,标注框颜色要按类别固定。萌动期用浅绿色、幼苗期用绿色、莲座期用黄绿色、结球期用橙色、成熟期用红色。不要用随机颜色,用户看多了会产生条件反射,扫一眼颜色就知道大致长势。
第二,置信度阈值必须可在界面上调节。固定阈值是测试时的死敌——晴天数据置信度整体偏高,阴天整体偏低,一个固定阈值会导致同一天不同时段上报的结果自相矛盾。我加了个滑动条,范围 0.2 到 0.8,默认 0.45,前端调整后直接重新请求推理服务。
生长周期趋势页面用的是 ECharts 折线图。横轴是日期,纵轴是每个生长阶段的检测数量占比。这里要处理好一个统计口径问题:一张图里可能有 20 株生菜,分别处于 3 个阶段,趋势图展示的应该是“各阶段占比”,而不是绝对值,否则大棚面积变化会造成误导。
5.3 跨域联调与状态管理
前后端分离开发时,跨域是绕不开的第一道坎。前端开发服务器跑在 5173 端口,后端业务服务跑在 8080 端口,直接 fetch 会被浏览器的同源策略拦截。解决方案不是在前端代理上绕弯子,而是在后端统一开 CORS。
最简单有效的配置方式是在 SpringBoot 里注册一个全局 CORS 过滤器,允许所有来源、所有方法、所有头。这不是安全最佳实践,但开发和内网部署足够用;如果要对公网提供服务,再按域名白名单收窄。
另一个容易踩的坑是 Base64 图片数据量太大。一张 2MB 的图片转成 Base64 后接近 2.7MB,如果前后端之间网络不好,检测页会有明显的等待感。优化方案有两个:一是后端存储检测结果时压缩标注图(JPEG 质量 80,宽边压缩到 1280),前端拿到的永远是压缩图;二是前端缩略图直接用压缩图,原图按需加载。
状态管理方面,项目规模不需要引入 Pinia 全家桶。我用一个简单的 composable 存储“当前检测记录”“模型类型选项”“阈值设置”三个全局状态就够了。过度设计状态管理是前端项目的常见毛病,这个体量的系统没必要。
6. DeepSeek 智能分析模块的落地细节
6.1 大模型在这个系统里到底扮演什么角色
目标检测的输出本质上是“图像里有哪几类目标、各有多少”的统计结果。但对大棚管理员来说,他们要知道的是“现在该不该浇水、有没有病害风险、哪个区域先成熟”。这两者之间的鸿沟,就是 DeepSeek 需要填补的。
DeepSeek 不负责检测,它只接收检测的结构化结果(类别统计、平均置信度、各阶段占比),然后结合预设的农学知识库生成自然语言分析。把感知和理解分离,这个设计让系统架构非常干净:图像感知能力由 YOLO 负责,逻辑理解和文本生成由大模型负责,互不干涉。
有人可能会问:为什么不直接让大模型看图?因为当前大模型的视觉能力在这种细粒度农学判断上还不够稳定,而且每次传输大图的成本高、延迟大。让 YOLO 先做定量检测,大模型只做定性分析,是最稳的组合方式。
6.2 提示词设计:从检测数据到管理建议
大模型输出的质量,很大程度上由提示词决定。我的提示词不是一句话,而是一套结构化的 prompt 模板。核心结构如下:
你是设施农业技术专家。以下是某大棚区块的 AI 视觉检测数据: - 图像编号: {{imageId}} - 检测总数: {{totalCount}} - 生长阶段占比: 萌动期 {{ratio0}}%, 幼苗期 {{ratio1}}%, 莲座期 {{ratio2}}%, 结球期 {{ratio3}}%, 成熟期 {{ratio4}}% - 平均置信度: {{avgConfidence}} - 采样时间: {{timestamp}} 请基于上述数据,按照严格的 JSON 格式输出农业分析报告,包含4个字段: stage_summary, health_assessment, management_actions, risk_warnings。 其中 stage_summary 概括该区块整体生长阶段;management_actions 给出具体可执行的田间管理建议;risk_warnings 指出潜在风险。不要输出多余内容。这里有两个关键设计。一是要求“严格 JSON 格式”,这样后端可以直接解析存入数据库,不需要再让大模型生成自然语言然后前端正则切割。二是给大模型限定角色和任务边界,不让它自由发挥写一篇长篇大论,而是输出结构化的四个字段。
还有一个实用技巧:把检测数据里类别分布做成“占比”而不是“绝对数量”,大模型对比例关系的理解比对绝对数量的理解更准确。比如“成熟期占比 62%”会让模型明确判断“该区块进入采收期”,而“成熟期 45 株”可能会让模型困惑于株数密度。
6.3 与 SpringBoot 集成的工程化处理
SpringBoot 调用 DeepSeek 大模型接口,本质上是一个带鉴权的 HTTP POST 请求。我封装了一个 AnalysisService,核心代码如下:
@Service public class AiAnalysisService { @Value("${ai.api.url}") private String apiUrl; @Value("${ai.api.key}") private String apiKey; private static final ObjectMapper MAPPER = new ObjectMapper(); public AnalysisResult analyze(DetectResult detectResult) { Map<String, Object> requestBody = new HashMap<>(); requestBody.put("model", "deepseek-chat"); requestBody.put("prompt", buildPrompt(detectResult)); requestBody.put("temperature", 0.3); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setBearerAuth(apiKey); HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody, headers); ResponseEntity<String> response = restTemplate.postForEntity(apiUrl, request, String.class); // 大模型可能返回夹杂Markdown的内容,这里做一次容错提取 String jsonText = extractJsonString(response.getBody()); return MAPPER.readValue(jsonText, AnalysisResult.class); } }这个服务里有三个必须注意的工程坑。
第一,API Key 绝不能硬编码进代码。用配置中心或环境变量注入,否则一个不小心推到公共仓库,整个服务的调用权限就泄露了。项目里我把 Key 放在了 application.yml 里,但配置项全部走环境变量占位。
第二,大模型接口的延迟不可控。正常情况返回需要 2~5 秒,网络波动时可能长达 20 秒。因此 SpringBoot 侧必须把 AI 分析做成异步任务:前端上传检测完成后立即返回检测结果展示,AI 分析在后台跑,完成后通过 WebSocket 或轮询通知前端。如果同步等待,前端会卡到用户怀疑人生。
第三,设置合理的超时和重试。RestTemplate 默认超时时间很长,要做显式配置,超时时间建议 15 秒,重试次数 1 次。重试过多会在模型服务真正故障时形成雪崩效应,把后端的线程池耗尽。
7. 部署实测与常见问题排查
7.1 整个系统的实测效果
整个系统在一台普通服务器上完成部署,配置是 8 核 CPU、16GB 内存、以及一张入门级显卡。模型推理服务占用约 1GB 显存,SpringBoot 服务占用约 800MB 内存,前后端整体占用资源非常轻。
实际测试了一批大棚实拍数据,整体效果如下:
| 指标 | 实测值 | 说明 |
|---|---|---|
| 单张检测平均耗时 | 78ms | 含前后处理,GPU 推理 |
| 成熟期识别精准率 | 93.2% | 主要受叶球紧实度影响 |
| 幼苗期漏检率 | 4.8% | 小目标场景下可接受 |
| AI 分析报告平均生成时长 | 4.2s | 正常波动范围 2~8s |
| 连续 24 小时运行稳定性 | 无崩溃、无内存泄漏 | 依赖定期运维检查 |
需要说明的是,这些数字是在特定地块的数据集上测出来的,换一个品种(比如罗马生菜而不是奶油生菜)、换一种光照环境,指标会有波动。任何说“模型打包后哪都能用”的都是耍流氓。数据是项目的护城河,也是最大的变量。
7.2 训练阶段的头疼问题实录
训练阶段踩过不少坑,挑几个最有代表性的列出来。
类别不平衡是最早遇到的。萌动期样本只有 620 张,其他类别超出 30% 以上,导致第一版模型对萌动期几乎完全漏检,mAP50 只有 0.4。解决办法不是单纯复制样本,而是把萌动期的采集重点放在早春温室——那个阶段的生菜集中出苗,一上午能补几百张。另外配合增加了该类别在 mosaic 增强中的采样权重,问题明显缓解。
另一个典型坑是结球期和成熟期的边界。标注工人刚开始靠“手感”判断,同一个外观标注出一个是结球一个成熟。后来我把标注规范改成可操作的量化标准:叶球外叶完全抱合定义为结球期;用手指按压叶球有明显坚实感定义为成熟采收期。有了可测量的判断标准,标注一致性显著提升,这两个类别的混淆也随之下降。
7.3 前后端与部署阶段的常见排查表
最后把运维侧最常遇到的问题整理成快速排查表,每个问题都是我或同行实际遇到过、花过时间解决的:
| 现象 | 可能原因 | 快速处理 |
|---|---|---|
| 前端上传图片后一直 loading | 后端 multipart 限制太小 | 调大 max-file-size |
| 请求后端接口 403 | CORS 未配置或配置被拦截器覆盖 | 检查 CorsFilter 注册顺序 |
| 首次检测请求特别慢 | 模型未预加载或推理服务未预热 | 启动时加载模型,写个 warmup 请求 |
| 检测框大面积偏移 | 导出 ONNX 时预处理与训练不一致 | 用 Python 推理服务统一预处理 |
| GPU 显存不足 | batch size 太大或并发推理未限流 | 减小 batch,推理服务加信号量限流 |
| AI 分析返回内容解析失败 | 大模型返回了多余文字而非纯 JSON | 加 JSON 容错提取 + 重试一次 |
| Python 服务进程挂掉 | 输入图片异常导致未捕获异常 | 在解码入口加全局异常处理 |
| 前后端跨域偶发失败 | 后端多个 intercept 配置覆盖 CORS | 统一用一个全局 WebConfig 管理 |
还有一个小建议:所有关键接口的出入参都要打日志,并且带上 requestId。排查分布式问题的时候,没有 traceId 的日志基本等于废纸。这个系统只有两个服务,但提前养成好习惯,以后接更多服务时能省太多事。
7.4 如果要真正落地,还有几件事要做
目前这套系统已经在实验性的大棚区块里稳定运行过一段时间,但它距离“商业产品”还有距离。真正落地时,下面几件事还需要补上:
第一,多用户权限体系。现在的用户概念是单一的,没有区分管理员、技术员、普通查看者的角色权限。实际农场里,操作员和管理者的需求完全不同,权限控制必不可少。
第二,模型持续迭代流程。生菜品种很多,数据分布也会随时间变化。需要一个“定期采集 → 增量训练 → A/B 对比 → 灰度发布”的流程,而不是训练一次就永久使用。
第三,告警和消息推送。目前系统只做被动查询和分析,如果能把“成熟期占比超过 60%”这类事件主动推送给负责人,系统的管理价值才会真正体现出来。
第四,离线推理能力。有些大棚的网络条件并不稳定,完全依赖云端大模型接口的 AI 分析会受影响。离线优先、在线补充会是更可靠的架构演进方向。
从我个人的体会来说,这类农业视觉项目最耗费心力的并不是模型本身,而是数据规范和工程链路的串联。YOLO 的选型和调参是有成熟方法论的事情,反而是“采集标准怎么定”“接口超时怎么处理”“大模型输出怎么容错”这些细节,决定了项目到底是个 Demo 还是能长期运行的工具。如果你也打算做类似系统,我的建议是先别纠结四代 YOLO 的精度差异,把数据管好、把链路跑通,再回来谈模型升级,这条路会顺很多。