做多模态视频理解的时候,最先让我头疼的不是模型效果,而是怎么把一个视频文件变成模型能理解的输入。最开始我天真地以为直接把视频丢给接口就行,结果要么报错说上下文超限,要么模型漏看关键内容,要么抽帧太密导致 Prompt 又长又贵还容易被拒。真正做过几个项目后才意识到:视频理解工程的价值,一半体现在“模型能力”上,另一半体现在“怎么选帧、给多少帧、把帧和指令组装成什么 Prompt”上。这篇文章就把我在这条链路里踩过的坑和沉淀下来的方法完整梳理一遍,包括抽帧策略、帧预算的计算思路、以及多模态 Prompt 的组装细节,希望能给正在做同类项目的朋友一些可直接落地的参考。

1. 视频理解工程的现实约束:为什么不能直接把视频丢给模型

1.1 多模态模型到底是怎么“看”视频的

先说一个最基础的问题:模型并不像人类一样连续地“看完”一段视频。无论是开源的多模态模型还是商业 API,其输入单位始终是“图像”和“文本”的序列。当一段视频被传给支持视频输入的接口时,服务端实际上也是在内部做好了抽帧,再把帧序列送入视觉编码器。只不过这个过程对调用方不可见、不可控,很多厂商的默认抽帧策略是每秒抽 1 帧或按固定间隔抽帧,分辨率、质量、时间戳位置都没法精细调整。

这带来一个矛盾:视频理解任务真正需要的,往往不是“均匀地看每一秒”,而是“在关键瞬间多看几眼”。以一段 10 分钟的体育集锦为例,默认每秒 1 帧会让模型把大量 token 花在静态镜头和重复画面上,而运动员起跳、扣篮、落地的一瞬间却可能只被捕捉到 1 到 2 帧,信息密度严重不足。如果这块内容恰好是业务方最关心的,那模型输出自然没法看。

我建议所有做视频理解工程的同学,先放弃“模型能直接处理视频”的幻想,把心态切换成“抽帧能力 + 帧预算控制 + Prompt 组装”三项基本功。这是我接手多个视频分析项目后最深刻的体会。

1.2 从“视频文件”到“结构化回答”的工程链路

我在实际项目里,会把视频理解拆成下面这条链路:

源视频文件 → 视频信息探测(时长、分辨率、帧率)→ 抽帧策略执行(均匀/场景检测/任务驱动)→ 帧预算控制(限制帧数、尺寸、token 估算)→ Prompt 组装(文本指令 + 图片序列 + 时间戳)→ 模型推理 → 结构化输出解析。

其中抽帧策略决定了信息的下限,帧预算决定了信息的上限,Prompt 组装决定了模型能不能把信息有效利用起来。三个阶段互相约束:抽帧抽得再多,帧预算装不下也是白搭;帧预算再宽松,Prompt 组织混乱,模型也容易答非所问。

如果把这段链路类比成数字信号处理:视频是连续信号,抽帧就是采样,帧预算就是量化精度,Prompt 组装就是编码。采样率太低会丢失细节,量化精度不够会引入噪声,编码方式不对则整个系统无法工作。工程上的优化,本质就是在采样率、量化精度和编码策略之间找到平衡点。

2. 抽帧策略:不同视频类型怎么选帧,才算把钱花在刀刃上

2.1 均匀抽帧:最省事的基线方案,但别无脑用

均匀抽帧是最常用的基线方案,做法就是每隔固定间隔抽取一帧,比如每秒抽 1 帧、每 3 秒抽 1 帧、每 30 帧取 1 帧。用 ffmpeg 实现非常简单:

# 每秒抽 1 帧,保存为 jpg 序列
ffmpeg -i input.mp4 -vf "fps=1" -q:v 3 frames/f_%04d.jpg

其中 -q:v 是 JPEG 质量参数,3 表示高质量压缩,对后续识别更友好。如果把 fps=1 改成 fps=0.5 ,就是每 2 秒抽 1 帧。

均匀抽帧最大的优点是“可预估”:视频时长乘以目标帧率,就能得到总帧数,方便提前算帧预算。它也是最不容易漏掉信息兜底策略——无论视频里发生了什么,总能以固定时间间隔捕捉到一部分画面。

但它的缺点同样明显。第一是冗余,一段人物访谈或监控画面,可能连续很多秒画面几乎不变,每一帧都在白白消耗 token;第二是错过关键瞬间,快速运动场景里,均匀抽帧极可能避开了动作最关键的节点,尤其对于需要识别“起跳瞬间”“碰撞瞬间”的任务,每秒 1 帧完全不够。我的建议是:均匀抽帧只作为基线,配合去重和关键帧补充,而不是最终方案。

