☰
告别Python!Spring Boot 3集成GPT-5.4,Java后端10分钟接入原生计算机操作
2026/10/18 19:42:08 网站建设 项目流程

1. Java 后端接 GPT-5.4 计算机操作为什么总卡在第一步

先说清楚这篇要解决什么。GPT-5.4 的 computer use 能力,通俗讲就是模型能"看懂"屏幕截图,然后返回"点哪里、输入什么"的结构化指令。它不是一个能直接操控你电脑的魔法,而是一个视觉理解加动作决策的接口。你要做的,是把截图喂给它,把返回的坐标和动作解析出来,再交给本地程序去执行。

适合谁看?手上有一台跑着 Spring Boot 3 的机器,想让 Java 服务直接调用模型完成屏幕理解与操作指令的后端开发者。不需要你会 Python,不需要装一堆胶水脚本,全程 Java 生态闭环。

我试过用最朴素的方式接:新建 Spring Boot 3.2.x 工程,JDK 17,只勾 Spring Web。为什么不用 Spring AI?因为 computer use 这类新能力,官方 SDK 往往滞后,直接用 WebClient 打 REST 接口反而更灵活,也方便你理解底层到底发生了什么。等你摸清了请求体长什么样,后面换任何封装都是顺手的事。

核心链路只有四步:截屏转 Base64、拼多模态消息体、POST 到统一 API 通道、解析返回的 JSON 动作。听起来简单,但真正卡人的地方在于:模型 ID 写错、图片格式不对、坐标没换算、返回内容裹在 markdown 代码块里。这篇就把这些坑一次性铺平。

先明确一个概念,所谓"原生计算机操作",指的是模型输出的是动作意图,不是它真的伸手去点。执行动作的那只手,是你自己的 Java 代码。理解这一点,后面所有配置都不会跑偏。

2. TaoToken 前置:统一 API 通道与三件套准备

在写代码之前,得先把"路"修好。国内 Java 服务直连外部模型接口,常见问题是网络链路不稳定、鉴权头格式对不上、模型 ID 找不到。TaoToken 在这里扮演的是一个统一 API 通道的角色,把不同模型的调用收敛成一套 OpenAI 兼容的 REST 规范,你只需要认准三件套:Base URL、API Key、Model ID。

Base URL 用https://taotoken.net/api,注意这个地址不带任何查询参数,直接作为根路径。API Key 去控制台生成,路径是 API Keys 页面,生成后复制保存,它只会完整显示一次。Model ID 填你要调用的模型标识,computer use 场景要选支持视觉输入的模型版本。

三件套的对应关系建议记牢,后面配置文件里就是这三个值在起作用:

配置项取值来源作用
Base URLhttps://taotoken.net/api请求根地址,WebClient 的 baseUrl
API Key控制台 API Keys 页面生成放在 Authorization 头,Bearer 格式
Model ID模型列表里支持视觉的版本请求体 model 字段

如果你还没生成 Key,先去控制台把 Key 建好,顺手把接入文档过一遍,确认当前支持的模型 ID 写法。文档地址在接入文档页,里面有完整的请求示例,和这篇的代码能对上。

这里要提醒一句:不要把 Key 硬编码进代码提交到仓库。用环境变量或者配置中心注入,application.yml里写占位符,启动时通过--spring.profiles.active或者环境变量覆盖。这是基本的安全习惯,后面配置片段我会用占位符演示。

另外,如果你后续要做长期的编码 Agent 或者多步任务编排,可以了解下 Coding Plan,它更适合持续性的调用场景;只是验证模型能不能返回动作指令,用模型对话页面手动发一次请求就够了。两条路按需选,别一上来就上重装备。

3. 可复制配置:application.yml 与 WebClient 三件套

这一节是全文最该抄的部分。先给application.yml,路径放在src/main/resources/application.yml,内容如下:

server: port: 8080 taotoken: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY:sk-替换成你的Key} model: gpt-5.4-computer-use-preview timeout-seconds: 60 spring: codec: max-in-memory-size: 20MB

几个点解释一下。base-url固定不带斜杠结尾,WebClient 拼接时我们自己控制。api-key用${}占位,优先读环境变量TAOTOKEN_API_KEY,本地调试没设环境变量时才用默认值,上线务必用环境变量。model字段填支持视觉的模型 ID,写错会直接 404 或者报模型不存在。max-in-memory-size调大是因为截图 Base64 之后体积不小,默认 256KB 很容易触发DataBufferLimitException。

