Java 后端调用大模型 API 实战

Java 后端调用大模型 API 实战

作者:xianhao · 30 天 AI 学习计划 Day 07 复盘
技术栈:Java 17 + Spring Boot 3 + DeepSeek API

写在前面

作为一名 Java 后端开发者,第一周的学习目标是:从零搭建一个可运行的 Chat API 服务。不是调个 Demo 就完事,而是把生产环境会遇到的几个核心问题都走一遍——API 调用、流式输出、错误重试、会话管理。

本文记录我的完整实战过程,代码已开源。


一、为什么选 DeepSeek?

DeepSeek API 兼容 OpenAI 的 Chat Completions 格式,这意味着:

  • 请求体结构通用:model + messages + stream
  • Java 侧可以直接用 RestTemplateHttpClient,无需专用 SDK
  • 后续切换到 GPT、通义千问等模型,改动成本极低

一个标准的请求长这样:

{
  "model": "deepseek-chat",
  "messages": [
    { "role": "user", "content": "你好" }
  ],
  "stream": false
}

二、Day 03:最简 API 调用

核心思路

Spring Boot 项目 + RestTemplate 发 POST 请求,解析 JSON 响应。

Map<String, Object> body = Map.of(
    "model", "deepseek-chat",
    "messages", List.of(Map.of("role", "user", "content", message)),
    "stream", false
);

ResponseEntity<String> response = restTemplate.exchange(
    url, HttpMethod.POST, new HttpEntity<>(body, headers), String.class
);

API Key 管理

绝对不要把 Key 写死在代码里。 我的做法:

  1. application.yml 放占位符
  2. application-local.yml 放真实 Key,加入 .gitignore
  3. 提供 application-local.yml.example 给其他人参考

踩坑

  • DeepSeek 返回的 choices[0].message.content 路径要判空
  • usage 字段包含 prompt_tokenscompletion_tokens,后面计费要用

三、Day 04:SSE 流式输出

为什么需要流式?

同步接口用户要等 5~30 秒才看到完整回复,体验很差。流式输出让用户看到「打字机效果」,感知延迟大幅降低。

技术选型

层级 方案
上游(DeepSeek) stream: true,读 SSE 行 data: {...}
下游(浏览器) Spring SseEmitter + 前端 fetch + ReadableStream

关键代码

调用上游(HttpClient 读流):

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(url))
    .header("Authorization", "Bearer " + apiKey)
    .POST(HttpRequest.BodyPublishers.ofString(requestJson))
    .build();

HttpResponse<InputStream> response = httpClient.send(
    request, HttpResponse.BodyHandlers.ofInputStream()
);

try (BufferedReader reader = new BufferedReader(
        new InputStreamReader(response.body()))) {
    String line;
    while ((line = reader.readLine()) != null) {
        if (line.startsWith("data: ") && !line.contains("[DONE]")) {
            String content = parseDeltaContent(line);
            onChunk.accept(content);
        }
    }
}

推送给前端:

@PostMapping(value = "/api/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter streamChat(@RequestBody ChatRequest request) {
    SseEmitter emitter = new SseEmitter(120_000L);
    CompletableFuture.runAsync(() -> {
        chatService.streamChat(request.sessionId(), request.message(),
            chunk -> emitter.send(SseEmitter.event().data(chunk)),
            record -> {
                emitter.send(SseEmitter.event().name("usage").data(usageJson));
                emitter.send(SseEmitter.event().name("done").data("[DONE]"));
                emitter.complete();
            }
        );
    });
    return emitter;
}

踩坑

  • 流式请求要加 stream_options: { include_usage: true } 才能在最后拿到 Token 统计
  • SseEmitter 要设超时(我设了 120 秒),否则长回复会断
  • 前端用 fetch 而非 EventSource,因为 EventSource 不支持 POST

四、Day 05:重试、限流与成本控制

指数退避重试

大模型 API 不稳定是常态。429(限流)、5xx(服务端错误)都应该重试。

public <T> T execute(Supplier<T> action) {
    int attempt = 0;
    long delayMs = initialDelayMs;  // 1000ms

    while (true) {
        try {
            return action.get();
        } catch (RetryableApiException e) {
            attempt++;
            if (attempt >= maxAttempts) throw e;
            Thread.sleep(delayMs);
            delayMs = Math.min((long)(delayMs * multiplier), maxDelayMs);
        }
    }
}

重试序列:1s → 2s → 4s(最多 3 次)。

滑动窗口限流

防止自己或用户把 API 打爆:

// 每分钟最多 10 次请求
rateLimiterService.acquire();  // 超限抛 429

Token 计费

每次调用记录 promptTokenscompletionTokens,按模型单价算费用:

费用 = input_tokens × 输入单价 + output_tokens × 输出单价

DeepSeek Chat 输入约 $0.27/百万 tokens,输出约 $1.10/百万 tokens。单次对话几分钱,但架不住量大——成本意识要从第一天建立


五、Day 06:多轮对话会话管理

问题

大模型 API 本身无状态。每次请求都是独立的,它不记得你上一轮说了什么。要实现多轮对话,必须把历史消息一起发过去

设计方案

sessionId → [user_msg_1, assistant_msg_1, user_msg_2, assistant_msg_2, ...]

每次对话流程:

  1. 客户端带 sessionId 发消息
  2. 服务端把 user 消息写入历史
  3. 读取历史 + 注入 system prompt + 上下文裁剪
  4. 完整 messages 数组发给 API
  5. assistant 回复写入历史

存储抽象

public interface ChatHistoryRepository {
    void saveMessage(ChatMessage message);
    List<ChatMessage> getMessages(String sessionId);
    void clearSession(String sessionId);
}

两个实现,配置切换:

  • InMemoryChatHistoryRepository — 本地开发零依赖
  • RedisChatHistoryRepository — 生产环境,支持 TTL 和分布式

上下文裁剪

历史越长,input tokens 越多,费用线性增长。默认保留最近 10 轮(20 条消息):

private List<ChatMessage> truncate(List<ChatMessage> messages, int maxRounds) {
    int maxMessages = maxRounds * 2;
    if (messages.size() <= maxMessages) return messages;
    return messages.subList(messages.size() - maxMessages, messages.size());
}

六、最终 API 一览

方法 路径 功能
POST /api/session 创建会话
GET /api/session/{id}/messages 查看历史
DELETE /api/session/{id}/messages 清空对话
POST /api/chat 同步多轮对话
POST /api/chat/stream 流式多轮对话
GET /api/usage/stats Token 用量统计
GET /api/usage/rate-limit 限流状态

七、第一周复盘

完成情况

任务 状态
API 调用 ✅ RestTemplate + HttpClient 双通道
流式输出 ✅ SSE 端到端打通
重试机制 ✅ 指数退避,覆盖 429/5xx
会话管理 ✅ Memory/Redis 双存储,支持裁剪
可运行服务 mvn spring-boot:run 即可体验

收获

  1. 大模型 API 的本质就是 HTTP — 没有黑魔法,RestTemplate 就能调
  2. 流式是体验的关键 — 用户不在乎总耗时,在乎首字出现的时间
  3. 重试和限流是生产必备 — Demo 可以不要,上线绝对不能省
  4. 会话管理是成本杠杆 — 不裁剪上下文,费用会指数增长

下周计划

  • Day 08:Spring AI 入门,用框架简化 API 调用
  • 逐步把手写 HTTP 调用迁移到 Spring AI 的 ChatClient

参考


如果这篇文章对你有帮助,欢迎 Star 项目或在评论区交流。

posted @ 2026-07-08 16:34  程序员鲜豪  阅读(8)  评论(0)    收藏  举报