
1. 多模态视频理解工程的整体设计思路1.1 为什么视频理解不能直接“喂”给模型做过视频理解的人都有一个共识视频本质上是一串连续的图像帧加上音轨而绝大多数多模态大模型的原生输入是图像 文本并不直接吃视频流。所以工程上要做的第一件事就是把视频“翻译”成模型能消化的形式——抽帧成图再配上文字提示最后组装成一次可提交的请求。这个翻译过程听起来简单但真正落地时会发现三个绕不开的约束。第一是Token 预算每一帧图像在模型侧都会被编码成一定数量的视觉 Token帧数越多Token 消耗越大成本和延迟同步上升。第二是信息密度视频里大量帧是冗余的比如静止画面、缓慢移动无脑均匀抽帧既浪费预算又抓不住关键动作。第三是时序语义很多问题“他什么时候放下杯子”“这个动作持续了几秒”依赖帧与帧之间的先后关系抽帧策略必须保留时间线索。所以一个合格的多模态视频理解工程核心就是在这三个约束之间找平衡用尽量少的帧保留尽量多的有效信息并让 Prompt 能准确指向这些信息。这也是我把标题拆成“抽帧策略、帧预算、Prompt 组装”三块的原因它们是一条流水线上的三个工位任何一个出问题最终答案都会崩。1.2 整条流水线的四个阶段我把实际项目里的处理链路拆成四段后面每个章节都会围绕它们展开。预处理阶段解码视频、读取元信息时长、帧率、分辨率、判断是否需要音频轨。抽帧阶段根据策略决定抽哪些帧、抽多少帧输出一组带时间戳的图像。帧预算阶段把抽出的帧按 Token 成本做裁剪、缩放、去重控制在模型可接受的范围内。Prompt 组装阶段把帧序列、时间戳、任务指令拼成结构化输入提交给模型。这四段里抽帧和帧预算是“省钱”的Prompt 组装是“花对钱”的。很多人只关注 Prompt 写得好不好却忽略了前面帧选得对不对结果就是 Prompt 再精致模型看到的画面本身就是错的答案自然离谱。1.3 适用人群与典型场景这套东西适合三类人参考。第一类是做视频问答、视频摘要、内容审核的工程师需要把视频理解能力接进自己的产品。第二类是做多模态 Agent 的开发者视频只是其中一种输入模态需要统一 Token 预算管理。第三类是研究侧的同学想复现视频理解实验需要一套可控、可解释的抽帧基线。典型场景包括短视频内容打标、长视频章节切分、监控片段异常检测、教学视频知识点定位、直播回放精彩片段提取。这些场景的共同点是视频长度差异极大从几秒到几小时如果只有一套固定抽帧参数要么短片段浪费预算要么长视频丢信息。所以策略必须是自适应的。2. 抽帧策略从均匀采样到内容自适应2.1 均匀抽帧最简单也最容易踩坑的基线均匀抽帧就是按固定间隔取帧比如每秒 1 帧1 fps或每 N 帧取 1 帧。它的优点是实现简单、时间分布均匀、帧与帧之间的时间差恒定方便模型做时序推理。代码上基本就是按帧号取模import cv2 def uniform_sample(video_path, fps_target1): cap cv2.VideoCapture(video_path) src_fps cap.get(cv2.CAP_PROP_FPS) step max(int(round(src_fps / fps_target)), 1) frames, idx [], 0 while True: ret, frame cap.read() if not ret: break if idx % step 0: ts idx / src_fps frames.append((ts, frame)) idx 1 cap.release() return frames这段代码看着没问题但实测下来有两个坑。第一个坑是变帧率视频很多手机拍摄或经过转码的视频标称 30fps 但实际帧间隔不均匀按帧号取模会导致时间戳漂移。解决办法是用CAP_PROP_POS_MSEC按时间戳定位而不是按帧号。第二个坑是关键帧对齐视频编码里 I 帧关键帧解码成本低P/B 帧依赖前后帧如果抽到的全是依赖帧解码会变慢。工程上可以优先在关键帧附近取样减少解码开销。提示均匀抽帧适合“内容变化平缓”的视频比如讲座、访谈。一旦视频里有快速动作或场景切换均匀采样很容易刚好错过关键瞬间。2.2 基于场景切换的抽帧抓住“变化点”视频里真正有信息量的往往是场景切换的那几帧。检测场景切换最常用的方法是计算相邻帧的直方图差异或结构相似度SSIM差异超过阈值就认为发生了切换。import cv2 import numpy as np def scene_change_sample(video_path, threshold0.6): cap cv2.VideoCapture(video_path) src_fps cap.get(cv2.CAP_PROP_FPS) frames, prev_hist, idx [], None, 0 while True: ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) hist cv2.calcHist([gray], [0], None, [64], [0, 256]) hist cv2.normalize(hist, hist).flatten() if prev_hist is not None: diff cv2.compareHist(prev_hist, hist, cv2.HISTCMP_BHATTACHARYYA) if diff threshold: frames.append((idx / src_fps, frame)) else: frames.append((idx / src_fps, frame)) prev_hist hist idx 1 cap.release() return frames阈值threshold是这套方法的核心参数。设太低会把镜头内的小抖动也当成切换帧数爆炸设太高会漏掉真正的场景变化。我的经验是先用 0.5 到 0.7 之间试再根据抽出的帧数反推调整。如果抽出的帧数超过预算上限就提高阈值如果关键场景没抓到就降低阈值。场景切换抽帧的另一个变体是结合运动量。用光流或帧差计算画面运动强度运动剧烈的地方多抽静止的地方少抽。这比单纯看直方图更贴近“内容变化”的本质但计算成本更高适合离线处理。2.3 混合策略均匀打底 变化点补强纯均匀会漏关键帧纯场景切换在长静态片段里会抽得太少。实际项目里我用的最多的是混合策略先按较低频率比如 0.5 fps均匀抽一遍作为“底图”保证时间轴上有基本覆盖再在场景切换点额外插入帧保证变化瞬间不丢。具体做法是维护一个帧集合均匀帧先放进去场景切换帧再按时间戳去重后加入。去重时设一个最小时间间隔比如 0.2 秒避免同一瞬间抽好几帧。这样最终帧序列既有均匀的时间骨架又在关键点上有加密。策略优点缺点适用场景均匀抽帧实现简单、时间均匀易漏关键帧、变帧率下漂移讲座、访谈、监控场景切换抓住变化点、信息密度高静态片段抽太少、阈值难调剪辑视频、动作片段混合策略兼顾覆盖与关键点实现稍复杂、需去重通用长视频理解2.4 抽帧策略选择的实操心得选策略之前先问自己一个问题这个视频的“信息”是均匀分布的还是集中在少数瞬间如果是前者均匀抽帧就够如果是后者必须上场景切换或混合策略。还有一个容易被忽略的点是音频。很多视频的关键信息在语音里比如“接下来我要演示第二步”。如果只抽帧不看音频模型可能完全抓不到重点。工程上可以先用语音识别把音频转成文字再把文字按时间戳对齐到帧序列作为 Prompt 的一部分。这样即使画面帧抽得少语义信息也不丢。注意抽帧不是越多越好。我见过有人为了“保险”把 1 小时视频抽成 3600 帧结果 Token 直接爆掉请求被拒。帧数和 Token 预算是强绑定的下一章专门讲怎么算这笔账。3. 帧预算Token 成本的精算与控制3.1 一帧到底消耗多少 Token这是整个工程里最容易被低估的问题。不同模型对图像的 Token 编码方式不同但大体遵循一个规律图像被切成固定大小的 patch每个 patch 对应一个视觉 Token。比如某类模型把图像缩放到固定分辨率后切成 14x14 的 patch一张 336x336 的图就是 24x24576 个 Token。所以一帧的 Token 成本取决于输入分辨率和模型的 patch 划分方式。分辨率越高patch 越多Token 越多。这就引出一个关键取舍视频帧真的需要原分辨率吗对于“识别画面里有没有人”这种任务224x224 就够了对于“读取画面里的文字”这种任务可能需要 448x448 甚至更高。我的做法是先确定任务所需的最小分辨率再据此估算单帧 Token。比如任务只需要判断动作类型224x224 足够单帧约 256 Token如果需要读字幕448x448单帧约 1024 Token。这个数字直接决定了帧预算的上限。3.2 帧预算的计算公式把上面的逻辑整理成一个可操作的公式可用帧数 (总 Token 预算 - 文本 Token - 系统开销) / 单帧 Token其中总 Token 预算由模型上下文窗口决定比如 128K 上下文留出 4K 给输出再留 2K 给系统提示和文本指令剩下约 122K 给视觉。如果单帧 256 Token理论上能放约 476 帧如果单帧 1024 Token只能放约 119 帧。这个计算看起来简单但实际用的时候要留安全余量。因为不同模型对图像的 Token 计算可能有额外开销比如图像前后的特殊标记而且 Prompt 里的时间戳、任务描述也会占 Token。我一般会在理论值上打八折避免请求被截断。def estimate_frame_budget(total_tokens, text_tokens, tokens_per_frame, safety0.8): visual_budget total_tokens - text_tokens max_frames int(visual_budget / tokens_per_frame) return int(max_frames * safety) # 示例128K 上下文文本占 3K单帧 256 Token budget estimate_frame_budget(128000, 3000, 256) print(budget) # 约 390 帧3.3 超预算时的三种裁剪手段当抽出的帧数超过预算不能简单粗暴地从头截断那样会丢掉视频后半段的信息。我常用三种手段按优先级排序。第一种是降分辨率。把每帧从 448x448 缩到 224x224单帧 Token 直接降到四分之一帧数就能翻四倍。代价是细节丢失适合对细节要求不高的任务。第二种是去冗余帧。相邻帧如果差异很小可以合并成一帧。用前面提到的直方图差异或 SSIM 做判断差异低于阈值的帧直接丢弃只保留代表帧。这一步通常能砍掉 30% 到 50% 的帧而且几乎不丢信息。第三种是分段采样。把视频按时间切成若干段每段按比例分配帧数。比如 10 分钟视频切成 10 段每段 1 分钟预算 100 帧就每段 10 帧。这样保证每个时间段都有覆盖不会出现“前面密后面空”的情况。裁剪手段Token 节省信息损失适用情况降分辨率高约 75%中细节丢失任务不依赖细节去冗余帧中30%-50%低静态片段多分段采样可控低长视频均匀覆盖3.4 帧预算与延迟的隐性关系Token 预算不只影响成本还直接影响响应延迟。视觉 Token 越多模型前向计算量越大首 Token 延迟和总生成时间都会上升。实测下来视觉 Token 从 10K 涨到 50K延迟可能翻两三倍。所以如果产品对延迟敏感比如实时问答帧预算要压得更紧。我的经验是实时场景单次请求视觉 Token 控制在 20K 以内离线批处理可以放宽到 100K 以上。这个阈值不是绝对的要结合具体模型的推理速度和硬件条件实测。提示做帧预算时先把“最坏情况”算出来——最长视频、最高分辨率、最多帧数看会不会超。如果会超就在抽帧阶段提前限制而不是等到组装 Prompt 时才发现放不下。4. Prompt 组装让模型看懂帧序列4.1 帧序列的结构化表达抽出来的帧是一堆图像直接丢给模型模型不知道哪张在前哪张在后也不知道每张对应什么时间。所以 Prompt 里必须把帧和时间戳绑定并且明确告诉模型这是按时间顺序排列的。我常用的格式是给每帧加一个文本标记比如[帧 1 | 00:00.0] image [帧 2 | 00:02.5] image [帧 3 | 00:05.0] image ... 问题视频中的人在第几秒放下了杯子这样模型既能看到画面又能通过时间戳建立时序关系。时间戳的精度根据任务定动作识别用 0.5 秒精度就够精细动作分析可能需要 0.1 秒。有一点要注意帧标记本身也占 Token。如果帧数很多比如 300 帧每帧标记 10 个 Token就是 3000 Token。所以标记要尽量简洁能省则省。我一般用[F1|0.0]这种短格式把说明放在系统提示里统一解释。4.2 任务指令的写法从模糊到可执行Prompt 组装里最容易出问题的是任务指令太模糊。比如“描述这个视频”模型不知道你要描述什么粒度、什么方面。好的指令应该是可执行、可验证的。对比一下模糊“这个视频讲了什么”可执行“按时间顺序列出视频中出现的所有动作每个动作标注开始时间秒和持续时长。”后者明确了输出格式时间顺序、动作、开始时间、持续时长模型更容易给出结构化答案后续也方便程序解析。对于视频问答我习惯把指令拆成三部分角色设定 任务描述 输出格式。角色设定让模型进入状态“你是一个视频内容分析助手”任务描述说清楚要做什么输出格式约束结果结构。这三段加起来通常 100 到 300 Token相对于视觉 Token 可以忽略但对结果质量影响很大。4.3 少样本示例的取舍在 Prompt 里放一两个示例few-shot能显著提升输出稳定性尤其是格式复杂的任务。但示例本身占 Token而且如果示例和实际视频差异大反而会误导模型。我的判断标准是任务格式越复杂越值得放示例任务越开放示例越可能限制模型。比如“输出 JSON 格式的动作列表”这种任务放一个示例能省掉大量格式纠错而“总结视频内容”这种开放任务放示例反而让模型照搬示例的措辞。如果决定放示例示例要短、要典型、要和实际任务同分布。我一般控制在 1 到 2 个每个不超过 100 Token。4.4 Prompt 组装的完整代码骨架把上面几块拼起来一个可复用的组装函数大概长这样def build_video_prompt(frames, question, task_typeqa): # frames: [(timestamp, image_placeholder), ...] lines [] for i, (ts, _) in enumerate(frames, 1): lines.append(f[F{i}|{ts:.1f}] image) frame_block \n.join(lines) if task_type qa: instruction ( 你是一个视频内容分析助手。下面是按时间顺序抽取的视频帧 每帧标注了序号和时间戳秒。请根据这些帧回答问题。 如果信息不足明确说明无法判断不要编造。 ) output_format 输出先给结论再给依据引用帧序号。 elif task_type summary: instruction ( 你是一个视频摘要助手。下面是按时间顺序抽取的视频帧。 请按时间顺序总结视频内容每个阶段标注起止时间。 ) output_format 输出分点列出每点包含时间段和内容概述。 prompt f{instruction}\n\n{frame_block}\n\n问题{question}\n{output_format} return prompt这个骨架的好处是任务类型可切换问答和摘要共用同一套帧表达只是指令和输出格式不同。实际项目里还可以加更多任务类型比如“定位某个动作出现的所有时间点”“判断视频是否包含某类内容”。4.5 Prompt 组装的避坑经验第一个坑是帧序号和时间戳不一致。如果抽帧时时间戳算错了Prompt 里的时间就是错的模型基于错误时间推理答案必然错。所以抽帧阶段的时间戳要单独校验最好和视频播放器对一遍。第二个坑是图像占位符和实际图像顺序不匹配。多模态接口通常要求文本里的图像标记和传入的图像数组顺序一致。如果组装时顺序乱了模型看到的画面和标记对不上结果会非常诡异。我的做法是组装完 Prompt 后打印一遍帧序号和图像数组的对应关系人工抽查前几帧。第三个坑是指令里的否定句。比如“不要描述无关内容”模型有时会反过来关注“无关内容”。更好的写法是正面约束“只描述与问题相关的画面元素”。注意Prompt 里的时间戳单位要统一。我见过有人混用秒和毫秒导致模型把 2.5 秒理解成 2.5 毫秒时序推理全乱。统一用秒保留一位小数是最稳妥的做法。5. 常见问题与排查技巧实录5.1 模型答非所问先查帧再查 Prompt遇到模型答非所问很多人的第一反应是改 Prompt。但我的经验是先查帧。把抽出的帧单独存成图片按顺序看一遍问自己如果我是模型只看这些帧能回答这个问题吗常见情况是帧抽得太稀关键动作刚好在两帧之间模型根本没看到。这时候改 Prompt 没用得回去调抽帧策略。另一种情况是帧抽得不少但分辨率太低画面糊成一团模型看不清细节。这时候要提分辨率或换更清晰的帧。只有确认帧没问题才去查 Prompt。Prompt 的问题通常是指令模糊、输出格式没约束、或者示例误导。5.2 Token 超限的排查路径Token 超限的报错通常很直接但定位原因需要一步步来。我的排查顺序是打印实际组装的 Prompt 长度字符数近似 Token 数。分别统计文本部分和视觉部分的 Token 占比。如果视觉占大头回去看帧数和单帧分辨率。如果文本占大头检查是不是示例太长或帧标记太啰嗦。现象可能原因解决方向请求被截断总 Token 超上下文减帧或降分辨率视觉 Token 异常高分辨率过高或帧数过多检查单帧尺寸和帧数文本 Token 异常高示例过长或标记冗余精简示例和标记延迟过高视觉 Token 过多压缩帧预算5.3 时序推理错误的典型表现时序推理错误的表现很有特点模型能描述画面内容但时间关系全错。比如把“先开门后开灯”说成“先开灯后开门”。这种错误八成是时间戳信息丢失或错乱。排查时先确认 Prompt 里每帧都有时间戳且时间戳单调递增。如果时间戳没问题再看帧的抽取顺序是不是和实际时间顺序一致。有些抽帧实现会并行处理导致帧顺序打乱这时候要在组装前按时间戳排序。还有一种情况是帧间隔太大模型无法判断先后。比如两帧间隔 10 秒中间发生了什么完全不知道模型只能猜。这时候要么加密抽帧要么在 Prompt 里明确说明“帧之间可能有未展示的内容”。5.4 独家避坑清单抽帧前先探测视频元信息时长、帧率、分辨率、是否有音频这些决定了后续所有参数。时间戳用浮点秒保留一位小数避免整数截断导致时序精度丢失。帧标记尽量短把解释放在系统提示里省下的 Token 可以多放几帧。组装完 Prompt 先本地打印检查确认帧序号、时间戳、图像顺序三者一致再提交。保留原始抽帧结果方便出问题时回溯不用重新解码视频。对长视频做分段处理每段独立抽帧和组装最后合并结果避免单次请求过大。Prompt 里的指令用正面表述少用否定句减少模型理解偏差。5.5 性能与成本的平衡技巧如果项目对成本敏感可以在抽帧阶段就做预筛选。比如先用一个轻量模型或简单的图像分类器判断每帧是否包含目标物体只把包含目标的帧送进大模型。这样能大幅减少视觉 Token代价是增加一次轻量推理。另一个技巧是缓存重复内容。如果多个视频有相同的片头片尾可以识别出来只处理一次。对于批量处理场景这个优化能省不少。还有一个容易被忽略的点是输出 Token 也要算进预算。如果任务要求模型输出很长的描述输出 Token 可能占几千这部分要从总预算里扣掉留给视觉的就更少了。所以任务设计时输出格式要尽量紧凑。6. 从工程视角看这套流水线的扩展方向6.1 音频与画面的联合理解纯视觉的抽帧会丢掉音频信息而很多视频的关键语义在语音里。把语音识别结果按时间戳对齐到帧序列作为 Prompt 的一部分能让模型同时利用画面和语音。实现上就是在帧标记之间插入对应的转录文本比如[F1|0.0] image [语音|0.0-2.5] 大家好今天讲抽帧策略 [F2|2.5] image这样模型既能看到画面又能读到讲解内容理解会更准确。代价是文本 Token 增加需要在帧预算里预留空间。6.2 自适应帧预算的在线调整固定帧预算适合离线批处理但在线场景下视频长度不可预知需要动态调整。我的思路是先按低频率抽一遍估算总帧数和 Token如果超预算就提高去冗余阈值或降分辨率直到落在预算内。这个过程可以迭代两三次找到满足预算的最大信息量方案。6.3 多视频对比场景的处理有些任务需要对比多个视频比如“这两个视频的动作有什么不同”。这时候帧预算要在多个视频间分配不能一个视频占满。我的做法是每个视频分配相同帧数Prompt 里用分隔符明确区分视频边界指令里说明“视频 A”和“视频 B”的对应关系。6.4 结果可解释性的增强模型给出答案后如果能标注答案依据的是哪几帧可解释性会强很多。实现方式是在 Prompt 里要求模型引用帧序号比如“结论第 5 帧到第 8 帧显示人物在跑步”。这样人工复核时可以直接跳到对应帧验证排查问题也更快。我个人在实际操作中的体会是这套流水线里最值得花时间打磨的是抽帧策略因为它决定了模型能看到什么。Prompt 写得再好也救不回没抽到的关键帧。帧预算和 Prompt 组装更像是“守门员”保证不超限、不混乱。把这三块都做扎实视频理解的准确率和稳定性会有明显提升。后续如果要做产品化建议先把抽帧和预算做成可配置的模块不同任务用不同参数而不是一套参数打天下。