接着是 WebClient 的 Bean 配置,单独一个配置类:

import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.web.reactive.function.client.WebClient; import java.time.Duration; @Configuration public class WebClientConfig { @Value("${taotoken.base-url}") private String baseUrl; @Value("${taotoken.api-key}") private String apiKey; @Value("${taotoken.timeout-seconds:60}") private long timeoutSeconds; @Bean public WebClient taotokenWebClient() { return WebClient.builder() .baseUrl(baseUrl) .defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey) .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .codecs(c -> c.defaultCodecs().maxInMemorySize(20 * 1024 * 1024)) .build(); } }

注意defaultHeader里 Authorization 是Bearer加空格再加 Key,少个空格就是 401。baseUrl不要以/结尾,否则和后面的路径拼出双斜杠,部分网关会 404。

然后是请求体的构造。computer use 的请求是标准的多模态 chat completions 格式,content 数组里同时放文字和图片:

import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import org.springframework.stereotype.Component; @Component public class ComputerUseRequestBuilder { private final ObjectMapper mapper = new ObjectMapper(); public String build(String model, String base64Image, String instruction) { ObjectNode root = mapper.createObjectNode(); root.put("model", model); root.put("max_tokens", 2000); ArrayNode messages = root.putArray("messages"); ObjectNode userMsg = messages.addObject(); userMsg.put("role", "user"); ArrayNode content = userMsg.putArray("content"); ObjectNode textPart = content.addObject(); textPart.put("type", "text"); textPart.put("text", instruction + " 请分析当前屏幕,若需操作返回 JSON:" + "{\"action\":\"click|type|done\",\"x\":0,\"y\":0,\"text\":\"\"}"); ObjectNode imagePart = content.addObject(); imagePart.put("type", "image_url"); ObjectNode imageUrl = imagePart.putObject("image_url"); imageUrl.put("url", "data:image/png;base64," + base64Image); return root.toString(); } }

这段的关键在image_url.url必须是data:image/png;base64,前缀加纯 Base64,不能带换行。截图工具如果返回带换行的 Base64,先replaceAll("\\s", "")清一遍。

4. 验证请求:一次端到端跑通与结果解析

配置齐了,来跑一次真实请求。先写截屏工具,用 JDK 自带的java.awt.Robot,不引第三方库:

import org.springframework.stereotype.Component; import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.util.Base64; @Component public class ScreenCapture { public String captureBase64() { try { Robot robot = new Robot(); Rectangle rect = new Rectangle(Toolkit.getDefaultToolkit().getScreenSize()); BufferedImage image = robot.createScreenCapture(rect); ByteArrayOutputStream out = new ByteArrayOutputStream(); ImageIO.write(image, "png", out); return Base64.getEncoder().encodeToString(out.toByteArray()); } catch (Exception e) { throw new RuntimeException("截屏失败: " + e.getMessage(), e); } } }

然后是调用与解析的 Service:

import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; @Service public class ComputerUseService { private final WebClient client; private final ComputerUseRequestBuilder builder; private final ObjectMapper mapper = new ObjectMapper(); @Value("${taotoken.model}") private String model; public ComputerUseService(WebClient taotokenWebClient, ComputerUseRequestBuilder builder) { this.client = taotokenWebClient; this.builder = builder; } public String analyze(String base64Image, String instruction) { String body = builder.build(model, base64Image, instruction); String resp = client.post() .uri("/v1/chat/completions") .bodyValue(body) .retrieve() .bodyToMono(String.class) .block(); return extractContent(resp); } private String extractContent(String resp) { try { JsonNode root = mapper.readTree(resp); String content = root.path("choices").get(0) .path("message").path("content").asText(); if (content.contains("```json")) { int start = content.indexOf("```json") + 7; int end = content.lastIndexOf("```"); content = content.substring(start, end).trim(); } return content; } catch (Exception e) { throw new RuntimeException("解析返回失败: " + resp, e); } } }

