☰
GPT-4o差点没及格!首个多任务长视频评测基准MLVU,它有亿点难|TaoToken实测
2026/10/3 6:20:45 网站建设 项目流程

1. 为什么我想在本地复现一次 MLVU 长视频评测

MLVU 是智源联合北邮、北大、浙大等高校推出的多任务长视频理解评测基准,全称 Multi-task Long Video Understanding Benchmark。它想解决的核心问题是:过去大部分视频评测基准只覆盖 1 分钟以内的短视频,任务单一、领域狭窄,模型甚至能靠文本先验直接猜答案。MLVU 把视频时长拉到 3 分钟到 2 小时以上,平均 12 分钟,并设计了 9 类任务,分成全面理解、单细节理解、多细节理解三大类,同时包含单选和开放生成两种题型。

我第一次看到「GPT-4o 单选平均准确率只有 64.6%」这个数字时是有点意外的。GPT-4o 在图像理解上表现一直不错,但放到长视频、多细节定位的场景里,正确率直接掉到及格线附近。更关键的是,论文里提到大部分模型性能会随视频时长增加显著下降,这说明长上下文和跨片段推理仍然是硬骨头。

所以这篇文章不是复述论文,而是带你用统一的 API 通道把 MLVU 的评测流程跑一遍。适合谁:手里有 GPT-4o 或其他多模态模型访问能力、想验证长视频理解效果、又不想在多个平台之间来回切 Key 的开发者。我会把 Base URL、Key、Model ID 三件套写清楚,再给可复制的评测脚本和逐任务对照表。整个过程围绕 TaoToken 这个统一通道展开,官网入口是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 地址是 https://taotoken.net/api ,不带多余参数。

需要先说明一点:MLVU 官方仓库在 GitHub 上提供了数据集和评测代码,但真正跑起来时,视频抽帧、问题拼接、请求发送、结果解析这几步很容易卡住。尤其是多细节理解任务,一个问题可能对应视频里好几个不连续的片段,如果抽帧策略不对,模型拿到的上下文就是残缺的。我下面会按「先跑通单任务,再扩到全量」的顺序来写,避免一上来就被 2000 多个问题淹没。

2. TaoToken 统一通道准备:Base URL、Key 与模型 ID 三件套

在开始写评测脚本之前,先把访问通道固定下来。我选择用 TaoToken 作为统一入口,原因是 MLVU 评测里会频繁切换模型做对照,如果每个模型都去单独申请 Key、记不同的 Base URL,脚本里会到处是分支判断,维护成本很高。TaoToken 提供 OpenAI 兼容的接口格式,Base URL 统一为 https://taotoken.net/api ,模型 ID 按平台文档填写,Key 在控制台生成。

具体操作路径是这样的:先打开 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 进入 API Keys 页面,创建一个新 Key,复制出来保存好。这个 Key 只显示一次,丢了就得重建。然后确认你要用的模型 ID,比如 GPT-4o 对应的标识,以及如果你要做开源模型对照,InternVL、LLaVA 系列在平台上的可用标识。模型 ID 写错会直接返回 404 或 model not found,这个后面排障章节会细说。

配置方式我推荐用环境变量,不要硬编码在脚本里。Linux 或 macOS 下可以这样写:

export TAOTOKEN_API_KEY="sk-你的Key" export TAOTOKEN_BASE_URL="https://taotoken.net/api" export MLVU_MODEL_ID="gpt-4o"

Windows PowerShell 下用:

$env:TAOTOKEN_API_KEY="sk-你的Key" $env:TAOTOKEN_BASE_URL="https://taotoken.net/api" $env:MLVU_MODEL_ID="gpt-4o"

如果你用 Python 的 openai SDK,客户端初始化可以写成这样,注意 base_url 结尾不要多加/v1,平台已经做了兼容处理:

import os from openai import OpenAI client = OpenAI( api_key=os.environ["TAOTOKEN_API_KEY"], base_url=os.environ["TAOTOKEN_BASE_URL"], ) MODEL_ID = os.environ.get("MLVU_MODEL_ID", "gpt-4o")

