ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

ShallowStream:先浅层索引再深度回答的流式视频理解架构

ShallowStream:先浅层索引再深度回答的流式视频理解架构 ShallowStream 这个命名已经把它自己的方法论写清楚了“先浅层索引再深度回答”。视频流进来不急着把每一帧都塞进大模型而是先让一个便宜的“浅层索引”把视觉信息按时间片快速记下来等用户带着问题出现再根据索引精准定位视频中的相关片段把真正昂贵的深度推理花在最有用的地方。这套设计本质上是把流式视频理解拆成了“持续低成本的写”和“按需高价值的读”两件事很适合直播内容理解、摄像头实时监控、体育赛事拆解、长视频问答这些对实时性和成本都敏感的场景。本文会把 ShallowStream 从设计思路上拆开讲清楚浅层索引层、深度问答层、查询同步机制、接口设计、性能观察方法并给出一套可以直接照做的最小验证方案。如果你正在做流式视频理解、视频问答系统或者想把长视频处理接入到视频多模态模型上这篇文章可以先收藏后面照着搭一个简化版原型。1. ShallowStream 核心设计速览能力项说明定位流式视频理解架构方法论系统级设计非单一模型核心思想先建立低成本的视觉索引查询到来时再对相关片段进行深度推理回答解决痛点全量逐帧推理成本高、长视频时序定位难、实时流无法预先缓存完整视频浅层索引层持续抽帧、特征编码、镜头检测、时间戳登记形成可检索的视觉记忆深度回答层用户查询驱动从索引中召回候选片段只对少量关键帧/片段进行高精度理解支持流式输入是视频流无需等待结束索引随帧到达持续更新支持多轮问答取决于深度回答层模型和查询管理设计接口能力可通过 REST/WebSocket 暴露查询服务方便接入业务系统批量任务可支持多路视频流并行索引批量视频场景需按输入源设计任务队列硬件门槛浅层索引用 CPU/轻量 GPU深度回答层建议独立使用较高显存 GPU 或模型服务集群显存占用与视频长度、特征维度、缓存帧数和 VLM 参数量强相关需以实际环境测试为准部署方式Python 服务化部署可拆分为索引进程、问答进程、统一 API适合读者流式视频系统研发、实时视频分析、视频问答应用开发者这里要说明一点ShallowStream 不是某个能直接 off-the-shelf 拿来推理的“模型权重”更像一套值得参考的系统组织方式。如果项目已发布官方实现以官方代码为准如果还没有本文提供的最小原型也足够把核心链路跑通。2. 流式视频理解为什么需要“先浅后深”流式视频理解和普通“视频理解”最大的区别在于输入形式视频不是一个静止的完整文件而是按帧不断到达的数据流。拿摄像头实时画面为例视频没有“结尾”你不能等整段视频下载完再做离线推理因为那样实时性就没意义了。直接做全量深度理解的方案往往有四个问题。第一是成本不可控。视频一秒钟通常有 24 到 30 帧就算按 1 秒抽 1 帧的关键帧策略一小时视频也要处理 3600 帧。把这些帧全部喂给视频多模态模型做逐帧理解推理开销和显存压力会非常夸张。如果视频源有几十路离线任务都要排队在线实时查询根本跑不动。第二是时序定位困难。用户的问题往往是“从第 10 分钟开始那个人在做什么”或者是“上一分钟这里是不是发生了异常”。如果系统没有预先建立时间索引回答问题时只能重新扫一遍历史视频定位成本极高。第三是计算资源与查询频率不匹配。真实业务中用户查询并不是每秒都来更多时候是“偶尔问一句”。如果为偶尔的查询长期占用一整块高性能 GPU 去跑全量视频资源利用率很低。第四是长序列建模仍有压力。视频 VLM 在长视频上的注意力计算会随帧数增长帧数越多推理越慢。把整段视频全塞进上下文不只是成本问题还是精度问题。ShallowStream 的思路是在这两个任务之间切一刀建索引的人一直干活但干的是便宜快速的活回答问题的人只在有需要时干活但干得足够细致。系统给人感觉是“一直在理解视频”但实际只有很轻的开销在持续发生深度推理被限制在查询触发的窄路上。这个思路跟 RAG检索增强生成在文本里的做法很像本质上就是给视频流引入了一层时间感知的检索记忆。3. ShallowStream 整体架构与数据流从工程实现上说ShallowStream 可以拆成四个组件视频流接入层、浅层索引引擎、深度回答引擎、查询同步服务。视频流接入层负责统一接收视频源。不管输入是 RTSP 摄像头、直播流、本地视频文件还是短视频平台下发的视频都先转成一个统一的时间轴流保证后续索引引擎能拿到带时间戳的图像帧。浅层索引引擎是整个架构里唯一持续运行的推理模块。它每秒钟处理视频流的抽帧结果做场景检测、物体/区域标记、视觉特征嵌入然后把这些信息写入索引存储。这个组件要求的是“快”和“稳”它的输出是深度回答引擎能够快速查找的视觉记忆索引。深度回答引擎是查询触发的。当用户提问到达系统先让一个轻量级查询理解模块把问题转换成检索条件在索引里召回与问题最相关的时间区间再从视频流回放模块中取出对应帧或多帧片段交给一个高质量视频多模态模型做深度回答。这个组件不要求一直工作但对单次回答质量和响应速度有要求。查询同步服务要解决的是“索引在持续更新查询随时到来”的并发问题。它以查询时间为基准决定答案是只基于已索引的视频内容回答还是允许等待一小段索引补录时间。对实时直播场景通常会选择“只基于已索引内容回答”宁可少一点信息也不能让用户等过久。整条数据流可以简化成下面这样一个流程不需要理解复杂概念顺着箭头往下看就行视频流接入层 - 抽帧 - 浅层索引引擎 - 索引存储 | 用户查询 - 查询理解 - 候选片段召回 ----- 深度回答引擎 - 文本回答建索引的路径一直在走深度回答的路径只在查询时激活。索引存储是用内存、SQLite 还是向量数据库取决于你的视频路数和索引规模后面会专门说怎么做选择。4. 浅层索引层持续低成本的“写路径”浅层索引层是 ShallowStream 里性价比最高的一层也是整套系统能不能跑起来的关键。设计目标很明确以极低成本把视频流中的重要视觉信息变成可检索的结构化数据不能漏掉用户可能会问的内容。先说抽帧策略。实时视频流帧率很高没必要每一帧都抽。常见实践是每秒抽 1 到 2 帧作为基础索引如果检测到镜头切换、画面剧烈变化、有物体进入特定区域再临时追加抽帧。这样日常索引开销小关键动作不会漏。接着是特征提取。对抽出来的帧用一个轻量级视觉编码器做特征嵌入产生一个向量。这里的关键在于“浅层”嵌入模型不追求对画面内容做出细致描述只要能表达“这个画面大致是什么”就足够支撑后续召回候选片段。如果视频有固定区域分析需求可以再把目标检测模型放到这一层检测结果作为结构化标签和时间戳一并存入索引。镜头检测和场景切换检测也适合放在浅层索引里。通过计算连续帧之间的视觉特征距离距离超过阈值就认为发生了场景切换标记一个新镜头起点。镜头信息对后续回答非常有用比如“从第二段开始发生了什么”系统可以直接把第二段的镜头范围作为候选区间。索引存储的数据结构建议至少包含这些字段字段含义frame_id帧序号stream_id视频源 IDtimestamp_ms视频时间戳毫秒feature_vector视觉特征向量用于相似召回scene_id所属镜头 IDtags检测标签如 person、vehicle、异常区域meta扩展信息如分辨率、曝光信息存储选型上如果只有一路视频且索引规模不大SQLite 加内存向量搜索就够用如果有多路视频且要长期保存历史索引建议用支持向量的检索数据库并且按 stream_id 和时间分区。这里不用纠结一开始就上多复杂的组件先跑通链路再考虑扩展。浅层索引层要特别注意“积压”问题。如果索引速度跟不上视频输入速度索引会越积越多查询时拿到的时间区间就会越来越滞后。所以这个组件的性能指标只有一个核心索引追赶延迟。测试时只要看系统启动后索引时间戳和视频流时间戳的差值是否在稳定范围内就能判断索引层是否有健康问题。5. 深度回答层查询驱动的“读路径”深度回答层是 ShallowStream 的质量上限。用户问“视频里穿红色衣服的人去了哪个方向”最终要由这一层给出准确回答。它的设计重心不是“每帧都看”而是“知道该看哪些”。第一步是查询理解。用户问题到达后先做一个轻量级查询解析提取问题中的关键实体、动作、时间范围、区域限定。例如“刚刚有没有人进入仓库”会解析出实体“人”、动作“进入”、区域“仓库”、时间“刚刚”。这一步很重要因为后续的索引召回依赖这些条件。第二步是候选片段召回。根据查询解析结果在浅层索引中找满足条件的帧和镜头。召回方式可以有以下几类基于特征向量的相似检索适合“找一个跟这张图片相似的画面”基于标签和时间的硬过滤适合“有人进入仓库”基于镜头边界的区间召回适合“第二段镜头发生了什么”召回结果是一组候选时间片段这时候系统不要急着把所有候选片段都送回看。更稳妥的做法是给候选片段排序按与查询的相关性取前 K 个片段K 值通常从 5 到 20 选取具体要看深度模型的上下文上限和显存能力。第三步是深度推理。把抽取出来的候选帧或短视频片段拼接成一个紧凑的时间片段序列交给视频多模态模型。这里可以参考主流的开源视频理解模型接入方式比如 Qwen2-VL、LLaVA-Video 这类视频 VLM但具体可用性和效果要以你本机安装的模型版本和显卡配置为准。输入时最好把索引阶段记录的镜头信息和时间戳一并带上让模型知道这段画面的时间顺序。第四步是回答生成。模型根据给定的候选片段和用户问题输出文本答案。如果系统支持多轮对话需要把上一轮的查询上下文一起带入注意控制历史长度避免多轮以后上下文膨胀导致显存溢出。这里有一个工程细节值得注意深度回答层最好设计成无状态服务。每次查询请求携带本次查询需要的候选片段和上下文服务不保存历史状态。这样并发查询可以独立扩展也方便在问答服务后面加缓存遇到相同或相似查询时直接返回历史结果。6. 最小可运行实现两个进程和一个媒体管道这一节给出一套可直接照做的最小验证实现用两个进程来验证 ShallowStream 的思路。注意这是一个通用原型结构不绑定特定开源仓库如果你找到了 ShallowStream 的官方实现以官方代码为准。整个原型分成三部分视频流接入模块、索引线程、查询服务线程。视频流接入模块负责打开视频源并抽帧。这里用 OpenCV 的 VideoCapture 来演示实际项目中你可能需要换成 RTSP 拉流或者解码器输入。# stream_capture.py import cv2 import time def video_frame_generator(source): cap cv2.VideoCapture(source) if not cap.isOpened(): raise RuntimeError(fcannot open video source: {source}) fps cap.get(cv2.CAP_PROP_FPS) or 25 frame_interval max(1, int(fps * 0.5)) # 默认每秒 2 帧可按需调整 count 0 while True: ok, frame cap.read() if not ok: break count 1 if count % frame_interval 0: ts_ms int(time.time() * 1000) yield frame, ts_ms cap.release()浅层索引线程读取帧数据计算特征写入索引存储。这里用一个简单的内存列表加向量距离检索来演示正式项目里替换成 SQLite 或向量数据库即可。# shallow_indexer.py import threading import time import numpy as np from transformers import CLIPVisionModel, CLIPProcessor class ShallowIndexer: def __init__(self): self.frames [] self.features [] self.stream_id stream_001 self.lock threading.Lock() self.processor CLIPProcessor.from_pretrained(openai/clip-vit-base-patch32) self.vision_model CLIPVisionModel.from_pretrained(openai/clip-vit-base-patch32) self.vision_model.eval() def add_frame(self, frame, timestamp_ms): inputs self.processor(imagesframe, return_tensorspt) with torch.no_grad(): outputs self.vision_model(**inputs) embedding outputs.last_hidden_state.mean(dim1).squeeze().numpy() with self.lock: self.features.append(embedding) self.frames.append( {stream_id: self.stream_id, timestamp_ms: timestamp_ms, feature: embedding} ) def search_frames(self, query_embedding, top_k5): with self.lock: if not self.features: return [] score_matrix np.vstack(self.features) query_embedding top_indices np.argsort(score_matrix)[-top_k:][::-1] return [self.frames[i] for i in top_indices]查询服务线程在用户请求到来时先从索引中召回候选片段再调用深度视频模型做回答。这里用伪代码表示深度模型调用实际项目里你可以用 HTTP 调用部署好的视频 VLM 服务也可以直接用本机加载的模型。# deep_answer_service.py import json import threading from flask import Flask, request, jsonify from shallow_indexer import ShallowIndexer import numpy as np app Flask(__name__) indexer ShallowIndexer() DEEP_MODEL_URL http://127.0.0.1:8000/video_understand app.post(/query) def query_video(): payload request.get_json() question payload[question] # 1. 查询理解这里示例直接用完整问题做嵌入检索 query_embedding np.random.rand(512) # 实际应调用查询编码器生成 candidate_frames indexer.search_frames(query_embedding, top_k10) # 2. 把候选帧转成模型输入调用深度模型服务 model_payload { question: question, frames: [{timestamp_ms: f[timestamp_ms]} for f in candidate_frames] } answer call_deep_model(model_payload) return jsonify({answer: answer, candidates: candidate_frames}) def call_deep_model(payload): # 这里替换成真实视频 VLM 服务的 HTTP 调用 return {text: 候选片段已提交请替换为真实模型输出}启动方式上先启动视频索引线程再启动查询服务最后让视频源开始推送# 先启动查询服务 export FLASK_APPdeep_answer_service.py flask run --host 0.0.0.0 --port 7860再单独跑一个索引主进程从视频源读取并写入索引python index_runner.py --source ./demo.mp4这个原型不是为了直接生产使用而是帮你验证“索引线程在持续写入、查询请求能及时拿到候选片段”这条核心链路。验证的重点不是答案有多准而是流程是否通、延迟是否可接受、各自的资源占用是否在预期范围内。7. 接口 API 与查询设计参考如果要把 ShallowStream 接入业务系统通常需要设计两个层面的接口。一个是面向用户的视频问答接口另一个是面向内部系统的索引管理接口。视频问答接口主要支持两类请求单次问答请求和流式回答请求。单次问答接口客户端发送问题服务端返回回答文本和引用片段POST /query { question: 第 5 分钟以后有没有人进入仓库, stream_id: camera_01, time_scope: { start_ms: 300000, end_ms: 360000 }, top_k: 10 }服务端返回时除了回答文本还应该返回命中的候选片段时间戳方便业务端做视频回放定位{ answer: 有在第 5 分 20 秒左右一个人从仓库东门进入。, hits: [ {timestamp_ms: 320000, confidence: 0.91}, {timestamp_ms: 321200, confidence: 0.86} ] }流式回答接口适合需要持续追踪视频异常的场景。客户端建立 WebSocket 连接后服务端可以定期推送索引状态或者触发查询结果比如每 5 秒推送一次“当前是否有异常事件”的判断。这类接口更适合接监控大屏或告警系统。索引管理接口主要有两个用途注册新的视频流、删除失效视频流。POST /streams/register { stream_id: camera_02, source_url: rtsp://video-source-address, index_policy: { base_fps: 2, enable_scene_detect: true } }批量任务方面ShallowStream 的架构天然支持“多路视频并行索引 全局查询”。有多路视频时一个索引进程负责一个或者几个视频流查询服务统一面对用户查询时把候选召回范围限定在用户指定的 stream_id 里就行。如果你要把历史视频批量建立索引可以直接把视频文件当作输入源以文件名为 stream_id跑一遍索引线程再把结果写入持久化存储。8. 资源占用与性能观察方法ShallowStream 的性能观察跟传统“一个模型跑一遍”不太一样它有两条独立的性能曲线索引侧和问答侧必须分开测分开看。先看索引侧的显存和 CPU 占用。浅层索引层的模型比较轻如果用的是 CLIP ViT-B/32 这类规模的特征提取器使用 CPU 也能跑显卡上更快。观察指标主要有三个帧处理吞吐量、索引延迟、CPU/GPU 占用率。简单来说就是看“视频每进来 N 秒索引层能不能在同样时间或更短时间内处理完”。再来看问答侧的显存占用。深度回答层的显存消耗跟候选片段数量、视频 VLM 参数量、输入分辨率、上下文长度强相关。测试时依次增加候选片段数比如从 5 段加到 20 段观察显存占用和回答响应时间的变化。如果出现显存溢出第一优先降低候选片段数第二降低输入图像分辨率第三才是换更大显存的机器。批量任务的资源观察跟单路不同。多路视频并行索引时重点看 CPU 核数和内存带宽多个查询并发时重点看显卡显存和推理并发队列。可以这样设计一个简单的压测流程准备一段长度 10 分钟的视频作为测试输入。先测索引侧记录从视频开始到索引完成的总耗时。再测问答侧固定 10 个问题逐个发送查询请求记录每个请求的响应时间和显存峰值。改变候选片段数量重复步骤 3做一个简单的参数对比。最后开 2 到 4 路视频并行索引再同时发 5 个查询观察服务是否出现积压。注意不要只记录最终耗时要记录过程中时间戳的推进情况因为流式系统最怕的是索引追赶不上导致查询延迟越来越大。如果发现索引时间戳和视频流时间戳差值持续拉大就是索引层性能不达标的信号。9. 功能测试与效果验证9.1 索引写入测试目标确认视频流中每一帧特征能按时间写入索引。操作步骤准备一个视频文件比如 30 秒的演示视频。启动索引进程设置 1 秒抽 2 帧。观察索引日志每秒应新增约 2 条记录。检查索引记录的时间戳是否连续递增。判断成功的标准是索引记录时间戳与视频时间戳差值在可接受范围例如 1 秒以内。如果差值过大说明特征提取速度太慢需要降低抽帧频率或换更高效的编码器。9.2 候选片段召回测试目标确认查询时能准确召回与问题相关的候选帧。操作步骤准备一段包含明显动作变化的视频例如一个人走进房间、离开房间。建立索引后发送查询“有一个人走进房间”。观察召回结果的时间戳是否符合预期。检查召回结果是否包含动作发生时刻的附近帧。判断成功的标准是召回结果时间戳与真实事件时间戳误差在 3 秒以内。如果误差大可能需要增加抽帧频率或提升特征向量的区分能力。9.3 深度回答测试目标确认深度回答层能输出有实际意义的文本。操作步骤优先使用候选召回接口确定一个日期精细的时间区间。把该区间内的帧或视频片段作为深度模型输入。提交问题记录模型回答。对比人工标注的事件结果评估回答准确率。判断成功的标准是回答文本与视频实际内容一致能正确描述事件主体、动作和时间信息。常见的失败原因有三个候选片段没召回关键帧、模型输入分辨率太低、问题描述太模糊。三个问题分别调整索引层召回策略、输入分辨率和查询改写策略。10. 适用场景与使用边界ShallowStream 最匹配的场景是那些视频较长、查询偶发但对准确性要求高的系统。举个例子园区安防场景里几十个摄像头每天产生大量视频但用户通常不会每秒钟都在问问题更多是“查一下昨晚 10 点到 11 点东门有没有人进出”。这种场景下浅层索引层可以一直记录所有摄像头的画面变化用户查询时再从索引里快速定位候选片段把深度模型算力集中在真实相关的那十几秒画面上。索引层成本分摊到所有时间深度推理成本按查询次数计费整体资源性价比远高于每路视频都跑一个完整 VLM。直播内容理解场景也适用。赛事直播中用户可能随时问“刚才那个进球的过程是什么”。索引层记录整场直播的画面变化查询来临时系统返回进球前后十几秒的候选片段深度模型只需要“重看”这一小段就能组织回答。但 ShallowStream 并不是所有场景的最优解。如果你的业务是对每一帧都做精细语义分析比如像素级目标分割、密集动作识别那就不能只建立浅层索引深度分析必须全程在线执行浅层索引只是一个辅助角色。同样如果你只有几十秒的短视频且查询频率极高索引层的额外开销可能不如直接全量推理来得干净。使用边界合规方面要特别注意视频监控、个人人脸、活体行为等数据属于高度敏感信息。部署前必须确认视频来源合法处理个人面部信息要获得相应授权涉及版权视频或直播内容的分析也要确认合法使用边界。端到端测试时推荐使用不含真实用户隐私的演示数据不要拿真实监控数据做基准测试。相关法律和平台规定可能动态更新具体落地前应核实当时的合规要求。11. 常见问题与排查方法问题现象可能原因排查方式解决方案索引时间戳持续滞后特征提取速度跟不上视频帧率查看索引进程 CPU/GPU 占用统计单帧处理耗时降低默认抽帧频率提高特征提取并行度或换轻量编码器查询返回空结果索引层未写入数据或召回条件过严检查索引进程是否在运行查询一定时间范围内是否有索引记录确认索引写入已完成放宽候选召回条件回答内容与视频无关候选片段召回不准确检查召回结果的时间戳与事件实际发生时间是否吻合增加抽帧频率改用标签特征混合召回或调整 top_k深度回答显存溢出候选片段数过多或输入分辨率过高查看推理日志确认单次推理输入规模降低 top_k压缩输入帧分辨率限制上下文长度API 请求超时深度模型推理耗时过长查看问答服务日志统计单次推理耗时增加超时时间缩小候选片段范围或扩展推理服务实例多路视频并行时资源不够索引线程过多或特征模型重复加载使用 nvidia-smi 查看显存占用查看 CPU 使用率按路数限制并发索引线程数复用模型实例索引意外丢失进程崩溃或索引存储未持久化检查系统日志查看索引存储文件索引存储增加持久化配置关键索引记录落盘12. 最佳实践与落地建议把 ShallowStream 从原型推向生产环境建议按下面的顺序做工程化改造。先建立一套最小可运行配置。把视频源、抽帧频率、特征模型、候选召回数量、深度模型这几个参数固定成配置文件确保每次测试结果之间具有可比性。不要每次运行都临时改参数那样很难定位问题。再规划目录结构。视频源、索引存储、候选片段缓存、深度模型缓存、输出结果要分目录管理。建议使用类似下面这样清晰的文件布局data/ streams/ # 视频源 index/ # 索引存储 cache/ # 候选片段缓存 output/ # 问答结果 models/ shallow/ # 浅层索引模型 deep/ # 深度回答 VLM config/ config.yaml # 统一配置 logs/ indexer.log answerer.log批量任务要加日志和失败重试。索引任务跑多路视频时不是所有视频源都能稳定输出偶发的网络中断、解码失败都会打断任务。建议用任务队列管理索引任务记录已完成的时间戳位置失败后可断点续跑。接口服务要限制访问范围。问答服务如果暴露在公网必须加鉴权限制访问 IP防止被恶意调用消耗 GPU 资源。内部部署时也要明确服务调用方避免内部其他系统误刷查询。最后是关于模型选择和效果复核。深度回答层不要一上来就追求超大模型先试 7B 或更小参数的视频 VLM验证链路通了再考虑换更大的模型。对要上线商用或发布的结果一定要安排人工复核特别涉及人脸识别结果、异常事件判断这些高影响场景必须控制误报和漏报风险。13. 总结与下一步ShallowStream 这套“Index Shallow then Answer Deep”的核心价值是把流式视频理解从“无差别全量推理”改成了“有记忆的按需推理”。用一层低成本索引长期记录视频信息再让查询触发一次高价值的深度回答理论上能同时兼顾实时性和成本特别适合视频流长、查询稀疏但要求精准的系统。最值得先尝试的功能是先验证浅层索引的“追赶速度”拿一段真实视频流跑一会儿看看索引层能不能稳定跟上视频输入。如果能跟上整套系统的地基就稳了如果跟不上先解决索引效率再谈问答质量。最容易踩的坑是候选召回不准召回不准后面深度模型再强也答不对所以测试时要把大量精力放在索引质量和召回效果对比上。这篇文章里给的最小原型核心链路已经可以验证。下一步你可以尝试接入真实视频 VLM用真实业务问题做一轮评估然后按第 7 节的接口格式把服务对外暴露。如果官方之后发布了更完整的实现建议把自己原型里验证过的配置和踩坑记录对照官方文档再做一次收敛这样既能保留工程经验也不用重复踩别人已经踩过的坑。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进