2.2 场景切换与运动感知抽帧:内容驱动的进阶做法

解决冗余问题,最直接的手段是“内容感知抽帧”。最常用的技术是场景切换检测:通过计算相邻帧之间的像素差异、直方图差异或内容差异,找到镜头切换点,然后在每个场景内部选 1 到 2 帧作为代表。这样既保证覆盖所有场景,又避免大量相似帧占据预算。

用 PySceneDetect 可以轻松实现:

from scenedetect import detect, ContentDetector

# 检测场景切换,threshold 越高,场景切分越宽松
scene_list = detect("input.mp4", ContentDetector(threshold=27.0))
for scene in scene_list:
    print(scene[0].get_seconds(), scene[1].get_seconds())

得到场景起止时间后,可以在每个场景的中间时间点抽帧。实际操作中我会再叠加一层运动感知:对相邻帧计算 SSIM 或光流能量,在运动剧烈的时间段提高抽帧密度。比如一个演示实验的视频,手部快速操作时,光流能量会突增,此时需要多抽;画面静止讲解时,低密度抽帧就够了。

但“内容感知”不等于“越复杂越好”。我在项目中会先做出判断:如果业务目标是全局摘要或简单问答,场景检测加均匀抽帧的组合已经足够;只有任务本身强依赖动作时序或小目标细节时,才值得引入光流、目标检测等更重的模块去定位关键帧。

2.3 任务驱动抽帧:同一段视频,不同任务有不同的最优帧率

工程落地时,最容易被忽略的其实是“任务类型决定抽帧率”。同一个视频,做全局摘要、做细节问答、做字幕 OCR,三者的最优抽帧策略完全不同。我用一个表格来表示:

任务类型 推荐抽帧密度 原因说明
全局摘要/主题分类 每 3-5 秒 1 帧 只捕捉场景和事件梗概,冗余帧会干扰主题判断
动作时序分析 每秒 2 帧以上 需要捕捉动作起止和顺序,帧率过低会丢失“先A后B”的因果
视频问答(细节型) 先粗后细,两阶段 第一遍低帧率定位,第二遍对相关时间段补抽高帧率
OCR/文字识别 保留高分辨率+多帧 文字可能只持续一瞬间,均匀低帧率容易漏字,全帧缩太小又会糊
目标计数/轨迹追踪 每秒 1-2 帧 + 目标检测辅助 需要识别目标出现和消失的帧,纯均匀抽帧难覆盖完整轨迹

我踩过的一个典型坑,是给一个“视频内容标签”任务用了每秒 2 帧的高帧率。结果模型收到了大量重复画面,不仅上下文迅速被占满,模型还因为在多个近乎相同的帧之间“无所适从”而输出了一些奇怪的标签。后来改成每 4 秒抽 1 帧后,准确率反而提升了约 15 个百分点。核心原因很简单:模型输入的每一帧都要占用注意力的,噪音帧太多,信号容易被淹没。

2.4 实操工具:ffmpeg 与 PySceneDetect 的常用组合

在实际项目中,我习惯用一套固定流程:先用 ffprobe 看视频信息,再均匀抽帧打底,再场景检测补充关键场景帧,最后合并去重。命令和脚本组合如下:

# 1. 获取视频信息
ffprobe -v quiet -print_format json -show_format -show_streams input.mp4

# 2. 均匀每秒抽 1 帧
ffmpeg -i input.mp4 -vf "fps=1" -q:v 3 frames/f_%04d.jpg

# 3. 用 PySceneDetect 得到场景时间点,再对每个场景中间帧单独抽帧
python - <<'EOF'
from scenedetect import detect, ContentDetector
scenes = detect("input.mp4", ContentDetector(threshold=27.0))
with open("scenes.txt", "w") as f:
    for start, end in scenes:
        mid = (start.get_seconds() + end.get_seconds()) / 2
        f.write(f"{mid:.2f}\n")
EOF

# 4. 按时间点抽关键帧
mid=10.50
ffmpeg -ss $mid -i input.mp4 -frames:v 1 keyframes/k_${mid}.jpg

合并以后,我还会用感知哈希做一次去重,把内容极其接近的帧删掉。 imagehash 是很好用的工具,两张图的感知哈希距离小于 5 时,我会保留其中分辨率更高的一张。这套组合兼顾了“均匀覆盖”和“重点捕捉”,目前还没遇到覆盖不了的情况。

3. 帧预算:先算账再编码,别等报错了再救火

