
MMBench
用 MMBench-GUI 接入并评测自定义 GUI 智能体模型
面向需要把自研或第三方 GUI 智能体接入 MMBench-GUI 评测流程的开发者:讲清四阶段推理管线的设计思路、代码目录结构、数据下载与目录约定,并以本地部署模型为例,逐步实现提示词构建、预处理、推理与后处理函数,最后跑通一次完整评测。
MMBench-GUI 是一套面向 GUI 智能体的分层评测框架,把评测任务拆成内容理解、元素定位、任务自动化、任务协作四个层级,并统一在同一套推理与打分流程里。它的核心设计目标是只让使用者改模型侧的一小部分代码:数据加载、推理循环、指标计算都由框架提供,你只需要把自家模型的提示词格式、输入预处理、输出解析接上去。这篇教程讲的就是这件事——如何把一个自定义模型(本地部署或 OpenAI 风格 API)接入 MMBench-GUI 并跑出评测结果。如果你只是想先了解这类评测工具的整体定位,可以先看 MMBench 的工具页,再回到本文动手。
适合的读者:手上已经有一个能对截图做问答或输出点击坐标的模型,希望用公开基准量化它在 GUI 场景下的表现;或者正在做 GUI 智能体训练,需要一个可复现的评测回路。本文不假设你熟悉框架内部实现,但假设你能读懂 Python,并且有跑通一次模型推理的算力或 API 额度。
准备工作
环境与依赖
框架在仓库里提供了两份依赖清单:requirements.txt 是运行所需的基础依赖,requirements/dev_env.txt 记录了开发环境导出的完整包版本明细。建议先按 requirements.txt 建一个干净的虚拟环境,遇到版本冲突时再对照 dev_env.txt 里的版本组合排查。
git clone <MMBench-GUI 仓库地址>
cd MMBench-GUI
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
本地部署路线还需要 transformers 与对应的视觉处理依赖;如果模型侧要用到 qwen_vl_utils 这类视觉信息处理工具,也要一并装好。API 路线则只需要一个兼容 OpenAI 接口的客户端。
数据下载与目录约定
评测数据不随仓库分发,需要单独下载。框架提供了一个下载脚本,通过环境变量 LMUData 指定数据根目录,脚本会自动下载并解压图片与标注文件:
LMUData=/path/of/data python utils/download.py
下载完成后的目录结构大致如下,图片按操作系统平台分目录存放,标注文件放在数据集根目录下:
DATA_ROOT/
|-- MMBench-GUI/
| |-- offline_images/
| | |-- os_windows/
| | |-- os_mac/
| | |-- os_linux/
| | |-- os_ios/
| | |-- os_android/
| | `-- os_web/
| |-- L1_annotations.json
| `-- L2_annotations.json
注意 LMUData 的默认值沿用 VLMEvalKit 的约定,如果你之前已经用同一套工具链下过数据,可以直接复用同一个根目录,避免重复占用磁盘。图片文件名是长哈希串,不要手动改名,标注文件里的 image_path 字段依赖这个命名。
先读两个示例
动手写自己的模型封装之前,强烈建议先把仓库里已有的两组示例读一遍,它们分别覆盖两条接入路线:
- 本地部署示例:
models/local_uitars.py配合configs/config_local_uitars.json - API 调用示例:
models/api_uitars.py配合configs/config_api_uitars.json
最省事的做法是把这两组文件复制一份、改名,然后在副本上改函数体,而不是从零新建。这样配置字段、注册方式、参数传递都能保持一致。
操作步骤
第一步:理解四阶段推理管线
数据从 dataloader 流出之后,推理被拆成四个阶段,每个阶段对应一个可替换的函数:
- custom_build_prompt:把原始数据行、系统提示词、用户提示词组织成统一的
messages字典列表。 - preprocess_func:把
messages进一步转成模型真正吃进去的输入。走transformers路线时,输出是input_ids这类 token 张量;走 OpenAI 风格 API 时,输出是包含输入内容与生成配置(例如 temperature)的 payload。 - generate:调用模型的
generate方法,或调用client.chat.completions.create,得到原始预测输出。 - postprocess_func:从原始输出里抽取真正要评分的回答内容。
四个阶段跑完之后,框架用 our_benchmark.evaluate 对回答打分。除此之外,你还需要提供一个 parse_response_func,负责从回答里解析出关键信息(例如选项字母、坐标框)。
这里有个容易被忽略的好消息:框架的基类与各层基准里已经给了这四个函数的默认实现。如果你的模型没有特殊的提示词格式或输出格式,这四个函数一个都不用写,直接复用默认实现即可。只有格式确实不同的时候,才需要覆盖对应函数。函数名本身没有严格限制,框架关心的是你在配置里指向了哪个可调用对象。
第二步:认清代码结构
仓库按职责分目录,改动基本集中在 models/ 与 configs/ 两处:
MMBench-GUI/
|-- benchmarks/ # 各层级基准与指标
| |-- l1_content_understanding.py
| |-- l2_element_grounding.py
| |-- l3_task_automation.py
| |-- l4_task_collaboration.py
| `-- matrics.py
|-- models/ # 模型封装放这里
| |-- base.py # 模型包装基类,覆盖 API 与本地两种
| |-- api_gpt.py
| |-- api_uitars.py # API 路线示例
| |-- local_aguvis.py
| `-- local_uitars.py # 本地路线示例
|-- utils/
| |-- download.py # 数据下载与解压
| |-- inference_tools.py # 推理循环,含 API 与本地两种
| `-- misc.py
|-- configs/ # 模型配置放这里
|-- requirements/
| `-- dev_env.txt
|-- requirements.txt
`-- evaluate.py # 评测入口
记住两个约定:模型代码进 models/,配置进 configs/,评测统一从 evaluate.py 启动。推理循环在 utils/inference_tools.py 里,一般不需要改。
第三步:新建模型文件
以本地部署路线为例,先建一个空的模型文件:
cd MMBench-GUI
touch models/local_uitars.py
实际接入时把文件名换成你自己的模型名,例如 models/local_myagent.py,避免和示例文件混淆。
第四步:实现提示词构建函数
不同模型的差异,多数情况下集中在系统提示词、用户提示词的组织方式,以及结果解析上。所以第一个要写的通常是提示词构建函数。它接收两样东西:来自 dataloader 的一行原始数据 line,以及基准名称 dataset。
dataset 的取值是四个固定字符串之一,用来区分不同任务该用哪套提示词:GUIElementGrounding、GUIContentUnderstanding、GUITaskAutomation、GUITaskCollaboration。
先看看两种典型数据行长什么样。L1 内容理解的数据行包含索引、图片路径、问题、选项字典、答案、解释、难度、图片尺寸、平台与应用名:
line = {
"index": 0,
"image_path": "os_ios/9e304d4e_...png",
"question": "Based on the navigation elements, what can be inferred about the current screen's position in the app's hierarchy?",
"options": {
"A": "It's a sub-screen within a 'Rings' section",
"B": "It's the main dashboard of the app",
"C": "It's a sub-screen within the 'Summary' section",
"D": "It's a standalone 'Awards' page accessible from anywhere",
"E": "It's the 'Sharing' section of the app"
},
"answer": "C",
"explanation": "...",
"difficulty": "easy",
"image_size": [1179, 2556],
"platform": "os_ios",
"app_name": "Fitness"
}
L2 元素定位的数据行则包含指令、归一化坐标框、数据类型、平台、应用名与定位类型:
line = {
"index": 0,
"image_path": "os_windows/0b08bd98_...png",
"instruction": "The downward arrow button allows you to scroll down through the list of years.",
"bbox": [0.3875, 0.1361, 0.3945, 0.1507],
"image_size": [2560, 1440],
"data_type": "icon",
"platform": "os_windows",
"app_name": "calendar",
"grounding_type": "basic"
}
构建函数的返回值必须是 list[dict],每个元素形如 {'role': ..., 'type': 'image' 或 'text', 'value': ...}。下面是一个按任务类型分支的骨架:
import os
def build_custom_prompt(line, dataset):
msgs = []
tgt_path = os.path.join("path/to/image/dir", line["image_path"])
if dataset == "GUIElementGrounding":
instruction = line["instruction"]
msgs.append({"role": "user", "type": "image", "value": f"{tgt_path}"})
msgs.append({"role": "user", "type": "text",
"value": GROUNDING_user_prompt.format(instruction=instruction)})
return msgs
elif dataset == "GUIContentUnderstanding":
instruction = line["instruction"]
msgs.append({"role": "user", "type": "image", "value": f"{tgt_path}"})
msgs.append({"role": "user", "type": "text",
"value": QA_user_prompt.format(instruction=instruction)})
return msgs
else:
pass
示例里只自定义了用户提示词,系统提示词沿用模型自带的默认版本。如果你的模型需要显式的系统提示词,在 msgs 最前面插入一条 role 为 system 的文本消息即可,后续预处理函数会识别它。
第五步:实现预处理函数
预处理函数把上一步的 messages 转成模型输入。它接收四个参数:message(消息列表)、model(模型包装器,本地或 API 两种之一,用来取模型侧变量或方法)、processor(transformers.AutoProcessor 实例,用来取处理相关的变量或方法)、以及 **kwargs(配置里 models.your_model_name.kwargs 下写的参数)。
输入消息的形态如下,系统提示词是可选的:
message = [
dict(role='system', type='text', value='You are an agent.'),
dict(role='user', type='image', value='path/to/your/image.png'),
dict(role='user', type='text', value='your user prompt')
]
一个典型的实现会先判断首条消息是不是系统提示词,是就单独抽出来,然后把剩余消息里的图片与文本拼成 content,最后套用 chat template:
def preprocess_uitars(message, model, processor, **kwargs):
from qwen_vl_utils import process_vision_info
messages = []
if message[0]["role"] == "system":
messages.append({"role": "system", "content": message[0]["value"]})
message = message[1:]
messages.append({
"role": "user",
"content": prepare_content(message, processor, **kwargs)
})
text = processor.apply_chat_template(messages, tokenize=False)
# 后续按模型要求调用 process_vision_info 与 processor,返回 BatchFeature
return inputs
返回值的类型是 dict 或 BatchFeature,也就是 processor 的输出。图片与文本如何拼成 content,各模型差异很大,示例里封装成了 prepare_content,具体写法参考 models/local_uitars.py。
第六步:实现后处理与结果解析
后处理函数负责把模型原始输出变成干净的最终回答,解析函数负责从回答里抽出评分需要的关键信息。这两步的写法完全取决于你的模型输出格式:如果模型会输出带思考过程的文本,后处理要负责截取最终答案;如果模型输出的是坐标,解析函数要负责把坐标从文本里还原成数值并做归一化。
如果模型的输出格式和示例模型一致,这两步可以直接复用默认实现,不必重写。
第七步:写配置文件
在 configs/ 下新建一份 JSON 配置,指向你刚写的模型类与四个函数,并填好模型路径、生成参数等。字段结构照抄 config_local_uitars.json 或 config_api_uitars.json,只改与你的模型相关的值。生成参数(例如 temperature)写在配置里,会通过 kwargs 一路传到预处理与推理阶段。
第八步:启动评测
一切就绪后,从 evaluate.py 启动评测,指定你的配置与要跑的基准层级:
python evaluate.py --config configs/config_local_myagent.json
具体可用的命令行参数以仓库当前版本为准,不同层级的基准可以分开跑,便于定位问题出在内容理解、元素定位还是任务执行环节。
一个完整示例
下面把本地部署路线的接入过程串成一个最小可跑通的例子,假设模型名为 myagent。
1. 准备环境与数据
git clone <MMBench-GUI 仓库地址>
cd MMBench-GUI
pip install -r requirements.txt
LMUData=/data/lmu python utils/download.py
2. 复制示例文件
cp models/local_uitars.py models/local_myagent.py
cp configs/config_local_uitars.json configs/config_local_myagent.json
3. 改提示词构建函数,按任务类型分支,图片路径用 os.path.join 拼上数据根目录:
def build_custom_prompt(line, dataset):
msgs = []
tgt_path = os.path.join("/data/lmu/MMBench-GUI/offline_images", line["image_path"])
if dataset == "GUIElementGrounding":
msgs.append({"role": "user", "type": "image", "value": tgt_path})
msgs.append({"role": "user", "type": "text",
"value": GROUNDING_user_prompt.format(instruction=line["instruction"])})
elif dataset == "GUIContentUnderstanding":
msgs.append({"role": "user", "type": "image", "value": tgt_path})
msgs.append({"role": "user", "type": "text",
"value": QA_user_prompt.format(instruction=line["instruction"])})
return msgs
4. 改预处理函数,处理可选系统提示词并套用 chat template,返回 processor 的输出。
5. 改配置,把模型类指向 models.local_myagent 里的类,把四个函数名指向你实现的版本,填好本地模型权重路径与生成参数。
6. 跑评测
python evaluate.py --config configs/config_local_myagent.json
如果跑 L1 内容理解时结果明显偏低,先检查提示词里的选项是否完整传入、图片路径是否拼对;如果跑 L2 元素定位时坐标全部解析失败,先检查后处理是否把坐标文本完整保留了下来。
注意事项
- 四个函数不是必须全写。 框架基类与各层基准已提供默认实现,模型格式与示例一致时可以直接复用,少写就少错。
- 函数名不受限制。 框架通过配置里的引用找到函数,名字叫什么都行,关键是配置指向正确。
- 不要把模型文件或外部代码直接塞进项目。 框架刻意避免这种做法,本地部署走
transformers.from_pretrained这类通用接口,API 走client.chat.completions.create,这样大部分代码才能复用。 - 图片路径必须拼对。 标注里的
image_path是相对路径,需要和LMUData下的offline_images目录拼接,拼错会导致整批图片读不到。 - 生成参数走配置。 temperature 之类的参数写在配置的
kwargs里,会传到预处理与推理阶段,不要硬编码在函数内部。 - 分层跑评测。 四个层级考察的能力不同,混在一起跑不利于定位问题,建议先跑 L1、L2 这类相对简单的层级验证接入是否正确,再跑任务自动化与协作层级。
- 版本与配额以官网为准。 依赖版本、数据规模、API 调用额度等信息会随仓库更新变化,动手前请以仓库与模型服务方的当前说明为准。