注意uri("/v1/chat/completions"),因为 baseUrl 是https://taotoken.net/api,拼出来就是完整的 chat completions 路径。extractContent里处理了模型把 JSON 裹在 markdown 代码块的情况,这是最常见的解析坑。

最后写个 Controller 暴露接口,方便用 curl 验证:

import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/ai") public class ComputerUseController { private final ComputerUseService service; private final ScreenCapture capture; public ComputerUseController(ComputerUseService service, ScreenCapture capture) { this.service = service; this.capture = capture; } @PostMapping("/do-task") public String doTask(@RequestParam String instruction) { String image = capture.captureBase64(); return service.analyze(image, instruction); } }

启动服务后,用 curl 打一发:

curl -X POST "http://localhost:8080/api/ai/do-task" \ --data-urlencode "instruction=看到记事本窗口了吗?点击编辑区中间位置"

成功的话,返回体里choices[0].message.content会是一段 JSON,形如{"action":"click","x":820,"y":460,"text":""}。拿到这个坐标,你的执行器就能把鼠标移过去点击。到这一步,从 Spring Boot 到计算机操作接口的最小闭环就跑通了。想先手动确认模型返回格式,也可以去模型对话页面发一次带图的请求对照。

5. 本篇常见错排查:401、local proxy failed 与 reading choices

跑不通的时候,对照下面几个真实报错,基本能定位到问题。

401 Unauthorized。九成是 Key 的问题。检查三处:application.yml里 Key 有没有被环境变量覆盖成空值;Authorization 头是不是Bearer加空格;Key 有没有多余空格或换行。还有一种情况是 Key 复制时漏了尾部字符,重新去 API Keys 页面生成一个最省事。

local proxy failed / Connection refused。这类报错说明请求根本没出去,或者被本地网络策略拦了。先确认base-url写的是https://taotoken.net/api,没有多写路径。再确认机器能正常访问外网 HTTPS。如果是公司内网,检查有没有 HTTP 代理环境变量干扰,http_proxy这类变量会让 WebClient 走错出口。

reading choices 报错 / NullPointerException。典型是返回体结构和预期不一致。可能原因:模型 ID 写错,返回的是错误对象而不是正常响应;或者choices数组为空。排查方法是在extractContent里先把完整resp打日志,看看到底返回了什么。如果返回体里是{"error":{"message":"..."}},那就是模型 ID 或参数问题。

DataBufferLimitException: Exceeded limit on max bytes。截图太大,超过了 WebClient 默认缓冲。回到WebClientConfig,确认maxInMemorySize设到了 20MB。如果还不行,把截图分辨率降下来,或者压缩成 JPEG 再转 Base64。

坐标点偏 / 鼠标飞到屏幕外。模型返回的是像素坐标,而 Windows 的mouse_event用的是 0 到 65535 的绝对坐标,必须做换算。换算公式是absX = x * 65535 / screenWidth,absY = y * 65535 / screenHeight。漏了这步,鼠标就会乱跑。

返回内容解析失败。模型有时会把 JSON 包在 markdown 代码块里,有时前后带解释文字。稳妥做法是用正则提取第一个{到最后一个}之间的内容,再交给 Jackson 解析,比单纯判断```json更抗造。

6. 语义一致 CTA:把闭环接到你的业务里

最小闭环跑通之后,下一步就是把它接到真实业务。几个方向供参考:自动化测试里用它做视觉断言,页面改版了也能靠视觉理解找到元素;RPA 流程里替代固定 XPath,降低维护成本;后台运维场景里做屏幕巡检,让模型判断界面是否异常。

要长期跑这些任务,建议把调用封装成带重试和限流的客户端。重试针对网络抖动,限流针对模型的每分钟请求数限制,用 Guava 的RateLimiter就能做。截图也可以做缓存,页面没变化就别重复传,省 token 也省时间。

需要生成 Key、查看完整接入示例,去 API Keys 和接入文档两个页面。想先手动验证模型对某张截图的返回,用模型对话页面发一次带图请求最快。如果打算做持续性的编码 Agent 或多步任务编排,Coding Plan 更适合这种长期调用场景。

最后留个实用习惯:把extractContent的原始返回打日志,前几次调试全靠它。等格式稳定了再关掉日志,避免刷屏。这套东西我踩过的坑基本都在第 5 节里了,照着排查,十分钟跑通不是问题。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询