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 侧可以直接用
RestTemplate或HttpClient,无需专用 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 写死在代码里。 我的做法:
application.yml放占位符application-local.yml放真实 Key,加入.gitignore- 提供
application-local.yml.example给其他人参考
踩坑
- DeepSeek 返回的
choices[0].message.content路径要判空 usage字段包含prompt_tokens、completion_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 计费
每次调用记录 promptTokens、completionTokens,按模型单价算费用:
费用 = 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, ...]
每次对话流程:
- 客户端带
sessionId发消息 - 服务端把 user 消息写入历史
- 读取历史 + 注入 system prompt + 上下文裁剪
- 完整
messages数组发给 API - 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 即可体验 |
收获
- 大模型 API 的本质就是 HTTP — 没有黑魔法,RestTemplate 就能调
- 流式是体验的关键 — 用户不在乎总耗时,在乎首字出现的时间
- 重试和限流是生产必备 — Demo 可以不要,上线绝对不能省
- 会话管理是成本杠杆 — 不裁剪上下文,费用会指数增长
下周计划
- Day 08:Spring AI 入门,用框架简化 API 调用
- 逐步把手写 HTTP 调用迁移到 Spring AI 的
ChatClient
参考
- DeepSeek API 文档
- Spring Boot SSE 文档
- 项目源码:
deepseek-api-demohttps://github.com/xianhao42-crypto/deepseek-api-demo.git
如果这篇文章对你有帮助,欢迎 Star 项目或在评论区交流。
本文来自博客园,作者:程序员鲜豪,转载请注明原文链接:https://www.cnblogs.com/hg-blogs/p/21255593

浙公网安备 33010602011771号