3.1 一帧到底消耗多少 token:不同模型的差异比你想象的大

帧预算是多模态视频理解工程里最硬核的约束。很多人只知道“图片会消耗 token”,但不知道不同模型对一张图消耗的 token 差距可以达到数倍甚至十数倍。原因是不同模型的视觉编码器设计完全不同:有的把图像固定缩放到某几个档位,有的按长宽动态分块,有的通过专门的视觉塔压缩信息。

以这几个常见模型的思路为例:

模型/方案 单帧图像 token 量级 特点
方案A(按最大长边缩放) 约 100-800 图像越小 token 越少,但有最低保底
方案B(动态分辨率分块) 约 20-2000 分辨率越高 token 越多,细节越好
方案C(视频 API 内置抽帧) 每帧换算成若干 token 内部抽帧,对调用方不可控
方案D(先降采样后编码) 约 30-300 适合小图理解,大图细节丢失严重

所以,做项目时千万不要照搬别人的“一张图 300 token”经验,正确做法是实测:拿一张典型的 1024x768 或 720p 图片,发给同一个模型接口,让它输出一段固定长度的文本,然后从 API 返回的 usage 字段里反推“输入文本 token”和“图像 token”的差值。这个实验成本很低,却能让你对帧预算有一个真实的基线。

3.2 帧预算的计算公式与分配方法

理解了图像 token 消耗后,帧预算的核心公式就一行:

总上下文窗口 = 文本指令 token + 每帧图像 token × 帧数 + 时间戳/文字说明 token + 输出 token

我用一个实际场景举例:假设某模型的上下文窗口为 128K,设置最大输出 token 为 8K,系统指令和用户指令约 4K,时间戳等附加文本约 2K,那么图像部分可用 token 约为:

128000 - 8000 - 4000 - 2000 = 114000 token

如果实测单帧图像 token 是 256,那最多可以容纳约 445 帧;如果是 512,则只能容纳约 222 帧;如果模型动态分辨率在 1024 尺寸下每帧高达 1024 token,则只有约 111 帧空间。这就是为什么同一个视频,在不同模型上的“可接受帧数”完全不同。

实操中我会给帧预算留 10%-20% 的缓冲,不会把图像 token 算到顶,因为模型输出如果意外变长,很容易在推理中途报超限错误。一个更稳妥的分配比例是:图像部分占上下文窗口的 70% 左右,文本指令加附加信息占 10%,输出预留 20%。如果某次任务必须塞更多帧,我会优先压缩图像分辨率,而不是硬塞更多低质量帧。

3.3 超长视频的三种预算扩展方案:分段、滑动窗口与层级摘要

当视频时长非常长,比如 30 分钟、60 分钟甚至数小时,任何单次调用都无法装下所有帧。这时候我有三套方案:

第一套是分段处理,也叫“map-reduce”思路。先把视频按时长切成若干片,每段独立抽帧、独立调用模型生成摘要,再把所有段落摘要拼接,进行第二次汇总。优点是每条请求的帧预算可控;缺点是跨段的因果信息可能丢失。解决方法是让分段之间有少量重叠,比如每段重叠 10 秒左右。

第二套是滑动窗口。类似视频理解里常见的时序建模思路,窗口长度固定,每次向后滑动 50% 或 75%,保证事件边界不会恰好被切断。代价是重复计算量增大,适合对完整性要求极高的场景,比如安全合规审查。

第三套是层级摘要。第一层用低帧率抽帧,让模型生成“粗粒度事件时间线”;第二层根据时间线里的事件区间,定向补抽高帧率帧,再进行细节确认;第三层把所有事件摘要合并成最终报告。我自己最常用的是这套方案,它能把“帧预算”这个硬约束,转化为“先看全貌、再聚焦关键区域”的多轮交互,效果稳得多。

4. Prompt 组装:如何把“抽帧结果”变成模型看得懂的任务输入

4.1 多模态消息结构与图片序列的组织方式

抽帧完成、预算确认后,就到了最影响效果的一步:Prompt 组装。多模态接口的消息结构,一般不是“图片 + 一段文字”那么简单的拼接,而是把内容和图片按顺序放入同一个 content 数组里。以常见的接口风格为例:

[
  {
    "role": "user",
    "content": [
      {
        "type": "text",
        "text": "下面按时间顺序展示一段视频的抽帧,请分析主要事件。"
      },
      {
        "type": "image_url",
        "image_url": {
          "url": "data:image/jpeg;base64,{frame_base64}"
        }
      },
      {
        "type": "text",
        "text": "时间戳:00:00:01"
      },
      {
        "type": "image_url",
        "image_url": {
          "url": "data:image/jpeg;base64,{frame_base64}"
        }
      },
      {
        "type": "text",
        "text": "时间戳:00:00:02"
      }
    ]
  }
]

