第 16 章:模拟用户与计算机操作任务
值班交接系统要新增一个设置向导。需求单只写着“按用户要求配置维护窗口”,真正的约束却在用户脑中:时区、执行时间和自动保存策略都没有落在票据里。Agent 如果直接猜,或许能把表单填完,却没有解决用户的问题;如果不断追问,又可能永远不进入执行阶段。进入图形界面后,按钮位置、焦点、窗口大小和动画还会把一次简单修改变成不稳定的视觉控制任务。
本章构建一个可重复的、非对抗式流程基线:Agent 必须先在可见界面里完成三轮澄清,再用鼠标和键盘保存配置,最后显式结束任务。它不调用外部模拟用户模型,因此不增加第二套模型调用;除本地计算资源外,只产生被测模型的调用费用。在此基础上,我们再讨论如何替换成不向 Agent 镜像交付隐藏值的 Simulated User Sidecar,以及如何迁移到 Ubuntu 或 Windows 远程桌面。
读完本章,读者应当能够:
- 区分任务级 Simulated User 配方、
computer-1Agent 和桌面 Environment Provider; - 为多轮对话定义状态、预算和终止协议;
- 构建一个可被截图、鼠标和键盘操作的 Ubuntu GUI Task;
- 用应用状态和终止证据评分,而不是对截图做脆弱的像素匹配;
- 识别 Windows、外部桌面服务、模型能力和费用的真实边界。
16.1 先划清三层边界
在 Harbor v0.18.0 中,“模拟用户”不是一个可以写进 task.toml 的内置对象。该版本 Roadmap 仍把包含 simulated users 的多轮对话评测列为未完成项;官方 Cookbook 的做法是让 Task 自己携带一个 MCP 用户服务。12 因而,本章使用 Simulated User 时,指的是一种任务设计模式,不是 SimulatedUser 类或 Harbor 自动管理的会话生命周期。
同样,“Computer Use”也不等于某一种云桌面。至少要分清以下三层:
| 层 | v0.18.0 中的实例 | 负责什么 | 不负责什么 |
|---|---|---|---|
| Task 交互层 | Cookbook 用户服务、本章确定性用户界面 | 隐藏需求、回复问题、保存会话证据 | 不替 Agent 操作桌面 |
| Agent 层 | 内置 computer-1 | 看截图,产生点击、输入、滚动、拖动、缩放和结束动作 | 不自动创造 Windows VM,也不把 MCP 用户服务变成视觉对话 |
| Environment 层 | docker、use-computer、cua-cloud | 启动任务所在的容器或远程系统并执行命令 | 不保证任意 Agent、OS、镜像组合都兼容 |
computer-1 在 v0.18.0 中是注册的内置 Agent。其构造参数包括桌面与窗口几何、max_turns、start_url、图像开关和 Provider 选择;模型名是必填项。3 它的运行时把动作翻译为 xdotool 命令,终止动作集合是 terminate、done 和 answer,而 bash 默认关闭、只能显式加入 extra_tools。4 这给出两个设计原则:
- GUI 任务应默认只开放视觉控制,不要用 Shell 旁路把它退化为文件编辑题;
- “表单保存成功”和“Agent 已正确终止”是两个不同的验收条件。
computer-1还使用两阶段完成确认:第一次终止动作得到“是否确定”的观察,只有再次提交相同最终答案,harness 才写入final_answer.txt并结束。5
16.2 把模拟用户写成有界状态机
一个只会把 persona 交给模型的 ask_user(question) 工具还不够。评测至少需要记录四类状态:已经披露的约束、对话轮数、终止原因和服务端审计。Cookbook 快照中的示例用 FastMCP 暴露 ask_user,在服务进程内维护消息历史,并通过 ANTHROPIC_API_KEY 调用独立模型;它演示了任务结构,但回复仍会受第二个模型影响。2
更稳妥的协议可以抽象为:
state = {
disclosed: set[requirement_id],
turns: int,
status: continue | requirements_complete | stopped,
stop_reason: complete | max_turns | unsafe_request | service_error
}
ask(question):
reject empty or oversized question
if status != continue: return terminal reply
turns += 1
disclose at most one relevant hidden requirement
if all required facts disclosed: status = requirements_complete
if turns == max_turns: status = stopped
append server-owned audit record
return {reply, status, turns, disclosed_ids}
“一次最多披露一项”不是自然对话的普遍规律,而是评测夹具的控制手段:它阻止 Agent 用一个罗列所有关键词的长问题绕过多轮能力。生产型 persona 可以允许一次回复多项,但必须在 Dataset 元数据里记录策略版本。
终止也要双向定义。模拟用户在需求齐备、超过轮数或遇到越权请求时停止;Agent 只有在界面提交成功后才能发出终止动作,并按 computer-1 的观察再次确认。不要把“模型没有继续说话”当成成功,它也可能是超时、上下文溢出或用户服务崩溃。
注意:LLM 用户和被测 Agent 不能共用未标记的模型调用账单。至少分别记录
agent_model、user_model、两侧 token/费用、重试次数和对话轮数,否则同一 Job 的成本无法解释。
16.2.1 确定性用户与 LLM 用户各自解决什么
确定性状态机适合验证流程能力:Agent 是否发现信息缺口、是否逐项提问、是否在信息齐备后停止提问。它的同一输入必然得到同一回复,容易编写反例,但无法覆盖含糊表达、反问、临时改口和自然语言中的礼貌策略。LLM 用户更接近开放对话,却会同时引入 persona 漂移、事实自相矛盾、意外泄露全部需求和额外模型失败。
| 目标 | 优先选择 | 仍要补的测试 |
|---|---|---|
| 调通 GUI 与终止协议 | 确定性用户 | 同义问法、空问题、重复问题、轮数耗尽 |
| 衡量澄清语言质量 | LLM 用户 | 人工校准、重复运行、persona 遵循率 |
| 回归检查 | 确定性用户为主 | 少量固定的 LLM canary |
| 生产近似实验 | LLM 用户 | 费用上限、服务错误分类、审计与脱敏 |
推荐采用两层 Dataset,而不是在第一天就把所有随机性叠在一起。第一层固定用户和固定界面,失败通常可以定位到 Agent 或 CUA;第二层保持 GUI 不变,只替换用户服务。若第二层退化、第一层稳定,排查范围自然收敛到用户模型、对话协议或网络。
Persona 也应写成数据契约。每条隐藏需求都有稳定 ID、合法取值和允许披露的触发条件;回复模型只能从这些事实生成语言,不能新增需求。遇到提示注入、索取系统提示或要求执行操作时,模拟用户应返回结构化拒绝并记录 unsafe_request,而不是扮演一个拥有额外权限的助手。对同一需求改口是可以专门设计的能力项,但必须在 Task 中显式标记,不能由用户模型偶然产生。
16.3 computer-1 的观察—动作循环
computer-1 每轮接收当前截图并提交一个动作。运行时支持点击、双击、右击、移动、输入、按键、拖动和滚动;zoom 只裁剪下一张截图,随后自动复位。4 每个动作后又获取新截图,所以 GUI 任务的主要费用不是“生成最终答案”这一轮,而是截图—推理—动作往返的总轮数。官方的 Computer Use 指南也把这种逐步截图交互描述为比专用工具更慢的通用途径。6
轨迹并非只有图片。v0.18.0 的 recorder 把动作参数、截图引用和逐步指标写入 ATIF trajectory,并汇总 prompt token、completion token、cache token 和可用时的美元成本。7 因此发布前应报告:
- 成功率和 Reward;
- 中位数及高分位动作轮数;
early_termination_reason的分布;- 输入/输出 token 与费用;
- GUI 基础设施错误比例。
不要只比较总耗时。冷启动桌面、模型思考、截图编码和应用动画都可能贡献延迟,含义并不相同。
16.3.1 Provider 路径不是模型支持清单
computer-1 的默认回退路径通过 LiteLLM 让视觉模型输出严格 JSON;原生路径则按 Provider 使用相应 SDK。v0.18.0 的模块注释和可选依赖列出了 Anthropic/Bedrock、Gemini 与 OpenAI 适配代码,但这只证明 Harbor 里存在对应实现,不证明任意产品名、账户区域或当日 API 都可用。8 正确的发布记录应保存实际 model_name、Harbor commit、SDK 锁文件和一次预检结果。
命令里的 provider/your-vision-model 因此是必须替换的占位符。若使用通用 LiteLLM 路径,应先用一张固定截图确认模型确实接收图像并能输出动作 schema;若强制原生 Provider,则还要安装 harbor[computer-1] 并验证对应凭据。不要为了让初始化通过而盲目设置 enable_images=true:该参数可以覆盖元数据检查,却不能把文本模型变成视觉模型。
16.3.2 为什么固定几何
点击坐标依赖截图空间。computer-1 默认参数是 1024×900 桌面和同尺寸窗口;运行时还会警告“桌面与窗口同起点但尺寸不一致”的情况,因为裸露桌面会进入每一张截图。34 本章保持四个尺寸参数一致,并让页面无滚动、无动画、控件位置固定。
16.3.3 为什么不用截图像素评分
截图适合诊断,不适合作为唯一真值。字体渲染、光标、窗口装饰和压缩都能改变像素。Harbor 自带的 click-calibration 示例采用了更可靠的办法:应用完成后把状态写进窗口标题,Verifier 用 wmctrl 读取标题,再与 final_answer.txt 对照。9 本章沿用这个思路,另外保存最终截图供人工复核。
16.4 实战:先澄清,再修改控制面板
创建目录 tasks/gui-clarify-config/:
gui-clarify-config/
├── task.toml
├── instruction.md
├── environment/
│ ├── Dockerfile
│ ├── control_panel.html
│ └── control_panel.js
└── tests/
└── test.sh
16.4.1 Task 配置
schema_version = "1.1"
[task]
name = "harbor-book/gui-clarify-config"
description = "Clarify hidden requirements, then configure a local GUI."
authors = []
keywords = ["computer-use", "simulated-user", "clarification"]
[metadata]
difficulty = "medium"
category = "computer-use"
tags = ["computer-1", "ubuntu", "deterministic-user"]
[verifier]
timeout_sec = 120.0
[agent]
timeout_sec = 600.0
[environment]
build_timeout_sec = 1200.0
cpus = 2
memory_mb = 4096
storage_mb = 4096
gpus = 0
network_mode = "no-network"
mcp_servers = []
[verifier.env]
[environment.env]
[solution.env]
这个任务与 v0.18.0 自带的 computer-1-click-calibration 使用相同的 schema 版本、资源量级和 no-network 配置形态。9 无网络既降低外部页面变化,也防止 Agent 把隐藏需求发送到任务外部;模型 API 调用发生在 Harbor 进程一侧,不等价于给任务容器开放互联网。
instruction.md 不直接写出三个取值,只声明交互目标与结果格式。注意,这不等于整个静态 Task 镜像具备防泄露能力:
控制面板中有一位模拟用户和三个配置项。
1. 通过可见的聊天框分别询问时区、每日维护时间和自动保存偏好;不要猜测。
2. 只有在三项要求都确认后,才修改右侧表单并点击 Save configuration。
3. 页面显示绿色 SUCCESS 后,提交 done/answer 动作。result 格式必须恰为:
timezone=<确认的时区>; maintenance=<确认的 HH:MM>; autosave=<on 或 off>
当 harness 请求完成确认时,以同一 result 再提交一次。
只能使用可见 GUI。不要使用 Shell、开发者工具或页面源码。
16.4.2 固定桌面依赖
environment/Dockerfile 提供 computer-1 运行时实际调用的 X11、Chromium、截图和输入工具。基础镜像使用发布时核验过的 manifest-list digest;升级镜像时要重新跑整套 GUI 回归,而不是只改 tag。
FROM python:3.12-slim@sha256:c3d81d25b3154142b0b42eb1e61300024426268edeb5b5a26dd7ddf64d9daf28
ENV DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8 LC_ALL=C.UTF-8
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates chromium curl dbus-x11 fonts-liberation \
imagemagick procps scrot webp wmctrl x11-utils xauth \
xdotool xfce4-session xfwm4 xvfb \
&& rm -rf /var/lib/apt/lists/* \
&& chmod 1777 /tmp /var/tmp \
&& mkdir -p /tmp/.ICE-unix /logs \
&& chmod 1777 /tmp/.ICE-unix \
&& chmod 777 /logs
WORKDIR /app
COPY control_panel.html control_panel.js /app/
16.4.3 一个确定性的模拟用户界面
把下面内容保存为 environment/control_panel.html。它一次只披露一个未确认约束;三项问完前保存按钮不可用。成功时,页面把规范化状态写入窗口标题,供 Verifier 读取。
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy"
content="default-src 'none'; script-src 'self'; style-src 'unsafe-inline';
img-src 'self'; connect-src 'none'; base-uri 'none'; form-action 'none'">
<title>BOOTING</title>
<style>
* { box-sizing: border-box; animation: none !important; transition: none !important; }
body { margin: 0; font: 18px sans-serif; background: #eef2f6; color: #17202a; }
main { width: 960px; height: 760px; margin: 38px auto; display: grid;
grid-template-columns: 1.15fr 0.85fr; gap: 20px; }
section { background: white; border: 2px solid #9aa8b6; border-radius: 8px; padding: 20px; }
#log { height: 420px; overflow: hidden; border: 1px solid #aab4be;
padding: 12px; white-space: pre-wrap; background: #f8fafc; }
input, select, button { width: 100%; font: inherit; padding: 10px; margin-top: 12px; }
button { background: #145da0; color: white; border: 0; border-radius: 5px; }
button:disabled { background: #82909d; }
label { display: block; margin-top: 18px; }
#autosave { width: auto; transform: scale(1.4); margin-right: 10px; }
#status { min-height: 72px; margin-top: 24px; padding: 14px; border: 2px solid #9aa8b6; }
.success { color: #096b32; border-color: #096b32 !important; font-weight: bold; }
</style>
</head>
<body>
<main>
<section>
<h1>Requirement chat</h1>
<div id="log">User: 请分别询问我三个具体约束。</div>
<input id="question" aria-label="Question to user" placeholder="Ask one specific question">
<button id="ask">Ask user</button>
</section>
<section>
<h1>Maintenance settings</h1>
<label>Timezone
<select id="timezone">
<option>Asia/Shanghai</option><option>UTC</option><option>America/Los_Angeles</option>
</select>
</label>
<label>Daily maintenance
<select id="maintenance"><option>00:00</option><option>02:00</option><option>04:00</option></select>
</label>
<label><input id="autosave" type="checkbox" checked>Enable autosave</label>
<button id="save" disabled>Save configuration</button>
<div id="status">PENDING — clarify all three requirements.</div>
</section>
</main>
<script src="./control_panel.js"></script>
</body>
</html>
把交互逻辑保存为 environment/control_panel.js:
const known = new Set();
let turns = 0;
const log = document.querySelector('#log');
const question = document.querySelector('#question');
const save = document.querySelector('#save');
document.title = 'PENDING|turns=0';
document.addEventListener('contextmenu', event => event.preventDefault());
function append(role, text) { log.textContent += `\n${role}: ${text}`; }
document.querySelector('#ask').addEventListener('click', () => {
const raw = question.value.trim();
if (!raw) return;
turns += 1;
append('Agent', raw);
const q = raw.toLowerCase();
let id = '';
let reply = '请一次询问一个具体约束。';
if (!known.has('timezone') && (q.includes('timezone') || q.includes('时区'))) {
id = 'timezone'; reply = '时区使用 UTC。';
} else if (!known.has('maintenance') &&
(q.includes('maintenance') || q.includes('维护') || q.includes('时间'))) {
id = 'maintenance'; reply = '每日维护时间是 02:00。';
} else if (!known.has('autosave') &&
(q.includes('autosave') || q.includes('自动保存'))) {
id = 'autosave'; reply = '关闭自动保存。';
}
if (id) known.add(id);
append('User', reply);
question.value = '';
document.title = `PENDING|turns=${turns}`;
if (known.size === 3) {
append('User', '三项要求均已确认,请配置并保存。');
save.disabled = false;
}
});
save.addEventListener('click', () => {
const timezone = document.querySelector('#timezone').value;
const maintenance = document.querySelector('#maintenance').value;
const autosave = document.querySelector('#autosave').checked;
const status = document.querySelector('#status');
if (known.size === 3 && timezone === 'UTC' && maintenance === '02:00' && !autosave) {
document.title = `PASS|turns=${turns}|timezone=UTC|maintenance=02:00|autosave=off`;
status.textContent = 'SUCCESS — configuration saved.';
status.className = 'success';
} else {
document.title = `FAIL|turns=${turns}`;
status.textContent = 'ERROR — settings do not match confirmed requirements.';
}
});
HTML 把初始标题设为 BOOTING,只有外部脚本成功加载才改成 PENDING;这也让“CSP 误伤正常脚本”可被 readiness 发现。script-src 'self' 没有加入 'unsafe-inline',按 CSP Level 3 的 inline check,javascript: URL 只有在所有策略都允许 inline script 时才执行,因此不能用它直接改写标题。10 页面同时禁用右键菜单;computer-1 默认没有 bash,运行时还会阻止常见开发者工具快捷键和 view-source: 导航。4
这些措施仍不是通用秘密隔离:隐藏值存在于 Task 镜像的 JavaScript 中,而且 computer-1 的 navigate 并不禁止 file:。Agent 可以暂时打开 file:///app/control_panel.js 阅读取值,再返回原页面完成三轮形式化提问;最终 CDP 只看到当前 URL,无法证明这段历史没有发生。因此,本例只能衡量“是否完成澄清交互与 GUI 操作流程”,不能衡量对抗答案泄露的能力,也不能证明 Agent 的取值确实来自用户回复。
发布本基线时,应审计 trajectory 里的 computer_action 参数与截图,标记 url 或 text 中出现 control_panel.js、data:、javascript:,以及离开目标页面的地址栏操作。这个后处理门能发现常见旁路,但不是安全边界。对抗式 Benchmark 必须把 persona 和隐藏取值移到独立 Sidecar:页面只提交问题并显示回复,Agent 容器不包含服务端源码或状态读取接口,Verifier 再读取服务端审计 Artifact。即使只评测 CUA,也要在发布门中实际尝试 JavaScript 标题改写、data 假页面和直接读取 JS,而不是从 CSP 或最终 URL 推断整个轨迹可信。
16.4.4 验证可观察状态和终止
tests/test.sh 同时检查窗口状态与最终回答。截图只作为失败证据,不参与分数:
#!/usr/bin/env bash
set -u
mkdir -p /logs/verifier
rm -f /logs/verifier/reward.json
export DISPLAY="${DISPLAY:-:1}"
scrot -o /logs/verifier/final-screen.png >/dev/null 2>&1 || true
CDP_URL="${HARBOR_TEST_CDP_URL:-http://127.0.0.1:9222/json/list}" \
python3 - <<'PY'
import json
import os
import re
import sys
from pathlib import Path
from urllib.request import Request, urlopen
LOGS = Path("/logs/verifier")
REPORT = LOGS / "gui-report.json"
TARGET_URL = "file:///app/control_panel.html"
TARGET_TITLE = re.compile(
r"PASS\|turns=([3-9]|[1-9][0-9]+)\|"
r"timezone=UTC\|maintenance=02:00\|autosave=off"
)
EXPECTED_ANSWER = "timezone=UTC; maintenance=02:00; autosave=off"
MAX_CDP_BYTES = 1_048_576
class InfrastructureFailure(Exception):
pass
def no_duplicate_keys(pairs):
result = {}
for key, value in pairs:
if key in result:
raise ValueError(f"duplicate key: {key}")
result[key] = value
return result
def reject_constant(value):
raise ValueError(f"non-finite JSON constant: {value}")
def write_report(payload):
REPORT.write_text(
json.dumps(payload, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
try:
request = Request(os.environ["CDP_URL"], headers={"Accept": "application/json"})
with urlopen(request, timeout=3) as response:
raw = response.read(MAX_CDP_BYTES + 1)
if len(raw) > MAX_CDP_BYTES:
raise InfrastructureFailure("CDP response exceeds size limit")
targets = json.loads(
raw.decode("utf-8"),
object_pairs_hook=no_duplicate_keys,
parse_constant=reject_constant,
)
if type(targets) is not list:
raise InfrastructureFailure("CDP response is not a JSON array")
for target in targets:
if type(target) is not dict:
raise InfrastructureFailure("CDP target is not an object")
if any(type(target.get(k)) is not str for k in ("type", "url", "title")):
raise InfrastructureFailure("CDP target has invalid type/url/title")
except Exception as exc:
failure = exc if isinstance(exc, InfrastructureFailure) else InfrastructureFailure(str(exc))
write_report({
"classification": "infrastructure_failure",
"reason": str(failure),
"cdp_url": os.environ.get("CDP_URL", ""),
})
print(f"infrastructure failure: {failure}", file=sys.stderr)
raise SystemExit(20)
pages = [target for target in targets if target["type"] == "page"]
answer_path = Path("/logs/agent/final_answer.txt")
try:
answer_bytes = answer_path.read_bytes() if answer_path.exists() else b""
if len(answer_bytes) > 4096:
raise ValueError("final answer exceeds size limit")
answer = answer_bytes.decode("utf-8").rstrip("\r\n")
except (OSError, UnicodeError, ValueError) as exc:
answer = ""
answer_error = str(exc)
else:
answer_error = ""
reward = 0.0
classification = "candidate_failure"
page_url = pages[0]["url"] if len(pages) == 1 else ""
page_title = pages[0]["title"] if len(pages) == 1 else ""
if len(pages) != 1:
reason = f"expected exactly one page target, found {len(pages)}"
elif page_url != TARGET_URL:
reason = f"page URL is not the task application: {page_url!r}"
elif TARGET_TITLE.fullmatch(page_title) is None:
reason = "task application title is not a valid PASS state"
elif answer_error:
reason = f"invalid final answer artifact: {answer_error}"
elif answer != EXPECTED_ANSWER:
reason = "missing or incorrect terminal result"
else:
reward = 1.0
classification = "success"
reason = "original application state and terminal result agree"
(LOGS / "reward.json").write_text(
json.dumps({"reward": reward}, indent=2) + "\n", encoding="utf-8"
)
write_report({
"classification": classification,
"reward": reward,
"reason": reason,
"page_count": len(pages),
"page_url": page_url,
"page_title": page_title,
"final_answer": answer,
})
PY
Verifier 不再从全部窗口里搜索一个看似正确的标题,而是读取 Chromium DevTools Protocol(CDP)的 /json/list:必须恰有一个 page target,URL 必须精确等于 file:///app/control_panel.html,然后才检查该 target 的标题。data URL 即使伪造出完整 PASS 标题,也会因 URL 不同得到 0。CDP 缺失、超大、非 UTF-8、重复键、非有限数或 schema 畸形属于基础设施错误:退出 20,且不生成 Reward;一个结构正常但 URL 错误、页面数错误或状态未完成的 CDP 响应属于候选解失败。
Verifier 不读取聊天文本,因为页面标题中的 turns>=3 与保存按钮的三项解锁条件只用于证明三次有效交互和最终保存状态。它不证明取值获取路径没有泄露;这一点必须由前述 trajectory 审计或 Sidecar 设计解决。若要评估提问质量,应额外收集去敏后的对话审计,并用第 12 章的方法单独评分;不要把模糊的“问得好”偷偷塞进确定性 correctness。
16.4.5 运行与验收
在任务父目录运行。CUA_MODEL 必须是你实际配置了凭据的视觉模型;不要把本书中的模型名称当成永久支持清单。
export CUA_MODEL='provider/your-vision-model'
harbor run \
-p ./gui-clarify-config \
-a computer-1 \
-m "$CUA_MODEL" \
--ak max_turns=40 \
--ak start_url=file:///app/control_panel.html \
--ak desktop_width=1024 \
--ak desktop_height=900 \
--ak window_width=1024 \
--ak window_height=900 \
-y
验收时查看 Trial 中的 reward.json、gui-report.json、final-screen.png、final_answer.txt 和 trajectory.json。Reward 通过条件只有四项:至少三轮有效询问;三个控件值匹配;原 URL 的窗口状态为 PASS;Agent 以精确 result 完成两阶段确认。发布审计还要检查 trajectory 是否访问 JS、data 或 JavaScript URL;这是基线收录门,不是当前 test.sh 的 Reward 条件。模型超时后由 fallback 生成一段文字,不应替代显式终止,因为它通常无法满足精确 result。
发布门:本章环境没有可用 Docker daemon,本次核验也未使用模型或外部桌面凭据,因此没有宣称上述端到端 Job 已通过。发布前必须在 Linux Docker 主机上完成镜像构建、一次人工操作、一次
computer-1Trial、错误配置反例和至少一组重复运行;还必须实测 CSP 对 JavaScript URL 的阻止、data 假页判 0、直接打开control_panel.js的 trajectory 审计告警。
16.4.6 在付费运行前先攻击 Verifier
这个 Verifier 至少要覆盖下列夹具,而不是只做 bash -n:
| 夹具 | CDP 的唯一 page | final_answer.txt | 预期 |
|---|---|---|---|
| 正确完成 | 原 URL + 精确 PASS,且 turns>=3 | 精确完成文本 | Reward 1 |
| 未完成 | 原 URL + PENDING/FAIL | 任意 | Reward 0 |
| data 假页面 | data:text/html,... + 伪造 PASS | 正确答案 | Reward 0 |
| 错误 URL 或额外 tab | 非原 URL,或 page 数不为 1 | 任意 | Reward 0 |
| CDP 故障 | 连接失败、非 JSON、重复键或错误 schema | 任意 | 不写 Reward,退出 20 |
还要增加三个 gaming 负例。第一,伪造 final_answer.txt,但让原页面保持 PENDING,结果必须为 0;反过来,页面 PASS 而 Agent 未正确终止也必须为 0。第二,在实际 Chromium 内导航到 javascript:document.title='PASS|...',随后通过 CDP 确认标题仍是原状态;再导航到 data URL 伪造同一标题,确认 URL 检查将其判为 0。第三,直接打开 control_panel.js、读出取值并返回;Reward 可能仍为 1,但发布 trajectory 审计必须拒绝该 Trial。这个负例刻意展示终态 Verifier 的边界。JavaScript 负例验证 CSP 的浏览器执行效果,无法由 JSON fixture 替代;没有可用 Chromium 时必须保留为发布门。
应用标题不是所有 GUI 的最佳证据。真实设置程序可以把规范化配置写入只读 API、数据库或 Sidecar Artifact;Verifier 从独立环境读取这些状态。关键不是标题这种载体,而是证据必须由被操作应用产生、语义稳定、无法仅靠最终文本伪造。若应用状态只能从屏幕看见,可以增加 OCR 或视觉裁判作为辅助维度,但不要让它取代可获得的结构化真值。
16.5 Ubuntu 与 Windows 不是一个开关
16.5.1 本地 Ubuntu 路径
本章示例实际上是 Linux 容器中的 Xvfb + XFCE + Chromium,而不是完整 Ubuntu VM。它适合浏览器 GUI、固定字体和无宿主显示器的 CI。需要 LibreOffice、系统设置或跨应用拖放时,应采用包含这些应用的桌面镜像,并继续固定分辨率、语言、时区和窗口布局。
Harbor v0.18.0 的 use-computer Environment 接受 ubuntu、windows、macos 和 ios 平台值并要求 USE_COMPUTER_API_KEY;cua-cloud 源码另有自己的平台集合和 CUA_CLIENT_ID/CUA_CLIENT_SECRET。1112 这些是两套 Environment Provider,不是 computer-1 的别名,也不能互换凭据。
16.5.2 三种“Windows”
| 路径 | Task 实际运行处 | v0.18.0 的关键限制 |
|---|---|---|
[environment].os = "windows" + 本地 Docker | Windows 容器 | 需要 Windows host/daemon;测试只允许声明 SUPPORTS_WINDOWS=True 的 Agent |
use-computer/cua-cloud 的 platform=windows | Provider 管理的 Windows 环境 | 两者源码都要求 Task 的 OS 也是 Windows;仍受 Agent OS 兼容检查 |
| Linux Task + Windows CUA Sidecar/MCP | Agent 在 Linux 控制器中,GUI 在外部 Windows sandbox | 是任务自带集成;依赖外部服务、网络、凭据和该服务的 API |
v0.18.0 的兼容测试明确规定只有 oracle 和 nop 声明 Windows 支持,其他已注册 Agent(包括 computer-1)保持 False 并在 setup() 前失败。13 因此不能写出“把本章命令的 platform 改为 windows 即可”。
官方 Cookbook 的 Windows recipe 选择第三条路:Linux Task 的 Sidecar 使用 Daytona 凭据创建外部 Windows sandbox,再把截图、鼠标和键盘封装为 MCP 工具;它不是 Harbor 本地 Windows 容器,也不是内置 computer-1 的 Windows 模式。14 Daytona 的官方文档确认其 Computer Use API 包含截图、鼠标、键盘和显示操作,并分别描述 Linux 与 Windows 能力;具体可用性仍应在运行时预检,而不能从 Cookbook 示例推断你的账户已经开通。15
16.6 控制波动,而不是用重复次数掩盖它
GUI 波动通常来自四层:
- 环境层:镜像漂移、字体缺失、语言与时区不同;
- 显示层:分辨率、DPI、窗口装饰和应用启动位置变化;
- 应用层:动画、异步加载、焦点竞争和弹窗;
- 模型层:视觉识别、坐标选择和终止判断具有随机性。
排查顺序也应按这四层进行。先看 final-screen.png 与 X11/Chromium 日志,确认页面是否正确出现;再看 trajectory 中最后一张截图与动作坐标;然后看页面状态;最后才分析模型推理。基础设施未启动时把 Reward 记为模型失败,会污染 Benchmark。
稳定性控制清单如下:
- 镜像以 digest 固定,字体包和 locale 明确安装;
- 桌面、窗口和应用内容区尺寸一致;
- 禁用动画,固定初始 URL、控件顺序和默认值;
- readiness 检查应用状态,不只检查端口;
- 每个关键动作后允许页面进入稳定态,但不使用任意长 sleep;
- Verifier 读取结构化应用状态或数据库,截图只作旁证;
- 每个 Trial 创建独立用户会话,不复用全局历史;
- 对 Agent、模拟用户和外部桌面分别设置轮数、时间和费用上限。
警告:Computer Use 扩大了提示注入和误操作面。对联网桌面应限制可访问域名、使用无真实账户的隔离环境,并阻止高影响操作。Anthropic 的官方文档也把联网 Computer Use 标为具有额外风险的 Beta 能力。16
16.6.1 建立轮数与费用预算
设一次 Trial 中 Agent 模型调用成本为 C_agent,模拟用户成本为 C_user,外部桌面费用为 C_desktop,则总成本应按三项分别汇总,而不是把桌面价格硬换算成 token:
C_trial = C_agent + C_user + C_desktop
这个式子不需要假设任何厂商单价。运行时记录实际 token、Provider 返回的成本字段、用户服务请求数、桌面占用时长和重试即可。若某 Provider 不返回费用,结果应保留 unknown,再用实验时锁定的官方价目离线估算;不要用 0 代替未知。
轮数预算应从任务的最短合理路径出发。本章至少需要三次提问、三个控件操作、一次保存和一次终止,但视觉 Agent 还可能需要聚焦、等待和纠错。max_turns=40 是本章的防失控上限,不是性能目标。分析时同时报告成功 Trial 的实际轮数和触顶比例;如果大量 Trial 接近上限,先检查焦点、页面状态和指令,再决定是否提高预算。
对 LLM 用户还要设独立的 max_user_turns 和单次输出上限。Agent 结束后立即停止用户服务,外部桌面则无论成功、失败还是取消都进入 finally 清理。只在正常成功路径释放 sandbox,会把偶发 Agent 超时放大为持续计费和资源耗尽。
16.6.2 失败场景
保存按钮一直灰色。 查看页面 transcript。如果 Agent 把三个主题塞进同一个问题,确定性用户只披露第一项;这是预期的多轮约束,不是 UI 故障。
点击落在控件边缘。 先比较 trajectory 截图尺寸与命令里的四个几何参数,再确认窗口是否在 (0,0)。不要通过把按钮无限放大来隐藏坐标问题;可以增加 padding,但应记录 UI 版本。
页面已显示 SUCCESS,Reward 仍为 0。 打开 gui-report.json。常见原因是 Agent 没有显式 done,或 result 多了说明文字。这里故意要求精确文本,用来独立验证终止协议。
Windows Trial 在 Agent setup 前失败。 先看 Task OS 与 Environment platform,再看 Agent 的 SUPPORTS_WINDOWS。不要把它诊断成桌面 API 认证失败。
费用突然上升。 按 trajectory 统计动作轮数和每步 token;再把模拟用户模型调用从 Agent 费用中拆出。若只是坐标反复重试,应修 UI 和 readiness,而不是换更贵模型。
16.7 本章小结
- Harbor v0.18.0 没有一等 Simulated User 抽象;可通过 Task 内服务或确定性界面实现。
computer-1是截图驱动的 Agent,不是桌面云 Provider;use-computer与cua-cloud又是不同 Provider。- 多轮评测必须显式定义披露状态、轮数预算和双方终止条件;
computer-1的完成还需二次确认。 - GUI correctness 应读取绑定原 URL 的应用状态,截图主要用于诊断;终止结果需要独立验收。
- 静态 file:// 用户界面只是非对抗流程基线;终态 CDP 不能证明 Agent 未读 JS,严格任务应使用 Sidecar 审计。
- 本地 Linux GUI、Windows 容器和外部 Windows CUA 是三条不同链路。
- 成本与稳定性取决于总动作轮数、两侧模型调用和桌面基础设施,不能只看最终 Reward。
16.8 练习
- 把本章用户状态机增加“部署区域”约束,并保证一个包含四个关键词的问题仍只披露一项;为成功和少问一轮各写一个 Verifier fixture。
- 将
control_panel.html的确定性用户替换为独立 HTTP/MCP Sidecar。会话以 Trial 唯一 ID 分区,并输出不含原始敏感文本的 JSONL 审计。 - 故意把
window_height改为 768,运行三次并比较截图、点击坐标和early_termination_reason;恢复为 900 后重复实验。 - 设计 Windows 外部 CUA 版本的发布门,至少覆盖凭据预检、sandbox 清理、分辨率、键盘键名差异和无可用 Windows 额度时的错误分类。
- 在固定 Agent 模型下比较确定性用户与 LLM 用户各十次。分开报告两侧 token/费用、需求披露一致率、GUI 成功率和平均轮数,不把基础设施错误计入模型失败。