Administrator
发布于 2023-04-16 / 11417 阅读
54

Java 接入大模型的第一种方式:HTTP 调用与流式响应

问题代码:第一版大模型调用把线程池打满了

四月我们想给工单系统接一个大模型做自动摘要。第一版我图省事,用 RestTemplate 直接 POST 到 OpenAI 兼容接口,同步等返回。压测一上来就出问题:模型平均响应 3 秒,线程池 200 个线程 10 秒就被占满,QPS 卡在 60 上不去。日志里全是 Timeout waiting for connection

排查:同步调用的天花板

大模型生成是流式输出的,等它一次性返回再处理,既浪费连接又让用户干等。正确姿势是走 SSE(Server-Sent Events),边生成边把 token 吐给前端。我换成 JDK 11 自带的 java.net.http.HttpClient,它原生支持响应体流式订阅。

根因:流式响应要自己按行解析

OpenAI 的 SSE 格式是每行 data: {...}\n\n,结束是 data: [DONE]。HTTP 客户端不会帮你拆帧,得在 BodyHandlers 里自己处理。关键点是不能用 ofString() 等整段读完,要用 BodyHandlers.ofInputStream() 拿流,再按行切。

解决方案:SSE 流式解析

核心解析逻辑:

HttpClient client = HttpClient.newBuilder()
    .connectTimeout(Duration.ofSeconds(5))
    .build();

HttpRequest req = HttpRequest.newBuilder()
    .uri(URI.create("https://api.example.com/v1/chat/completions"))
    .header("Content-Type", "application/json")
    .header("Authorization", "Bearer " + KEY)
    .POST(HttpRequest.BodyPublishers.ofString(body))
    .build();

HttpResponse<InputStream> resp = client.send(req,
    HttpResponse.BodyHandlers.ofInputStream());

BufferedReader br = new BufferedReader(
    new InputStreamReader(resp.body(), StandardCharsets.UTF_8));
String line;
while ((line = br.readLine()) != null) {
    if (line.startsWith("data: ")) {
        String payload = line.substring(6).trim();
        if ("[DONE]".equals(payload)) break;
        JsonNode node = mapper.readTree(payload);
        String token = node.at("/choices/0/delta/content").asText();
        // 推给前端 WebSocket
    }
}

超时与重试处理

三个坑必须兜住:

  • 连接超时connectTimeout(5s),防止 DNS 或网络抖动一直挂起。
  • 读取超时:HttpClient 本身没有整体 read timeout,得在订阅层用 CompletableFuture + orTimeout(30s) 兜底,否则慢连接会一直占着流。
  • 重试:只有 429(限流)和 5xx 才重试,且要带退避。我们用指数退避,最多 3 次:
int attempt = 0;
while (attempt < 3) {
    HttpResponse<?> r = client.send(req, ofInputStream());
    if (r.statusCode() == 429 || r.statusCode() >= 500) {
        Thread.sleep((long) (500 * Math.pow(2, attempt++)));
        continue;
    }
    break;
}

另外 429 的 Retry-After 头要尊重,限流时硬重试只会雪崩。

留个问题

关于《Java 接入大模型的第一种方式:HTTP 调用与流式响应》里这个坑,你当时是怎么处理的?欢迎在评论区聊聊你踩过的类似情况。

参考