组装时有三个细节必须注意。

第一,任务指令要放在图片序列之前或之后,不要夹在大量图片中间。我见过很多人把指令写在十几张图的中间,模型在看到指令前已经处理了大量视觉信息,弱化了指令对推理的引导作用。更稳妥的做法是:把任务目标、输出格式、约束条件写在所有图片之前,图片之后只留一句简短的“请开始分析”。

第二,每张图片最好带一个清晰的时间标记。文本形式的时间戳可以放在图后,但如果接口对“图与文本顺序”的绑定不确定,我更推荐直接把时间戳“画”到图片上。

第三,base64 编码会让图片体积增加约 33%。如果一次请求携带 50 张图、每张图 500KB,直接就是几十 MB 的请求体,传输和解析都会有明显延迟。我在工程里会把帧先压缩到合适的分辨率和质量,再编码传输,避免超时。

4.2 时间戳与上下文注入:给模型一张“时间轴”

模型看帧时,最缺的不是画面内容,而是时间感知。它不知道这一帧出现在视频的第几秒,不知道帧与帧之间隔了多久,也不知道整个视频的时长边界。如果缺少这些信息,模型就无法回答“事件发生在什么时候”这类问题,也容易把不同时间段的事件混为一谈。

文本时间戳是最轻量的做法。组装时,我会在每张图片后面插入 [00:00:01] 这样的标记,并且在 Prompt 中补充一句话:“帧序列严格按时间顺序排列,时间戳标注在每帧之后,请以它为时间轴。如果某些事件的时间需要估计,请基于此时间轴推断。”

但更可靠的做法,是在抽帧阶段就用 ffmpeg 的 drawtext 滤镜把时间戳直接写入图像:

ffmpeg -i frame_0001.jpg -vf "drawtext=text='%{pts\:hms}':fontsize=36:fontcolor=white:box=1:boxcolor=black@0.5:x=20:y=20" -q:v 2 tagged_0001.jpg

这样无论模型是否严格解析文本顺序,都能通过图片本身看到时间信息。另一个工程习惯是生成一个 frames.json 清单,记录每帧文件名、时间戳、所属场景,随请求一起发给模型或留给下游做对齐分析。清单一举两得,既服务于 Prompt 组装,也服务于结果回溯。

4.3 实战模板:视频摘要、事件检测与问答三类 Prompt

我把实际项目中常用的三类 Prompt 模板整理出来了,可以直接套用。

视频摘要模板:

你是一个视频理解助手。下面是一段视频的抽帧序列,视频总时长约 {duration} 秒,
按时间顺序排列,每帧对应的时间戳已标注。请完成以下任务:
1. 总结视频的核心内容;
2. 列出出现的场景和人物;
3. 提取主要事件及大致时间段。
要求:用中文输出,不超过 300 字。

事件检测模板:

给定候选事件标签:{candidate_labels}。
请根据抽帧序列判断哪些事件确实出现在视频中,并给出每个事件的开始和结束时间。
输出 JSON 格式:{{"events": [{{"event": "标签名", "start_sec": 12, "end_sec": 45}}]}}。
如果没有检测到任何事件,返回 {{"events": []}}。

细节问答模板:

用户问题:{question}
请根据抽帧序列回答以下问题。若你能确定答案,请在回答后附加相关时间戳;
若画面无法支持回答,请明确说“无法判断”,不要猜测。

这三个模板的共同点是:任务目标明确、输出格式固定、时间语义齐全。尤其是第三个模板,对幻觉的抑制效果非常明显,因为它在开头就明确告诉模型“能判断就判断,不能判断就直说”,降低了后续输出解析的脏数据比例。

4.4 输出约束:让模型稳定产出结构化 JSON

仅仅让模型返回“一段话”对工程化往往不够。我更希望输出是结构化的,比如 JSON,这样下游可以直接入库或进入业务逻辑。为了稳定拿到 JSON,我在 Prompt 里会加几条约束:

  • 明确写明“只输出 JSON,不要包含任何其他文本或 Markdown 代码块标记”;
  • 给出字段名和示例值,比如 start_sec 用数字秒, summary 用字符串;
  • 对枚举字段,把可选标签全部列出来,避免模型自造词;
  • 如果模型偶尔输出多行解释,我会在代码里做一次 JSON 提取兜底,用正则找到第一个 { 到最后一个 } ,再交给解析器。

