Computer Use Agent Java 落地实战:感知-行动循环、执行器与安全围栏生产级实现

一、为什么 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"]
}

注意两个关键点:

  1. type 字段是版本号,不同代模型绑定不同工具版本------Claude 4.6 家族(Opus 4.6/Sonnet 4.6/Haiku 4.5)用 computer_20251124,配 beta header computer-use-2025-11-24;Sonnet 4.5 及更早模型用 computer_20250124 配 computer-use-2025-01-24。模型、工具版本、beta header 三者必须配套,配错直接 400。
  2. 除了 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);
    }
}

这里有两个刻意的生产细节:

  1. type 动作加 --delay 40:老系统的表单是 JS 逐字段校验的,输入太快会丢字符;键盘事件之间留 40ms 是实测稳态。
  2. 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 的主场。

相关推荐
又过二十1 小时前
结合Godot八向图实现网页角色设计器测试
ai·aigc·godot·game·qwen-image·游戏素材
文丑颜不良啊2 小时前
LeetCode2161~2180题目解析
java·leetcode
aicat_cn2 小时前
json-render 接入 Chat SDK:Agent 在 Slack 和 Teams 里直接回表格和按钮
ai·大模型
文丑颜不良啊4 小时前
LeetCode2141~2160题目解析
java·leetcode
LabVIEW开发4 小时前
LabVIEW 连接板:28 个端子怎么排,优先级怎么定
java·算法·labview
天天被压力4 小时前
【A股分钟级高频数据完全手册 #07】北交所历史分钟K线接口:京市Pro版分时批量
java·人工智能·python
天天被压力5 小时前
【A股分钟级高频数据完全手册 #08】股票日、分时数据接口对比:普通分时与Pro版差异实测
java·人工智能·python
特立独行的猫A5 小时前
AI 时代,我为什么推荐用 cj-tauri 开发桌面应用
java·后端·python
金銀銅鐵5 小时前
从一个类名到一张类图:用反射自动生成 PlantUML
java·后端·uml