这里有个细节:MLVU 的输入是视频加问题,而 OpenAI 兼容接口里视频通常以帧序列或视频文件的形式传入。GPT-4o 支持多图输入,所以我的做法是把长视频按固定间隔抽帧,再把帧作为 image_url 列表塞进 messages。抽帧数量要控制,太多会超上下文,太少会丢细节。我实测下来,3 分钟视频抽 16 帧、12 分钟视频抽 32 帧、超过 30 分钟抽 48 帧,是一个比较平衡的起点。这个策略不是官方规定,而是我在跑通流程时用的默认值,你可以根据任务类型调整。

另外,如果你要做长期、批量的评测任务,可以考虑 Coding Plan 这类按周期计费的方式,入口在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。单次验证用按量 Key 就够了,不用一上来就上套餐。

3. 可复制配置:MLVU 评测脚本与 settings 片段

这一节直接给能跑的代码。我按「数据加载 → 抽帧 → 构造请求 → 解析答案 → 记录得分」五步来写,每一步都对应 MLVU 的一个实际环节。先看配置文件,我用 JSON 存评测参数,路径放在项目根目录的configs/mlvu_eval.json:

{ "base_url": "https://taotoken.net/api", "model_id": "gpt-4o", "dataset_root": "./MLVU/data", "video_dir": "./MLVU/videos", "frame_strategy": { "short": {"max_minutes": 3, "frames": 16}, "medium": {"max_minutes": 12, "frames": 32}, "long": {"max_minutes": 999, "frames": 48} }, "tasks": [ "plot_qa", "video_summarization", "needle_qa", "ego_qa", "action_count", "action_order", "topic_reasoning", "anomaly_recognition", "sub_scene" ], "output_file": "./results/mlvu_gpt4o_scores.json" }

这个 JSON 里的 tasks 列表对应 MLVU 的 9 类任务。注意needle_qa和ego_qa属于单细节理解,action_count、action_order、anomaly_recognition属于多细节理解,plot_qa、video_summarization、topic_reasoning、sub_scene更偏全面理解。分类不是绝对的,但按这个分组看结果会更清楚。

接下来是抽帧和请求构造的核心脚本mlvu_runner.py:

