
一、为什么 Java 后端工程师现在必须看懂 Computer Use
过去一周,Computer Use(计算机操作)密集刷屏:OpenAI 发布 GPT-6.1 Terra,把"计算机操作"从实验性功能推向产品化;腾讯混元 Hy4 首次引入视觉理解与 Computer Use 能力,权重即将全面开源;Perplexity 给 Agent API 加上了 ultra 努力级别。CSDN 热榜上那篇《同一个模型从 30 分到 95 分:拉开差距的不是模型,是 Harness》,说的正是这件事------决定 Agent 能不能干活的,往往不是模型,而是模型外面那套工程系统。
很多 Java 工程师看到"AI 操作电脑"第一反应是:这是 Python 自动化脚本的事,跟我没关系。恰恰相反。Computer Use Agent 的本质是一个"感知-决策-执行"分布式系统,模型只负责中间那一步决策,两头的感知(截屏、缩放、校准)和执行(动作执行、审批、审计、限流)全是 Java 工程师的主场。 Anthropic 官方文档说得很直白:API 只提供"智能",执行基础设施全部由你自己搭建------而搭建执行基础设施,正是 Java 团队做了二十年的事。
这篇文章我们把一个生产级的 Computer Use Agent(下称 DeskOps 巡检 Agent)从协议层到代码层完整拆一遍:感知-行动循环的协议细节、坐标空间的数学、执行器的实现、以及九个真实的生产踩坑。
二、协议层:Computer Use 到底是怎么工作的
2.1 一个"schema-less"的特殊工具
Computer use 在 Anthropic Messages API 里不是普通的自定义工具。普通工具由你提供 JSON Schema,模型填参数;Computer use 工具是一个类型化工具(typed tool),它的输入 schema 内置在模型里,你不能修改。你在请求里这样声明:
json
{
"tools": [
{
"type": "computer_20251124",
"name": "computer",
"display_width_px": 1280,
"display_height_px": 720,
"enable_zoom": true
}
],
"betas": ["computer-use-2025-11-24"]
}
注意两个关键点:
type字段是版本号,不同代模型绑定不同工具版本------Claude 4.6 家族(Opus 4.6/Sonnet 4.6/Haiku 4.5)用computer_20251124,配 beta headercomputer-use-2025-11-24;Sonnet 4.5 及更早模型用computer_20250124配computer-use-2025-01-24。模型、工具版本、beta header 三者必须配套,配错直接 400。- 除了
computer,官方还提供text_editor和bash两个配套工具,分别用于文件查看编辑和跑 shell 命令。生产上通常三件套一起下发。
2.2 感知-推理-行动循环
整个 Agent 的心脏是一个循环,协议层面只有四步:
1. 截屏,把当前屏幕状态(截图 + 任务描述 + 历史消息)发给模型
2. 模型返回 stop_reason="tool_use",content 里带 tool_use 块:
{ "type": "tool_use", "id": "toolu_xxx", "name": "computer",
"input": { "action": "left_click", "coordinate": [842, 315] } }
3. 你的执行器执行这个动作,截新屏,把结果包成 tool_result 回传(带原 tool_use_id)
4. 回到第 1 步,直到模型返回最终文本(stop_reason 变为 end_turn)
也就是说,模型永远不碰你的电脑。它只输出"我想点 (842, 315)"这样的意图,点不点、怎么点、点了之后干不干净,全在你的 Java 执行器里。这就是为什么说执行层是 Java 工程师的护城河。
可用动作随版本扩展:基础动作(screenshot、left_click、type、key、mouse_move)全版本支持;增强鼠标动作(right_click、double_click、triple_click、left_click_drag)、精细控制(left_mouse_down/left_mouse_up、hold_key、wait)、方向滚动(scroll)从 20250124 版开始支持;区域放大 zoom 是 20251124 版新增。
2.3 坐标空间契约:整套系统最容易死掉的地方
模型输出的坐标不是"屏幕坐标",而是你声明的 display_width_px / display_height_px 坐标空间里的坐标。这里藏着整套 API 最重要的约束------API 内部有图像处理上限,超限的截图会被静默降采样,模型看到的是降质图像,而你的执行器还在按原始分辨率解释它返回的坐标:
- Claude 4.6 家族:长边最大 1568 像素,总像素最大 1.15MP(1280×720 ≈ 0.92MP,用掉约 80% 预算,是最安全的默认值)
- Opus 4.7:长边 2576 像素,总像素 3.75MP(可以上 1080p)
如果虚拟桌面是 1920×1080,正确做法是先把截图缩到限制内,再把 display_width_px 设成缩放后的尺寸,执行动作时把模型坐标按比例放大回真实屏幕。缩放因子公式:
python
scale = min(1.0, 1568 / max(w, h), sqrt(1_150_000 / (w * h)))
Anthropic 官方明确说:99% 的点击失败可以追溯到尺寸/缩放不匹配。这条我们放到踩坑清单里细说。
三、代码实战:DeskOps 巡检 Agent 生产级实现
3.1 架构与技术选型
项目背景:企业内有一批遗留业务系统只有 Web 界面、没有 API(供应商停止维护的老 ERP),运维团队每天要人工登录巡检十余个系统的指标和告警。DeskOps 的任务:驱动 Linux 虚拟桌面里的浏览器登录这些系统,读取告警数字,截图取证,落库告警。
技术栈:Spring Boot 4 + Java 25(虚拟线程)+ Micrometer + PostgreSQL。
架构上有一个关键决策要先说清楚:Spring AI 2.0 的 AnthropicChatModel 走 ToolCallingChatOptions 体系,ToolCallback 会被翻译成"自定义工具"(带 JSON Schema 的普通 tool)。而 computer use 是 schema-less 的类型化工具,Spring AI 目前没有一等支持。 生产上的正确姿势是:感知-行动循环这一段直接对接 Messages API(用 Spring 的 RestClient 手写,协议上面已经拆清楚了),而审计、会话、指标、告警落库这些外围系统照常用 Spring 生态。这不是绕开框架,而是把框架用在它擅长的层。
java
@Component
public class RawAnthropicClient {
private final RestClient restClient;
public RawAnthropicClient(RestClient.Builder builder,
@Value("${deskops.anthropic.api-key}") String apiKey) {
this.restClient = builder
.baseUrl("https://api.anthropic.com")
.defaultHeader("x-api-key", apiKey)
.defaultHeader("anthropic-version", "2023-06-01")
.defaultHeader("anthropic-beta", "computer-use-2025-11-24")
.build();
}
public JsonNode createMessage(Map<String, Object> body) {
return restClient.post()
.uri("/v1/messages")
.contentType(MediaType.APPLICATION_JSON)
.body(body)
.retrieve()
.body(JsonNode.class);
}
}
3.2 截图服务:先缩放,再发送
java
@Component
public class ScreenshotService {
// 4.6 家族限制:长边 1568px,总像素 1.15MP
private static final int MAX_LONG_EDGE = 1568;
private static final long MAX_PIXELS = 1_150_000L;
private final VirtualDesktop desktop; // 封装 Xvfb 虚拟屏
/** 截屏并缩放到 API 安全区,返回缩放后的尺寸与 Base64 图像 */
public Screenshot capture() throws Exception {
BufferedImage raw = desktop.capture();
double scale = computeScale(raw.getWidth(), raw.getHeight());
int w = (int) Math.round(raw.getWidth() * scale);
int h = (int) Math.round(raw.getHeight() * scale);
BufferedImage scaled = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
Graphics2D g = scaled.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.drawImage(raw, 0, 0, w, h, null);
g.dispose();
ByteArrayOutputStream out = new ByteArrayOutputStream();
ImageIO.write(scaled, "png", out);
return new Screenshot(Base64.getEncoder().encodeToString(out.toByteArray()), w, h, scale);
}
private double computeScale(int w, int h) {
double byLongEdge = (double) MAX_LONG_EDGE / Math.max(w, h);
double byPixels = Math.sqrt((double) MAX_PIXELS / ((long) w * h));
return Math.min(1.0, Math.min(byLongEdge, byPixels));
}
public record Screenshot(String base64, int width, int height, double scale) {}
}
注意 Math.min(1.0, ...)------只缩小、绝不放大。桌面本身小于限制时按原样发送。
3.3 执行器:模型说"点",你来决定点不点
执行器分两层:坐标层(纯粹的数学映射)和动作层(xdotool 执行 + 安全拦截)。
java
@Component
public class ActionExecutor {
private final VirtualDesktop desktop;
private final SafetyGate safetyGate; // 安全围栏,见 3.5
private final AuditLog auditLog;
/** 执行一个 computer 动作,返回 tool_result 内容(通常是新截图) */
public Map<String, Object> execute(JsonNode input, double scale) throws Exception {
String action = input.path("action").asText();
switch (action) {
case "screenshot" -> { /* 直接返回新截图 */ }
case "left_click" -> {
JsonNode coord = input.path("coordinate");
int x = scaleBack(coord.get(0).asInt(), scale);
int y = scaleBack(coord.get(1).asInt(), scale);
safetyGate.intercept(action, x, y); // 危险区域校验,可能抛 ApprovalRequiredException
desktop.run("xdotool", "mousemove", String.valueOf(x),
String.valueOf(y), "click", "1");
}
case "type" ->
desktop.run("xdotool", "type", "--delay", "40", input.path("text").asText());
case "key" ->
desktop.run("xdotool", "key", input.path("key").asText());
case "scroll" -> {
String dir = input.path("scroll_direction").asText("down");
int amount = input.path("scroll_amount").asInt(3);
desktop.run("xdotool", "key", dir.equals("up") ? "Page_Up" : "Page_Down");
}
case "wait" ->
Thread.sleep(Math.min(input.path("duration").asLong(1000), 5000));
default -> throw new UnsupportedActionException(action);
}
auditLog.record(action, input, scale);
return Map.of(); // 由调用方统一附加新截图
}
private int scaleBack(int apiCoord, double scale) {
return (int) Math.round(apiCoord / scale);
}
}
这里有两个刻意的生产细节:
type动作加--delay 40:老系统的表单是 JS 逐字段校验的,输入太快会丢字符;键盘事件之间留 40ms 是实测稳态。scroll退化成 Page Down :xdotool的滚轮事件在部分 Web 页面上不生效(页面监听的是滚动容器事件),官方文档也承认滚动可靠性是已知弱点,键盘替代方案更稳。
3.4 感知-行动循环:预算、停滞检测与退出条件
java
@Component
public class DeskOpsLoop {
private static final int MAX_STEPS = 25;
private static final Duration STEP_TIMEOUT = Duration.ofSeconds(120);
private final RawAnthropicClient anthropic;
private final ScreenshotService screenshots;
private final ActionExecutor executor;
private final NoProgressDetector stallDetector;
public TaskOutcome run(String goal) {
List<Map<String, Object>> messages = new ArrayList<>();
messages.add(Map.of("role", "user", "content", goal));
for (int step = 1; step <= MAX_STEPS; step++) {
Map<String, Object> body = buildBody(messages); // tools + model + maxTokens
JsonNode resp = anthropic.createMessage(body);
if (!"tool_use".equals(resp.path("stop_reason").asText())) {
return TaskOutcome.finished(extractText(resp), step);
}
List<Map<String, Object>> toolResults = new ArrayList<>();
for (JsonNode block : resp.path("content")) {
if (!"tool_use".equals(block.path("type").asText())) continue;
JsonNode action = block.path("input");
stallDetector.observe(action); // 指纹检测:连续相同动作+坐标 = 停滞
Object result;
try {
result = executor.execute(action, currentScale());
} catch (ApprovalRequiredException e) {
result = Map.of("type", "text",
"text", "ACTION_BLOCKED: 该操作需要人工审批,任务暂停");
// 回传结构化错误而非沉默失败,让模型知道"被拦"而不是"没点上"
}
toolResults.add(Map.of(
"type", "tool_result",
"tool_use_id", block.path("id").asText(),
"content", result));
}
if (stallDetector.isStalled()) {
return TaskOutcome.stalled(step); // 3 步无进展立即终止,防止 token 烧毁
}
messages.add(Map.of("role", "assistant", "content", resp.path("content")));
messages.add(Map.of("role", "user", "content", toolResults));
}
return TaskOutcome.budgetExhausted(); // 明确返回失败,绝不静默"假装成功"
}
}
三个退出门缺一不可:模型自己说做完了(end_turn)、步数预算耗尽(BUDGET_EXHAUSTED)、停滞检测熔断。预算耗尽必须显式返回失败状态------"跑满 25 步没完成"和"任务成功"是两个天差地别的结果,混在一起就是事故。
循环跑在虚拟线程上(Spring Boot 4 默认 virtual: true):每次 API 调用 30-120 秒起步,平台线程池扛不住几个并发任务,虚拟线程可以轻松支撑几十个并发巡检。
3.5 安全围栏:模型提议,系统拍板
Computer Use 的安全模型必须先记住一句话:网页、邮件、文档、截图都是不可信输入。一个被巡检页面里藏着的提示注入指令,完全可能把 Agent 从"读告警"带偏到"删配置"。围栏分四层:
java
@Component
public class SafetyGate {
private final Set<String> forbiddenRegions = Set.of("toolbar-delete", "nav-settings", "admin-panel");
private final ApprovalService approvals; // HITL 审批
public void intercept(String action, int x, int y) {
// 第一层:坐标围栏------删除按钮、设置入口等区域的点击一律拦截
String region = regionResolver.locate(x, y);
if (forbiddenRegions.contains(region)) {
throw new ForbiddenRegionException(region);
}
// 第二层:语义围栏------删除/提交/支付类动作升级人工审批
if (DANGEROUS_ACTIONS.contains(action) && pendingContext().isWriteOperation()) {
approvals.requestApproval(action, x, y); // 阻塞等待,超时 fail-safe
}
}
}
加上基础设施层:DeskOps 的虚拟桌面跑在一次性 Docker 容器里(Xvfb + Fluxbox + Firefox),容器内无 SSH 密钥、无密码管理器、无浏览器登录态;出口网络走域名白名单;每个会话结束销毁重建。记住官方那句警告:容器能降低暴露面,但 Docker 不会自动让 Agent 变安全------特权参数、挂载宿主目录、开放端口、泄漏的凭证,每一样都能让沙箱形同虚设。
四、九个生产踩坑清单
以下每一条都来自真实落地过程或官方文档明确警示,按杀伤力排序。
1. 点击整体偏移------display 尺寸与实际图像不一致(杀伤力最大)。 99% 的点击失败根因在此。display_width_px/display_height_px 必须精确等于实际发送的截图尺寸,不是虚拟桌面尺寸。日志里同时打印这两个值和真实图像尺寸,出问题第一件事就是比对这三个数。
2. macOS Retina 的 2 倍设备像素比。 Retina 截屏出来是逻辑分辨率 2 倍的图,坐标空间直接错一倍。要么发送前缩回 1x,要么把返回坐标减半。服务端跑 Linux 没这个问题,但本地开发机调试时必踩。
3. API 静默降采样。 超过 1568px/1.15MP 的图会被 API 内部默默缩小,不报任何错。模型按降质图给坐标,你按原始分辨率执行,点击永远差一截。解法只有一个:发送前自己缩放到限制内,让坐标空间自洽。
4. 内容顺序:文字在前,图像在后。 content 数组里先放文字指令再放截图。顺序反了会显著增加"差一点点点上"的近失误------模型在缺乏语义上下文的情况下先看图,元素识别误判率上升。
5. thinking effort 拉满是负优化。 UI 操作本质是感知任务不是逻辑推理任务。实测 Claude 4.6 家族 medium effort 与 high 成功率几乎持平,输出 token 少 50%;max effort 比 high 多花 30-50% token,成功率没有提升。别用 ChatGPT 式思维套这个 API。
6. 截图历史与 prompt caching。 多步任务 20-30 轮之后消息数组全是截图,token 增长恐怖。用滚动窗口 + Anthropic prompt caching:把历史截图标记为可缓存,只保留最近几张全分辨率图,可以砍掉大部分输入成本。
7. 滚动失效。 scroll 在部分页面(滚动容器事件、iframe、canvas)不生效。键盘替代(Page Down / 空格)更稳。zoom 工具(20251124 版)用于小目标(复选框、下拉箭头)的精细定位。
8. 提示注入是常态不是例外。 官方文档原话:"Claude 有时会遵循内容中的指令,即使与你的指令冲突"。防御组合拳:官方 20251124 工具默认内置注入检测分类器(这只是第一层不是全部)+ 坐标围栏 + 不可逆动作 HITL + 最小权限(不需要下载文件就不给下载能力)。
9. 别给 Agent 下发开放式大任务。 成功率数据很说明问题:窄域固定流程的任务可以做到 95%+,动态 UI 或跨多应用的任务会显著掉到 60-70%。正确姿势是把任务拆小、限定允许的站点和应用、明确"什么算完成"、起点屏幕状态可预期。CAPTCHA、会话中途超时、任务运行中 UI 变版,都是已知掉分点------遇到就干净利落地转人工,不要让模型硬试。
五、总结:模型负责看和想,Java 负责"敢让人少盯着"
回看整套系统:感知-行动循环的协议部分只有几十行,坐标缩放是个一元一次方程;真正决定这套系统能不能上生产的,是步数预算与停滞熔断、坐标围栏与 HITL 审批、一次性沙箱与审计日志、以及"失败要显式、成功要有据"的工程纪律。这些全是 Java 工程师的老手艺。
2026 年的 Agent 落地分水岭,已经不是"模型能不能做",而是"系统敢不敢让人少盯着一点"。Computer Use 把这条线又往前推了一步------当 AI 可以直接操作你企业的遗留系统时,决定上限的是执行层的工程化能力。而执行层,从来就是 Java 的主场。