03 · 任务定义与执行式判分
这一章讲 OSWorld 最有辨识度的一半:任务不是代码而是声明式 JSON;判分不看 agent 自述,而是进真机取证再按规则比对(execution-based evaluation)。读完你能看懂任意一个任务 JSON,并知道它最后是怎么变成一个 0~1 的分数。
3.1 一个任务就是一份 JSON
369 道题(10 个 domain)每题一个 JSON,放在 evaluation_examples/examples/<domain>/<id>.json。test_all.json 是总清单({domain: [id,...]},共 369 项)。任务数分布:
| domain | 题数 | domain | 题数 |
|---|---|---|---|
| multi_apps | 101 | os | 24 |
| libreoffice_calc | 47 | vs_code | 23 |
| libreoffice_impress | 47 | libreoffice_writer | 23 |
| chrome | 46 | vlc | 17 |
| gimp | 26 | thunderbird | 15 |
一个任务 JSON 的骨架(字段取自 evaluation_examples/examples/libreoffice_calc/1954cced-...json):
| 字段 | 作用 |
|---|---|
id | 任务唯一 id(也是结果目录名) |
snapshot | 该题基于哪个 VM 快照 |
instruction | 给 agent 的自然语言指令 |
config | 初始状态布置:一串 setup 动作(reset 时执行) |
related_apps | 涉及哪些软件 |
evaluator | 怎么判分:func + result + expected + options + postconfig |
proxy / possibility_of_env_change | 是否需代理、环境易变度等元信息 |
那道透视表任务的 config 就两步:download 下载 Invoices.xlsx,再 open 用 LibreOffice 打开它——干净利落地把「初始文件」摆好。
3.2 setup:声明式地布置初始状态
config 里每一项形如 {"type": "download", "parameters": {...}}。SetupController.setup(controllers/setup.py:57)按 type 拼出方法名 _{type}_setup 反射调用(setup.py:92)。常用 setup 动作 :
| type | 干什么 | 方法 |
|---|---|---|
download | 从 URL 下文件到 VM(带缓存、重试) | _download_setup (setup.py:108) |
open | 用默认程序打开某文件 | _open_setup (setup.py:280) |
launch | 起一个进程/命令 | _launch_setup (setup.py:300) |
execute | 在 VM 里跑一条命令 | _execute_setup (setup.py:324) |
chrome_open_tabs | 预置浏览器标签页 | _chrome_open_tabs_setup (setup.py:579) |
login | 预登录某服务 | _login_setup (setup.py:753) |
sleep | 等一会(等界面稳定) | _sleep_setup (setup.py:457) |
_download_setup 有个务实设计:先按 URL 的 uuid5 在本地 cache_dir 找缓存,命中就不重下(setup.py:120-126),失败重试 3 次。跑一次全量榜要拉大量初始文件,缓存能省掉重复下载。
3.3 判分的心法:去真机取证,而非信自述
OSWorld 判分的核心信念是:agent 说完成了不算,去系统里查最终状态才算。 这落成 DesktopEnv.evaluate()(desktop_env.py:458)里一套「getter 取证 + metric 打分」的两段式:
evaluate()
① 跑 evaluator.postconfig(收尾动作,如 Ctrl+S 保存文件) desktop_env.py:463
② result_getter(self, result_config) → 进真机把「实际产物」取回 host(如下载改后的 xlsx)
③ expected_getter(self, expected_cfg) → 取回「标准答案」(如云端 golden 文件) [可选]
④ metric(result_state, expected_state, **options) → 比对,返回 0~1
getter = 取证员,metric = 判卷员。 二者在 evaluator 里用字符串名字声明,_set_evaluator_info(desktop_env.py:369)用反射把名字变成函数:
metric = getattr(metrics, func)(desktop_env.py:380)——如compare_table。result_getter = getattr(getters, "get_" + result["type"])(desktop_env.py:385)——如 type=vm_file→get_vm_file。
所以那道透视表任务的 evaluator(func: compare_table、result 是 VM 上的 Invoices.xlsx、expected 是云端 golden xlsx)判分流程就是:
postconfig 里先 Ctrl+S 存盘
→ get_vm_file 把 VM 里改后的 Invoices.xlsx 拉回 host
→ get_cloud_file 下载标准答案 xlsx
→ compare_table(实际, 标准, rules=[{type:pivot_table,...}]) → 0/1
compare_table(metrics/table.py:260)用 openpyxl/pandas 把两个 xlsx 读进来,按 options.rules 里的一条条规则(sheet 名、透视表结构、单元格值…)逐条比对。这是「执行式校验」的典型:不解析 agent 说了什么,只解析它产出的真实文件对不对。
3.4 getter 有哪些「取证」手段
getter 覆盖了「怎么从真机/真软件里把可判分的证据取回来」的各种方式(evaluators/getters/):
| getter 类型 | 取什么证据 | 文件 |
|---|---|---|
get_vm_file | VM 上的某个文件(拉回 host) | getters/file.py |
get_cloud_file | 云端标准答案文件 | getters/file.py |
get_vm_command_line | 在 VM 里跑命令、拿 stdout | getters/general.py |
get_rule | 直接把 JSON 里写死的期望值当证据 | getters/misc.py |
get_open_tabs_info / get_history… | Chrome 的标签页/历史/书签 | getters/chrome.py |
get_accessibility_tree | 当前无障碍树 | getters/misc.py |
配对的 metric 也按软件/文件类型分门别类(evaluators/metrics/):compare_table/compare_csv(表格)、compare_docx_lines(Word)、compare_pdfs(PDF)、is_expected_tabs(浏览器)、check_include_exclude(命令输出含/不含某串)等。每个 metric 都是一个纯函数 (result[, expected], **options) -> float(Metric 类型别名见 desktop_env.py:21)。
3.5 多指标合取:and / or
一道题可以挂多个 metric(func 是列表)。evaluate 按 metric_conj("and"/"or")合取(desktop_env.py:481-509):
and:任一 metric == 0 → 立刻返 0;全过 → 返平均分
or :任一 metric == 1 → 立刻返 1;否则 → 返最大值
设计上 func、result、expected、options 四个列表必须等长一一对应(desktop_env.py:412 的断言),即使某个 metric 不需要 expected 也要在列表里占个 None。这让「一道题=多条独立校验规则」表达得很整齐。
3.6 一类特殊题:infeasible(本就做不到)
OSWorld 里有一批「陷阱题」——任务其实不可能完成,正确行为是 agent 认清现实并回 FAIL。这由 evaluate 开头的特判处理(desktop_env.py:469):
若 evaluator.func == "infeasible":
最后一个动作是 FAIL → 得 1(对:识破了不可行)
否则 → 得 0(错:硬做/瞎报完成)
否则(普通题):
若最后动作是 FAIL → 直接 0(不该放弃)
这把「知道什么时候该放弃」也变成了可打分的能力,专治 agent 的过度自信与幻觉完成。test_infeasible.json 是这类题的清单。
3.7 判分全景一图
env.evaluate()
│
┌────────┴─────────┐
infeasible? 普通题
│ │
末动作FAIL? 末动作FAIL? ──是──▶ 0
是→1 否→0 │否
▼
postconfig(如 Ctrl+S 存盘)
│
result_getter 取真机产物 ──┐
expected_getter 取标准答案 ─┤
▼ │
metric(实际, 标准, options) → 0~1
│(多 metric 时按 and/or 合取)
▼
写 result.txt
至此三条主线(环境执行、agent 决策、任务判分)都讲完了。想看巧妙设计、边界与坑、以及可 grep 的代码地图,见 04-internals-and-map.md。