我实测过,加了“只输出 JSON”的指令后,单次调用返回合法 JSON 的概率能从大约 70% 提升到 95% 左右。剩余 5% 的脏结果,再靠重新请求或文本解析兜底。这样整个视频理解的输出链路才能自动化运行,而不是每次都要人工看一遍模型输出。

5. 实测踩坑实录:抽帧、预算与 Prompt 组装中的高频问题

5.1 最常见的五个报错与排查思路

我在项目中整理过一个报错排查表,命中率非常高,也许能帮你少走弯路。

现象 常见原因 排查与解决思路
请求报上下文超限 帧数 × 单帧 token + 输出 token 超过窗口 降低抽帧率、压缩图像、改用分段方案
Prompt 被内容安全策略拦截 文本中混入异常字符、base64 数据损坏、图像编码异常 检查文本内容;重新用标准 MIME 编解码图像
模型答非所问 指令被放在大量图片中间 / 时间信息缺失 把任务指令提到图片之前,加上时间戳或剪裁框
输出不稳定或返回空 输入帧数量过多导致视觉信息被压缩、覆盖 减小单次图片数量,分批处理
请求超时 单次请求携带多张高清 base64 图 压缩图片、缩小尺寸、并发预加载

其中最隐蔽的是图片编码问题。我曾经遇到一个接口反复报内容审核失败,最后排查发现是某几张抽帧图片的 base64 字符串在拼接时被截断,导致解码出的图片数据异常。整体上,报错未必是模型能力问题,更多时候是组装层的脏数据。

5.2 容易被忽视的相似帧、小字与顺序问题

第一个问题是相似帧浪费预算。静态访谈节目中,人物讲话的十几秒内画面几乎不变,均匀抽帧会抽出一串近似帧。模型不仅浪费 token,还可能因为重复画面过多,忽略掉后续真正有变化的帧。我一般用感知哈希去重:计算每帧的 imagehash ,与前一帧相似度极高时丢弃当前帧。这样可以把一小时的访谈视频,从均匀抽帧的 3600 帧压缩到几百帧,同时不影响信息覆盖。

第二个问题是小字识别不清。视频里的字幕、PPT 上的小字、表格里的数字,如果整帧被缩到 768 宽,细节基本就没了。解决办法是两阶段抽帧:先用低分辨率帧定位文字出现的区域,再针对该区域用 crop 滤镜切出高分辨率局部图,单独送入模型。我做过一个视频会议纪要项目,用这个方案把字幕内容的准确率提升了一倍以上。

第三个问题也很常见:模型会漏看某些帧。多模态模型对超长图片序列的注意力并不均匀,排在中间位置的图片很容易被弱化。我的应对方式是:将图片序列拆成多个更小的批次,每个批次只放 10-20 帧,分批推理后汇总结果;或者在图片上叠加时间戳和序号,在 Prompt 里明确要求“按时间戳上下文理解,不要遗漏任何帧”。

5.3 提高视频理解稳定性的几条实操习惯

在我自己的落地项目里,有几个习惯已经固化成流程了,每次都能显著提升稳定性。

第一,建立“抽帧配置-任务-效果”的小型对照测试集。我会准备三四段不同内容类型的测试视频,每段标一个可度量的任务,比如“找出所有出现人物的时间段”。每次调整抽帧率、是否加场景检测、是否加时间戳水印,都在这个测试集上跑一遍,记录准确率和 token 消耗,而不是凭感觉上线。

第二,对视频先做内容分类,再定制策略。访谈、讲座这类静态内容,均匀低帧率加去重就够了;体育、实验演示、监控这类动态内容,则必须加运动感知或关键帧补充。不同场景强套同一套抽帧参数,是最容易翻车的做法。

第三,大任务永远用“多轮渐进”代替“一次塞满”。先用低帧率让模型生成事件时间线,再针对重点时间段补抽高帧率帧,最后让模型基于补充帧输出细节结论。这个模式相当于把长视频理解从“一次大要求”拆成“多次小询问”,既能让帧预算保持可控,又能让模型在每一轮聚焦更少的信息量,输出质量和稳定性都会高很多。

在做下一个视频理解项目时,我会建议你从三个问题开始思考:这个任务真正需要看到视频里的什么细节?当前模型允许我消耗多少视觉 token?我该用怎样一组帧和指令,让模型在预算内看到最关键的画面?想清楚这三件事,比盲目堆更多帧、更长的 Prompt 要有效得多。

Logo

邀请您加入社区

更多推荐