AB
AiBoss站
教程

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 流出之后,推理被拆成四个阶段,每个阶段对应一个可替换的函数:

  1. custom_build_prompt:把原始数据行、系统提示词、用户提示词组织成统一的 messages 字典列表。
  2. preprocess_func:把 messages 进一步转成模型真正吃进去的输入。走 transformers 路线时,输出是 input_ids 这类 token 张量;走 OpenAI 风格 API 时,输出是包含输入内容与生成配置(例如 temperature)的 payload。
  3. generate:调用模型的 generate 方法,或调用 client.chat.completions.create,得到原始预测输出。
  4. 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 调用额度等信息会随仓库更新变化,动手前请以仓库与模型服务方的当前说明为准。