
这几年做视觉项目我的工作流一直是典型的 YOLO 单帧路线标注一批图片训练一个模型看看 mAP再把权重部署到设备上。这套流程在图片分类、离线质检、照片结构化场景里完全够用。直到一个智慧园区项目找上门——十几路监控摄像头要实时分析每路都得在秒级内发现异常并推送告警我才意识到纯 YOLO 那套“图片进、坐标出”的心智模型根本撑不起实时视频 AI。后来我把整个链路重新梳理了一遍基于媒体处理框架封装了 SmartMediaKit才把集成思路跑通。这篇就把我在这个过程中的技术思考和踩坑记录整理出来。1. 单帧检测再准也撑不起一路实时视频1.1 从“识别一张图”到“盯住一段流”问题完全变了在 YOLO 的常规目标检测流程里我们要么从文件夹读一张图要么从摄像头抓一帧然后送进模型得到检测框。整个过程是“请求-响应”式的有图片就有结果没有图就等着。但实时视频 AI 完全是另一回事。视频是连续不断的帧流每一帧之间有时间先后、有运动关系。人物在画面里行走可能在某一帧被遮挡下一帧又出现车辆在转弯时形变严重连续几帧都框不准。这时候如果只做单帧检测你会看到检测框在视频里剧烈抖动、闪烁一会儿有人一会儿没人告警系统濒临崩溃。更关键的是评价体系变了。单帧检测看的是 mAP、Precision、Recall而实时视频 AI 看的是端到端延迟多少毫秒、每秒能处理多少帧、连续运行 7 天会不会内存暴涨、告警有没有重复推送。这些指标和 mAP 一样重要甚至更重要。一个 mAP 很高的模型如果推理延迟 200ms在 25fps 的视频流里就只能在抽帧策略下勉强工作体验会非常差。1.2 延迟预算实时系统里的那道数学题我做系统设计时习惯先把延迟预算算清楚。一个实时视频 AI 链路的端到端时延大致包括摄像头采集与网络传输时延拉流和解码时延帧预处理时延缩放、归一化、通道转换模型推理时延后处理时延NMS、坐标映射结果回传与告警推送时延假设一路 1080p 25fps 的摄像头我们需要在不丢关键事件的前提下做到每帧或每两帧分析一次。如果模型在 GPU 上单帧推理要 20ms看起来很快但注意这里有一个并发放大效应如果是 15 路摄像头每路都要跑到 12.5fps那每秒就需要推理 15 × 12.5 187.5 次单次 20ms 就是 3.75 秒的计算量也就是说 GPU 必须在一秒内干完 3.75 秒的活需要 3.75 倍于实时推理的算力。这还没算解码。很多项目只盯推理时间忽略了 H.264 解码本身要占 CPU/硬件解码器资源。等 GPU 推理优化好了CPU 解码又成了瓶颈。所以我后来做设计时一定把“解码-预处理-推理-后处理”当成一个整体去算预算而不是只看模型耗时。1.3 为什么不能把视频流简单拆成图片一帧帧跑有些人会说那我用 OpenCV 循环读帧每帧调一次模型不就行了吗实测下来这条路有两个大问题。第一OpenCV 的 VideoCapture 在 RTSP 弱网环境下非常不稳定网络抖动时丢帧、花屏、阻塞是家常便饭。第二逐帧串行调用模型时解码线程、推理线程、网络回传线程互相阻塞某一帧卡住会导致后面所有帧排队延迟像滚雪球一样越滚越大最终从“实时”退化成“点播”。真正的实时视频 AI 必须用流水线架构拉流和解码在一个线程推理在另一个线程结果回传在第三个线程线程之间用有界队列解耦。这样即使某一帧推理慢了也只是丢一帧不会影响整条链路的实时性。这也是我后来做 SmartMediaKit 的最初动机——先解决视频接入和调度问题再在上面挂 YOLO 模型。2. SmartMediaKit 在整条链路里到底解决什么问题2.1 定位媒体接入与推理调度的骨架层先说明一下SmartMediaKit 是我们内部封装的一套媒体处理与推理调度框架定位是在 YOLO 等模型和原始视频流之间补上“视频工程”这一层缺失的能力。它不负责训练模型也不改变模型结构而是把视频接入、解码、抽帧、推理调度、结果回传这些脏活累活统一封装好让上层业务只关心“我拿到了一帧图像和它的时间戳我要检测什么”。很多做算法的朋友有个误区觉得自己用的是 YOLO环境配置好了、模型能跑通了项目就完成了一大半。实际上模型训练和推理只是实时视频 AI 里的一环。一个完整的系统还需要处理协议接入RTSP/GB28181、硬件解码、帧率控制、多路并发、异常恢复、结果去重、数据落盘。这些如果每个项目都从零写一遍不仅浪费时间而且稳定性很难保证。SmartMediaKit 当时的设计目标很明确把视频流抽象成统一的“帧流”把 YOLO 推理封装成可插拔的处理器让新增一路摄像头、更换一个模型都只需要改配置而不是改代码。2.2 与纯 YOLO 方案的边界划分YOLO 擅长的事是从一张图像里输出目标的类别和边框坐标它的职责边界在“感知”这一层。SmartMediaKit 的职责边界则覆盖感知之外的所有工程环节输入侧RTSP/RTMP/GB28181 流接入H.264/H.265 解码调度侧帧率控制、抽帧策略、多路视频共享推理资源输出侧检测结果格式化为标准 JSON通过 MQTT/WebSocket/HTTP 推送运维侧断流自动重连、队列积压告警、运行时指标上报这个边界划分很重要。它让团队里写算法的同学只需要面对一个回调函数on_frame(frame, timestamp_ms)在里面调用模型得到检测结果然后把结果交给框架回传。至于这个画面的流是从海康摄像头来的还是大华录像机来的解码用的硬解还是软解回传走的是 MQTT 还是 WebSocket算法同学根本不用关心。2.3 选型细节为什么接入层最终选了 FFmpeg 而不是从零写视频接入层我几乎没有犹豫就选了 FFmpeg。原因很直接它支持 RTSP、RTMP、HTTP-FLV、GB28181通过插件或扩展等主流协议对 H.264/H.265 的软解和硬解支持完善而且社区生态成熟网上的踩坑资料最多。使用 FFmpeg 时有个容易忽视的细节解码器输出的帧可能是 YUV420P 格式模型输入通常要求 RGB 或 BGR这个颜色空间转换在 CPU 上非常耗时。1080p 的帧从 YUV 转到 BGR用 sws_scale 每次要花几毫秒到十几毫秒。我后来把颜色转换放在 GPU 上用 CUDA 核函数做或者干脆在预处理时用 GPU 的缩放和转换库CPU 占用瞬间降下来。另外 FFmpeg 的硬件解码值得认真搞。NVIDIA 平台上用 cuvid/nvdec 解码 H.264解码时延和 CPU 占用率都会大幅下降。RK3588 平台上用 MPP 硬件解码一路 1080p25 的解码 CPU 占用不到 5%。很多项目推理已经优化得很好了结果解码把 CPU 吃满这是非常容易翻车的点。2.4 备选方案 GStreamer 为什么被我淘汰了可能有人问为什么不用 GStreamer它的 pipeline 理念很优雅插件机制也比 FFmpeg 更“正”。我团队早期原型确实用过 GStreamer但维护成本有点高。它的插件图pipeline graph调试起来不够直观一个插件报错会把整个链路搞挂动态修改 pipeline比如切换分辨率、动态增减摄像头写起来非常繁琐而且团队成员对它的熟悉程度远不如 FFmpeg新人上手慢。FFmpeg 的优点是 API 简单直接av_read_frame、avcodec_send_packet、avcodec_receive_frame几行代码就能跑通一条流。做工程不是做艺术品团队的长期维护效率才是第一位的。所以我最后保留了 FFmpeg 做接入层只在需要复杂媒体编排的场景下才引入 GStreamer。3. 从 RTSP 拉流到检测结果回传一条链路的完整落地3.1 整体流水线五级有界队列实时视频 AI 的代码架构我最终采用了“五级流水线”结构拉流线程负责连接 RTSP 服务器读取压缩码流包解码线程把压缩包解码为原始帧预处理线程做缩放、格式转换、归一化生成模型输入张量推理线程把张量送给 YOLO 模型执行推理回传线程解析检测结果并推送到告警/业务平台线程之间用有界队列连接队列满了就丢最旧的帧保证延迟可控。核心伪代码如下// 伪代码流水线线程模型 void pipeline_run() { Thread t1 thread(pull_stream); // 拉流 - packet_queue Thread t2 thread(decode_packet); // 解码 - frame_queue Thread t3 thread(preprocess); // 预处理 - tensor_queue Thread t4 thread(infer_yolo); // 推理 - result_queue Thread t5 thread(publish_result);// 回传 - MQTT/HTTP }每条流水线只处理一路视频。多路视频时每路都有独立的拉流和解码线程但可以共享同一个推理线程池让 GPU 做动态合批这个后面细说。3.2 拉流与解码阶段B 帧乱序、时间戳与断流重连真正写拉流代码时第一个坑就是 B 帧乱序。H.264 码流里的帧并不是按显示顺序排列的解码器输出的帧带 DTS解码时间戳和 PTS显示时间戳。如果你直接把解码出来的帧按顺序送进模型会发现视频是“跳”的检测结果在时间轴上错位。解决办法是维护一个按 PTS 排序的小缓冲区把解码器输出的帧按显示顺序排好再交给预处理。缓冲区不能太大否则会增加延迟我一般控制在 5 到 10 帧既能排序又不会引入过多时延。断流重连也是必踩的坑。摄像头重启、网络抖动、RTSP 服务器主动断开都会让拉流线程退出。我的策略是采用指数退避重连第一次失败等 1 秒第二次等 2 秒第三次等 4 秒最多不超过 30 秒。同时维护一个“最近帧时间戳”监控如果超过 5 秒没有新帧到达就主动断开连接并触发重连而不是傻等 FFmpeg 的内部超时。3.3 预处理letterbox 不是选项而是刚需YOLO 模型输入要求固定尺寸通常是 640×640而摄像头画面是 1920×1080 或 2560×1440。直接 resize 会破坏图像宽高比导致目标变形检测精度明显下降。正确做法是 letterbox 缩放保持宽高比缩放到短边等于输入尺寸然后对长边两侧填充灰色像素。这一步很多人嫌麻烦用一句 cv2.resize 糊弄过去结果模型精度掉了几个点还找不到原因。预处理还有一个细节YOLO 系列模型在训练时通常使用 RGB 顺序而 FFmpeg/OpenCV 解码出来是 BGR。如果直接用 OpenCV 读图再推理很多人习惯了 BGR 顺序忘转换模型输出会非常奇怪。我在 SmartMediaKit 里统一规定模型输入端固定为 RGB float32NCHW 布局数值归一化到 0~1。这样无论是 YOLOv5、YOLOv8 还是以后的模型只要按这个标准接入就不会出错。预处理阶段的优化空间也很大。如果每帧都在 CPU 上做 letterbox 和颜色转换1080p 下可能要 5-10ms把这些操作放到 GPU 上通过 CUDA 核函数或者 NPP 库实现单帧可以压到 1ms 以内。3.4 后处理与坐标映射YOLO 边框坐标如何回到原图模型输出的原始张量是一堆预测量要经过完整的后处理才能变成检测框。在这件事上我走了一遍标准的 YOLO 后处理流程置信度过滤把低于阈值的候选框直接丢掉类别置信度筛选每个框取置信度最高的类别NMS非极大值抑制对重叠度高的框进行抑制保留最优结果坐标解码把模型输出还原到输入图像尺寸NMS 的算法本身不复杂但怎么高效实现很关键。Python 里纯 for 循环写 NMS640×640 输入下可能就要几十毫秒我用的是按类别分组的向量化 NMS一次处理全部分类耗时能控制在 1ms 左右。关键的一步是坐标映射。模型输出的坐标是在 letterbox 后的 640×640 图上的要映射回原始 1920×1080 画面需要反向计算缩放比例和填充偏移量。核心公式如下scale min(input_width / orig_width, input_height / orig_height) offset_x (input_width - orig_width * scale) / 2 offset_y (input_height - orig_height * scale) / 2 orig_x (box_x - offset_x) / scale orig_y (box_y - offset_y) / scale这条映射链出错过很多次。有时候是忘了减 offset导致框偏到右上角有时候是宽高比不同的摄像头用了同一个缩放系数框全都跑偏。后来我把坐标映射封装成独立模块并针对不同分辨率写死单元测试才彻底解决。3.5 结果回传MQTT、WebSocket、HTTP 怎么选检测结果要送到业务平台常见的选择有三个通道优点缺点适用场景HTTP POST简单直接通用性好每次请求都有开销大批量推送时延迟高低频告警、事件类推送MQTT轻量级发布订阅模型带宽占用低需要额外部署 Broker调试相对麻烦多路摄像头都订阅同一主题、告警推送WebSocket双向通信实时性高头部开销小需要维护长连接状态断线重连逻辑要自己写需要在网页/大屏上实时展示画面和检测框我实际项目的选型是“双轨制”告警事件走 MQTT因为下游有多个订阅方短信服务、大屏、手机 App发布订阅模型最省事视频画面叠加实时检测结果走 WebSocket因为它天然支持持续推送二进制的 JPEG 帧。HTTP 只用于手动测试和调试接口。回传时还有一个必须处理的点告警去重。单路摄像头 25fps一个异常事件可能连续持续好几秒甚至十几秒如果每帧都推送告警下游会被噪声淹没。我的方案是引入“事件冷却窗口”同一摄像头、同一类别、且空间上重叠的检测框在 5 秒内只推送一次告警但持续更新该事件的“最近确认时间”。这样既不会漏报也不会刷屏。4. 边缘设备上的实战经验RK3588 与 NVIDIA 平台的部署差异4.1 为什么说边缘部署才是实时视频 AI 的重头戏很多算法原型跑在数据中心的大 GPU 上但到了项目落地现场视频流往往分散在各处的摄像头里。如果把所有视频都拉回云端集中推理一方面带宽成本极高十几路 1080p 的视频回传会占满上行链路另一方面延迟不可控跨地域的网络抖动会让实时性名存实亡。越来越多项目要求在边缘设备上直接完成推理在园区机房放一台边缘服务器或者直接在摄像头旁边部署一台边缘盒子。这类设备的算力远不如数据中心 GPU但对能效比、稳定性、体积都有要求部署优化也就成了关键问题。4.2 RK3588 上的完整部署链PyTorch → ONNX → RKNNRK3588 是瑞芯微的一款边缘 AI 芯片带 6 TOPS 的 NPU非常适合部署 YOLO 系模型。我在一个消防隐患识别项目中把 YOLOv5s 完整部署到了 RK3588 上整个流程走的是 PyTorch → ONNX → RKNN。第一步先从 PyTorch 导出 ONNX这一步相对简单只需要固定输入尺寸、关闭动态维度。我用的是 640×640 静态尺寸# 导出 ONNX示例 python export.py --weights yolov5s.pt --include onnx --imgsz 640 --batch-size 1第二步用 rknn-toolkit2 把 ONNX 转成 RKNN 格式。这里最容易出问题的是算子兼容性。YOLOv5 的 Detect 头里有大尺寸的 concat 和 sigmoid 操作在 NPU 上不一定都有优化实现。我的经验是导 ONNX 时可以把后处理从模型里拆掉只保留 backbone neck head 的特征输出把 NMS 留在 CPU 上跑。这样模型更小NPU 加载更快部署也稳定。第三步在板端用 RKNN Runtime 加载模型推理。代码核心大概是# RKNN 推理核心伪代码 ret rknn.load_rknn(yolov5s.rknn) ret rknn.init_runtime() outputs rknn.inference(inputs[img_tensor], data_formatnhwc)4.3 三平台推理耗时实测对比我把自己常用的几个部署目标做过一组实测模型都是 YOLOv5s输入 640×640单帧 batch1平台推理耗时量化类型备注NVIDIA Jetson Orin Nano约 12msFP16用 TensorRT 加速RK3588 NPU约 28msINT8需要做量化校准PC 端 GTX 1660约 8msFP16桌面级 GPU注意 RK3588 上 28ms 是 INT8 量化后的结果。如果不做量化FP16 直接在 NPU 上跑往往比这个慢很多。INT8 量化是有代价的mAP 可能会下降 1~3 个点需要结合业务容错度来决定。另一个经验是RK3588 虽然宣传 6 TOPS但 NPU 的利用率很依赖模型结构和算子融合。YOLOv5s 这种结构对 NPU 还算友好但稍微冷门一些的自定义算子就可能掉到 CPU 上执行性能断崖式下跌。所以选择在边缘设备上跑的模型时一定要避开过于复杂的自定义模块。4.4 帧率上不去时先查解码还是先查推理很多人在边缘设备上发现帧率上不去第一反应是模型推理太慢拼命换更小的模型。但我的排查经验是先看解码先看 CPU 占用率。如果某个核已经 100%大概率是软解在跑检查有没有启用硬件解码器。再看解码线程是否与推理线程争抢 CPU 资源。RK3588 上我通常把解码线程和推理线程分别绑到不同大小核上避免互相干扰。然后看预处理是否在 CPU 上执行。很多边缘设备的 GPU/NPU 只负责推理letterbox 和颜色转换如果都压在 CPU 上1080p 输入也会让 CPU 负载很高。最后才是看 NPU/GPU 的利用率。如果 NPU 利用率不到 30%可能是模型小但预处理和前后处理成了瓶颈或者是数据拷贝内存 ↔ NPU 显存占了大部分时间。我遇到过一次很典型的问题RK3588 上推理只要 20ms但总帧率只有 15fps。排查后发现瓶颈根本不是模型而是每帧从内存拷贝到 NPU 显存需要 30ms来回拷贝就把时间吃掉了。优化思路是预分配 NPU 内存用内存池复用不频繁申请释放帧率立刻翻倍。5. 训练侧必须同步调整数据标注质量决定线上表现5.1 从 KITTI 标注转 YOLO 格式入手很多项目拿到的现成数据集是 KITTI 格式或 COCO 格式而 YOLO 训练需要的是一套特殊的归一化坐标。KITTI 标注的文件里每行是car 0.00 0 0.00 100 200 300 400 0 0 0 0 0 0 0其中坐标是像素级的 x1 y1 x2 y2。YOLO 格式要求的是归一化后的中心点坐标和宽高class_id center_x center_y width height转换逻辑很简单但我在转换时踩过坑KITTI 里有的类别是 DontCare注释直接标记为 -1这类样本既不能丢也不能当成有效的 negative 样本。我建议转换时先过滤掉 DontCare再统计类别数量确保类别 ID 映射正确。转换脚本核心如下# KITTI - YOLO 格式转换 def convert_kitti_to_yolo(kitti_line, img_w, img_h, class_map): parts kitti_line.strip().split() cls_name parts[0] if cls_name DontCare: return None # 过滤掉 x1, y1, x2, y2 map(float, parts[4:8]) box_w (x2 - x1) / img_w box_h (y2 - y1) / img_h cx (x1 x2) / 2 / img_w cy (y1 y2) / 2 / img_h return f{class_map[cls_name]} {cx:.6f} {cy:.6f} {box_w:.6f} {box_h:.6f}数据准备好之后还需要做数据集划分。这里有个容易翻车的地方如果直接从所有图片里随机划分训练集和验证集同一个视频里连续两帧可能一条在训练集、一条在验证集模型相当于“见过”了验证数据评估结果虚高。对视频数据正确做法是先把视频切片再按整个视频片段划分数据集。我一般按 8:1:1 划分训练、验证、测试集并且按视频源维度做隔离。5.2 视频数据集的特殊性连续帧几乎是重复的实时视频项目采集数据时摄像头的帧率通常是 25fps。如果你直接连续采样所有帧相邻两帧画面高度相似数据多样性非常差。我的做法是每秒抽 1 到 2 帧用于训练其余帧要么丢弃要么用来做无监督/半监督的辅助样本。这一段操作很容易被忽视但对模型在视频流上的表现影响巨大。只针对“抽出来的独立图片”训练出来的模型在真正的视频流里可能会出现检测框抖动。原因在于训练数据的帧间连续性不足模型没有学会利用时序上下文。后续我们尝试在半监督辅助标注时用未标注帧的伪标签和时序平滑做二次训练抖动问题才明显改善。关于标注环节现在团队已经很少用纯手动框标注了基本流程是先跑一个预标注模型自动打框人工再修正。这个“预标注人工修正”的模式能省 60% 以上的时间。管理标注任务的平台最好能支持多人协作、冲突检查和格式导出最近关注的开源标注工具也基本都覆盖了这些能力成本主要在部署和模板配置上。5.3 YOLO 训练参数怎么兼顾实时推理训练实时模型时有几个人为可控的参数会直接影响部署性能输入分辨率imgsz640×640 是精度与速度的平衡点。如果目标很小比如远距离的人、烟头、积水可以尝试 960 或 1280但推理时间会显著上升。我用 1280 训练过远程烟火检测精度确实涨了 3-4 个点但边缘设备上帧率掉了一半最后只在云端推理场景使用。混合精度训练时开 AMP 能大幅减少显存占用同时训练速度更快还能间接提升最终模型的鲁棒性。用 NVIDIA GPU 训练时我基本默认开启。batch size硬件允许的情况下越大越好能提升训练稳定性。但要配合学习率调节策略不能一味加大。锚框设置新版 YOLO 已经能自动学习锚框对特殊场景如摄像头俯视、目标尺度差异大非常有用。YOLO 的损失函数包括分类损失、框回归损失和置信度损失其细节不需要自己实现但选模型版本时要了解较强的损失函数比如 CIoU 家族在小目标上的收敛更好。这个特点和监控场景的小目标检测需求直接相关。另一个容易被忽略的是类别分布。视频数据天然存在正负样本不平衡大部分时间段画面里没有目标。训练时需要控制每个 batch 里负样本无目标帧/无目标的裁剪块的比例否则模型会学成“什么都不输出”。5.4 模型要小才能跑得快蒸馏和剪枝的实际收益某些项目里原始模型太大边缘设备实在跑不动。我的做法是先训练一个大模型作为“教师”然后用蒸馏的方式训练一个小模型。例如用 YOLOv8m 蒸馏出 YOLOv8s能让小模型在相同推理耗时下精度比直接训练高出 1~2 个点。这个过程需要调蒸馏损失权重实践下来 0.5 到 1.0 的软标签权重比较稳妥。剪枝是另一个方案。YOLOv5 官方有基于 BN 稀疏化的剪枝教程可以对通道进行结构化剪枝直接减少计算量。但剪枝后的模型在 RK3588 上不一定能加速因为 NPU 对通道数的对齐有要求。所以我给边缘项目的建议是优先用更小的模型蒸馏而不是硬剪枝否则容易在 NPU 上白忙活。6. 多路并发与线上稳定性踩过的坑和最后留下的方案6.1 丢帧策略 vs 追帧策略实时系统里必须二选一实时视频 AI 有一个反直觉的结论处理不过来的时候丢帧是对的追帧是错的。我见过不少团队发现推理慢了以后还把每一帧都排队处理结果队列越来越大从“延迟 1 秒”变成“延迟 10 秒”最后画面上显示的检测框和实际时间点完全对不上。实时系统里时间戳是生命线检测结果一旦严重滞后就失去了实时告警的意义。我的策略是在每个队列上设置最大长度比如 5 帧。队满时直接丢弃最旧的帧保证新帧能及时进入处理管线。这样极端场景下可能出现几帧漏检但整体延迟不会失控。业务上我们规定漏检一帧可以接受延迟 10 秒不可接受。6.2 多路视频共享 GPU 的推理排队与动态批处理多路视频并发时如果把每路视频的推理单独跑GPU 利用率会非常低。GPU 推理更擅长批量处理把多个请求合并成一个 batch 同时推理吞吐量可以成倍提升。我最初的实现里推理线程池从所有视频队列里取出待处理的帧积累到一定数量比如 4 帧或者等待一小段窗口时间比如 3ms再一起送进模型。这一套“动态批处理”逻辑让 GPU 利用率从 30% 提升到 70% 以上。但动态批处理有一个副作用每条流的延迟会比独立推理略高因为要等待同 batch 的其他帧。对实时要求很苛刻的场景需要设置最大等待时间和 batch 大小之间的平衡。我一般用“batch4、等待 2ms”的配置多路并发下延迟增加不超过 10ms吞吐却能翻倍完全划算。6.3 长时间运行稳定性显存泄漏与线程堆积实时服务是 7×24 小时跑的最怕的问题是长尾内存泄漏。YOLO 推理时如果用 PyTorch 默认的 CUDA context偶尔会有一段显存不释放。我排查过一个连续跑一周后显存占满的案例最后定位到是某个版本里每次推理前创建的临时 tensor 没有释放导致显存缓慢增长。解决方式是推理 session 常驻不要每次调用都重新加载模型推理过程中避免在循环里创建新的 tensor 或者调用会导致隐式内存分配的函数使用显存池复用中间结果。代码层面我建议在每次推理后打印 cuda memory 信息观察是否随帧数线性增长# 监控显存占用 print(torch.cuda.memory_allocated() / 1024**2, MB allocated) print(torch.cuda.memory_reserved() / 1024**2, MB reserved)线程堆积也是常见的隐形炸弹。如果回传通道比如 MQTT broker临时不可用回传线程可能阻塞后面帧就会越积越多最终导致整个流水线阻塞。我的策略是回传失败直接丢弃结果记录到本地日志等通道恢复后通过定时任务补发关键告警而不是让回传线程阻塞整个推理管线。6.4 线上监控怎么判断 AI 真的在“实时工作”项目上线后我最常被问的问题就是“这套系统到底跑得怎么样”。只看业务告警是不够的必须建立一份 AI 运行指标面板。我最终固定的核心指标包括指标正常范围含义解码帧率 (fps)与视频源帧率接近判断拉流和解码是否正常推理帧率 (fps)不低于业务设定值判断模型推理是否达到要求端到端延迟 (ms)通常 500ms从画面出现到结果推送的时间队列积压深度接近 0判断链路是否发生拥堵GPU/NPU 利用率30%~80%判断资源是否被充分利用有一次项目上线后面板显示解码帧率正常但推理帧率只有源帧率的一半。最后查到是底层推理线程池线程数配置过小在弱网环境下视频码流波动导致解码输出瞬时增加推理线程无法消化任务。我把线程数和队列策略调成自适应模式后推理帧率才稳定下来。这些指标还有一个用途调优之后可以通过对比面板数据量化收益。比如换了硬件解码以后延迟从 800ms 降到 300ms数据一目了然汇报和复盘都不用拍脑袋。我个人在几个项目滚过来之后最大的体会是实时视频 AI 不是“在 YOLO 外面包一层视频流”这么简单它是一个从采集到解码再到推理和回传的系统工程。模型本身的精度重要但工程链路的稳定性、延迟可控性、多路并发能力有时比模型 mAP 更能决定项目成败。如果你是第一次从单帧检测往实时视频方向过渡建议从最小闭环开始先跑通一路 RTSP 视频的完整流水线再逐步扩展多路并发和边缘部署。另外一个小技巧在线上环境保留一个“最近 30 秒抽帧回放”的环形缓冲出问题的时候能立刻定位是模型误判还是链路故障这个能力在调试阶段帮了我大忙。