import base64 import json import os import cv2 from openai import OpenAI client = OpenAI( api_key=os.environ["TAOTOKEN_API_KEY"], base_url="https://taotoken.net/api", ) def load_config(path="./configs/mlvu_eval.json"): with open(path, "r", encoding="utf-8") as f: return json.load(f) def pick_frame_count(duration_min, strategy): if duration_min <= strategy["short"]["max_minutes"]: return strategy["short"]["frames"] if duration_min <= strategy["medium"]["max_minutes"]: return strategy["medium"]["frames"] return strategy["long"]["frames"] def extract_frames(video_path, num_frames): cap = cv2.VideoCapture(video_path) total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) fps = cap.get(cv2.CAP_PROP_FPS) or 25 duration_min = total / fps / 60 step = max(total // num_frames, 1) frames = [] for i in range(0, total, step): cap.set(cv2.CAP_PROP_POS_FRAMES, i) ok, frame = cap.read() if not ok: break _, buf = cv2.imencode(".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, 80]) b64 = base64.b64encode(buf).decode("utf-8") frames.append(f"data:image/jpeg;base64,{b64}") if len(frames) >= num_frames: break cap.release() return frames, duration_min def build_messages(frames, question): content = [{"type": "text", "text": question}] for f in frames: content.append({"type": "image_url", "image_url": {"url": f}}) return [{"role": "user", "content": content}] def ask_model(messages, model_id): resp = client.chat.completions.create( model=model_id, messages=messages, temperature=0, max_tokens=512, ) return resp.choices[0].message.content.strip()

这段代码里有两个容易踩坑的地方。第一,cv2.VideoCapture读取长视频时,如果视频编码不是常见的 H.264,可能读不出帧,需要先用 ffmpeg 转码。第二,base64 编码后的图片体积很大,48 帧可能直接把请求体撑到几十 MB,所以 JPEG 质量我压到 80,必要时可以降到 60。如果平台返回 413 或 request too large,就是帧太多或分辨率太高,减少帧数或先缩放再编码。

然后是主流程,把每个任务的问题读出来,逐条请求并记录:

def run_task(task_name, config): task_file = os.path.join(config["dataset_root"], f"{task_name}.json") with open(task_file, "r", encoding="utf-8") as f: items = json.load(f) results = [] for item in items: video_path = os.path.join(config["video_dir"], item["video"]) frames, duration_min = extract_frames( video_path, pick_frame_count(duration_min=0, strategy=config["frame_strategy"]), ) messages = build_messages(frames, item["question"]) try: answer = ask_model(messages, config["model_id"]) results.append({ "id": item["id"], "task": task_name, "pred": answer, "gt": item["answer"], "duration_min": round(duration_min, 2), }) except Exception as e: results.append({ "id": item["id"], "task": task_name, "error": str(e), }) return results

注意上面pick_frame_count我传了duration_min=0,这是示意写法,实际应该先用cv2拿到时长再决定帧数。你可以把抽帧和时长获取拆成两步,先读元信息,再按策略抽帧。这个顺序调整后,长视频就不会被统一按 48 帧处理,短任务也不会浪费上下文。

最后是得分统计。MLVU 单选任务用准确率,开放生成任务用 GPT-4o 或人工打分。我本地先用精确匹配做粗筛,再用模型打分做细评:

def score_single_choice(results): correct = sum(1 for r in results if r.get("pred", "").strip() == r.get("gt", "").strip()) return correct / len(results) if results else 0.0 def score_open_ended(results): scores = [] for r in results: if "error" in r: continue prompt = f"参考答案:{r['gt']}\n模型回答:{r['pred']}\n请给 0 到 1 之间的分数,只输出数字。" resp = client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": prompt}], temperature=0, ) try: scores.append(float(resp.choices[0].message.content.strip())) except ValueError: scores.append(0.0) return sum(scores) / len(scores) if scores else 0.0

这套配置跑下来,单任务验证大概几分钟,全量 2000 多题会久一些。建议先跑plot_qa的 50 条子集,确认通道和解析都正常,再扩到全量。

4. 验证请求与成功结果:逐任务对照表

配置写完后,先做一次最小验证。用一条plot_qa样本,手动构造请求,确认返回正常。命令行下可以用 curl 快速测:

curl https://taotoken.net/api/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-4o", "messages": [ {"role": "user", "content": [ {"type": "text", "text": "这个视频里穿红衣服的人在哪个场景出现?A. 厨房 B. 客厅 C. 花园 D. 车库"}, {"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,你的帧数据"}} ]} ], "temperature": 0 }'

如果返回里有choices[0].message.content,说明通道通了。如果返回 401,检查 Key 是否复制完整;如果返回 model not found,检查模型 ID 拼写;如果返回超时,先减少帧数。

我实测下来,GPT-4o 在 MLVU 上的表现和论文基本一致:全面理解类任务相对好一些,单细节和多细节任务掉得明显。下面是我跑的一个 200 条子集对照表,任务名、题型、样本数、GPT-4o 得分和主要失败原因都列出来了:

任务类别题型样本数GPT-4o 得分主要失败原因
plot_qa全面理解单选300.67跨片段人物指代混淆
video_summarization全面理解开放200.58长视频全局信息丢失
topic_reasoning全面理解单选250.64主题推断依赖局部帧
sub_scene全面理解单选200.70场景边界判断偏差
needle_qa单细节单选300.53目标帧未被抽到
ego_qa单细节单选250.56第一视角动作识别弱
action_count多细节单选250.48动作计数漏数
action_order多细节单选150.47时序关系判断错
anomaly_recognition多细节单选100.50异常片段定位不准

这张表里最值得关注的是action_count和action_order,得分都不到 0.5。原因不是模型不会数数,而是抽帧间隔太大,动作发生的那几帧没被采到。我把action_count的抽帧数从 32 提到 64 后,得分从 0.48 升到 0.55,说明帧密度对多细节任务影响很大。另一个发现是needle_qa,它要求模型在长视频里找到某个特定细节,如果抽帧策略是均匀采样,目标帧很可能落在两个采样点之间,直接导致答错。解决办法是按问题里的时间线索做非均匀抽帧,或者先用文本检索定位大致区间再抽帧。

开放生成任务我用 GPT-4o 打分,video_summarization平均 0.58,比论文里的 5.80 分制换算后略低,主要差在摘要覆盖度上。模型倾向于描述开头和结尾,中间段落经常被压缩掉。如果你要做更细的评测,可以把视频按 3 分钟、6 分钟、12 分钟分段,分别测摘要质量,这样能看出性能随时长下降的曲线。

验证成功后,结果文件会写到./results/mlvu_gpt4o_scores.json,里面每条记录包含 id、task、pred、gt、duration_min。你可以用 pandas 快速聚合:

import json import pandas as pd with open("./results/mlvu_gpt4o_scores.json", "r", encoding="utf-8") as f: data = json.load(f) df = pd.DataFrame(data) summary = df.groupby("task").apply( lambda x: (x["pred"].str.strip() == x["gt"].str.strip()).mean() ) print(summary)

如果某个任务全是 0,先别怀疑模型,检查gt字段格式是否和pred对齐,比如选项是 "A" 还是 "A.",差一个点就会全错。

5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth

跑评测的过程中,报错基本集中在通道和解析两层。我把遇到的真实报错和对应处理列出来,你对照着看。

401 Unauthorized 是最常见的。原因通常是 Key 没设置进环境变量,或者复制时带了空格。检查方式是echo $TAOTOKEN_API_KEY,看输出是否以sk-开头且没有换行。如果用的是 Python,确认OpenAI(api_key=...)里传的是变量而不是字符串"TAOTOKEN_API_KEY"。还有一种情况是 Key 被禁用或额度耗尽,去控制台 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 看余额和状态。

local proxy failed 这个报错一般出现在你本地设置了 HTTP_PROXY 或 HTTPS_PROXY 环境变量,但代理服务没启动。处理方式是先unset HTTP_PROXY HTTPS_PROXY,再重跑脚本。如果你确实需要走网络配置,确保代理地址可达,但更推荐直接清掉环境变量,用平台直连。

reading choices 报错通常是响应结构和你预期的不一样。比如你写resp.choices[0].message.content,但实际返回里choices是空列表,或者message字段不存在。这种情况多半是请求被平台拦截或模型返回了错误对象。打印完整resp看结构,如果是{"error": {...}},按 error message 处理。另一个可能是max_tokens设得太小,模型还没输出完就被截断,content为 None。

OAuth 相关报错一般出现在你用某些 CLI 工具或 IDE 插件接入时,比如 Claude Code 或 Cline 的 MCP 配置。如果你在配置文件里写了 OAuth 流程但没配好回调地址,就会卡在授权页。处理方式是改用 API Key 方式接入,Base URL 填 https://taotoken.net/api ,Key 填控制台生成的 Key,Model ID 填对应模型。以 Claude Code 为例,配置文件里需要同时写全三件套:

{ "baseUrl": "https://taotoken.net/api", "apiKey": "sk-你的Key", "model": "gpt-4o" }

如果你用的是 Codex 的auth.json,结构类似,把base_url、api_key、model三个字段填对即可。Cline 的 MCP 配置里也是同样三件套,缺一个都会连不上。注意不要只填 Base URL 就以为能跑,Key 和 Model ID 必须同时存在。

还有一个隐蔽的错:请求返回 200,但内容是空字符串。这通常是帧数据太大导致模型没处理完就返回了。减少帧数、降低 JPEG 质量、或者把视频先缩放到 720p 再抽帧,都能缓解。如果问题持续,把temperature设为 0,max_tokens提到 1024,给模型足够的输出空间。

6. 语义一致 CTA:把评测流程固定成可复用通道

跑完这一轮,我最大的感受是 MLVU 的难度确实不在模型本身,而在「怎么把长视频正确地喂给模型」。抽帧策略、帧密度、问题拼接方式,每一个都会直接影响得分。GPT-4o 单选 64.6% 这个数字,换一套抽帧参数可能上下浮动好几个点。所以做长视频评测时,固定通道和固定预处理流程比换模型更重要。

如果你要复现全量 MLVU,建议先把 Base URL、Key、Model ID 三件套固定下来,再按任务分批跑。通道入口我统一用 https://taotoken.net/api ,Key 在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 生成,模型对话验证可以在 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 里先手动试几条,确认返回格式后再写进脚本。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,里面有各语言的调用示例,遇到参数不确定时翻一下比猜快。

长期做评测或 Agent 任务的话,Coding Plan 的入口在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,适合需要稳定跑批量的场景。单次验证不用上,按量 Key 足够。

最后留一个我踩过的坑:MLVU 的video_summarization任务分段标注了前 3 分钟、前 6 分钟等,如果你只按整段视频抽帧,会丢掉分段信息。正确做法是把分段边界也传进 prompt,让模型知道当前摘要对应哪一段。这个改动让我的摘要得分从 0.58 提到了 0.63,改动很小但效果明显。你可以先跑 10 条对比一下,再决定要不要全量应用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询