1. 为什么要在 Spring Boot 里接 DeepSeek API
先聊点实际的。最近大模型 API 越来越普及,DeepSeek 凭借便宜、上下文窗口大、推理能力强的特点,成了很多后端项目接入 LLM 的首选。但我在社区里看到不少朋友问"deepseek api如何调用""spring boot怎么集成"这类问题,说明大家真正的痛点不是写 Prompt,而是怎么把模型能力干净利落地嵌进现有的 Java 后端服务里。
这篇文章就从我实操的角度,完整记录我在 Spring Boot 项目里集成 DeepSeek API 的全过程。包括工程搭建、核心调用代码、流式输出、结构化解析、异常处理、安全防护,以及生产环境里那些文档上不会写的问题。适合正在做 Java 后端、想把大模型能力接进业务系统的读者,无论是做智能客服、内容生成、代码辅助还是数据分析,这套方案都能直接拿来改。
我先说结论:DeepSeek API 兼容 OpenAI 的请求协议,这意味着你在 Spring Boot 里可以用标准的 HTTP 客户端工具,以很低的成本完成接入。但真正决定项目成败的,往往是细节——超时设置、流式处理、密钥管理、并发控制,这些才是本文的重点。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 准备阶段:从 API Key 到工程骨架
2.1 获取 API Key 与基础信息
在写任何代码之前,先搞定凭证。DeepSeek 开放平台的 API Key 申请流程很常规:注册账号、登录控制台、创建 API Key、充值。这里提醒一点,API Key 创建出来只显示一次,一定要立刻复制保存到本地密码管理器里,一旦关掉页面就只能重新创建了。
DeepSeek API 的 Base URL 是 https://api.deepseek.com(部分文档也提到可以用 https://api.deepseek.com/v1,两者在请求路径上略有区别,但官方推荐的是前者)。模型名称主要有 deepseek-chat(对话模型)和 deepseek-reasoner(推理模型)。deepseek-chat 适合日常对话、内容生成、代码补全;deepseek-reasoner 则擅长复杂的逻辑推理和数学问题,会在回答前先生成一段思维链。实际项目里我通常默认用 deepseek-chat,只有在需要深度推理的场景才切换。
还有一个值得注意的信息:DeepSeek API 的响应格式与 OpenAI 保持兼容,也就是说,如果你之前项目里集成过 OpenAI 的接口,迁移过来几乎零成本。这一点对技术选型影响很大,意味着 Spring Boot 生态里成熟的 OpenAI 客户端库、HTTP 工具、结构化输出方案都能直接复用。
2.2 Spring Boot 工程初始化
我用的是 Java 17 + Spring Boot 3.2.x,这个组合在当前版本里最稳定。如果你还在用 Spring Boot 2.x,要注意 RestClient 是 Spring 6.1 才引入的,旧版本要么升级,要么改用 RestTemplate。
工程依赖只需要三个核心的:
xml复制<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
不需要引入任何 OpenAI 或 DeepSeek 的专属 SDK,因为本质上就是一次 HTTP POST 请求。用标准 HTTP 客户端的好处是可控性最强,不会被某个 SDK 的封装限制住,后续要做流式、重试、超时控制都特别顺手。
2.3 配置文件里的关键参数
在 application.yml 里定义 DeepSeek 相关配置:
yaml复制deepseek:
api-key: ${DEEPSEEK_API_KEY:}
base-url: https://api.deepseek.com
model: deepseek-chat
max-tokens: 2048
temperature: 0.7
timeout-seconds: 60
这里必须强调一个安全习惯:API Key 绝对不要硬编码在 YAML 文件里提交到 Git 仓库。我见过太多项目因为密钥被提交到 GitHub,结果被别人盗刷,损失惨重。正确做法是用环境变量注入,本地开发可以在 IDEA 的运行配置里设置 DEEPSEEK_API_KEY,生产环境用部署平台的密钥管理服务(如 K8s Secret、云厂商的密钥管理系统)注入。
3. 核心调用实现:同步、流式、结构化
3.1 搭建 HTTP 客户端
Spring Boot 3.2 以后,RestClient 是我首选的 HTTP 客户端。它相比 RestTemplate 更现代,相比 WebClient 更轻量,API 设计也符合直觉。初始化配置:
java复制@Configuration
public class DeepSeekConfig {
@Value("${deepseek.api-key}")
private String apiKey;
@Value("${deepseek.base-url}")
private String baseUrl;
@Value("${deepseek.timeout-seconds}")
private int timeoutSeconds;
@Bean
public RestClient deepSeekRestClient() {
return RestClient.builder()
.baseUrl(baseUrl)
.defaultHeader("Authorization", "Bearer " + apiKey)
.defaultHeader("Content-Type", "application/json")
.requestFactory(createRequestFactory())
.build();
}
private ClientHttpRequestFactory createRequestFactory() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(Duration.ofSeconds(10));
factory.setReadTimeout(Duration.ofSeconds(timeoutSeconds));
return factory;
}
}
ClientHttpRequestFactory 必须要单独配置,因为 Spring 默认的 JDK HttpURLConnection 对连接池、超时控制都不友好。用 SimpleClientHttpRequestFactory 可以精细设置连接超时和读取超时。如果请求量很大,建议换成 Apache HttpClient 5 或 OkHttp 的实现,支持连接池复用,性能会好很多。
超时时间的设置策略也值得聊聊:连接超时建议 10 秒以内;读取超时根据模型响应速度来定,普通对话 30~60 秒,流式响应可以放宽到 120 秒甚至更长。为什么?因为大模型的生成是逐 token 输出的,思考复杂问题可能耗时较长,读取超时设太短会导致任务被误杀。
3.2 请求与响应模型
DeepSeek 的 Chat Completion 接口接受如下请求体:
json复制{
"model": "deepseek-chat",
"messages": [
{"role": "system", "content": "你是一个智能助手"},
{"role": "user", "content": "你好,介绍一下你自己"}
],
"stream": false,
"temperature": 0.7,
"max_tokens": 2048
}
对应 Java 类:
java复制@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ChatRequest {
private String model;
private List<Message> messages;
private Boolean stream;
private Double temperature;
private Integer max_tokens;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class Message {
private String role;
private String content;
}
}
响应体:
java复制@Data
public class ChatResponse {
private String id;
private String object;
private Long created;
private String model;
private List<Choice> choices;
private Usage usage;
@Data
public static class Choice {
private Integer index;
private Message message;
private String finish_reason;
}
@Data
public static class Message {
private String role;
private String content;
}
@Data
public static class Usage {
private Long prompt_tokens;
private Long completion_tokens;
private Long total_tokens;
}
}
这里有个细节:DeepSeek 响应里的 content 字段在 deepseek-reasoner 模式下可能还包含 reasoning_content 字段,用于存放思维链内容。如果你的业务需要展示推理过程,需要在 Message 类里额外加一个 reasoning_content 字段,否则反序列化会丢失这部分数据。
3.3 同步调用与业务封装
同步调用是最简单的接入方式,适合对实时性要求不高的场景:
java复制@Service
public class DeepSeekService {
private final RestClient restClient;
private final DeepSeekProperties properties;
public DeepSeekService(RestClient deepSeekRestClient, DeepSeekProperties properties) {
this.restClient = deepSeekRestClient;
this.properties = properties;
}
public String chat(String systemPrompt, String userMessage) {
ChatRequest request = ChatRequest.builder()
.model(properties.getModel())
.messages(List.of(
new ChatRequest.Message("system", systemPrompt),
new ChatRequest.Message("user", userMessage)
))
.stream(false)
.temperature(properties.getTemperature())
.max_tokens(properties.getMaxTokens())
.build();
ChatResponse response = restClient.post()
.uri("/chat/completions")
.body(request)
.retrieve()
.body(ChatResponse.class);
if (response == null || response.getChoices() == null || response.getChoices().isEmpty()) {
throw new DeepSeekException("DeepSeek API returned empty response");
}
return response.getChoices().get(0).getMessage().getContent();
}
}
注意 messages 列表的构造,role 有三种取值:system(系统提示词)、user(用户输入)、assistant(模型历史回复)。多轮对话时,要把历史消息全部传给 API,模型本身是无状态的,每次请求都是独立的,上下文完全靠 messages 列表来维护。
3.4 流式输出:让响应像打字机一样流畅
如果产品形态是聊天机器人或者文本生成工具,你绝对不想让用户等 10 秒才看到第一段文字。流式输出(stream: true)可以在模型生成第一个 token 时就推送数据,体验好很多。
Spring 里的实现思路是用 WebClient 或 RestClient 的 exchange 方法拿到响应流,然后逐行解析 SSE(Server-Sent Events)数据。这里我直接给一个基于 WebClient 的完整实现:
java复制@Service
public class DeepSeekStreamService {
private final WebClient webClient;
public DeepSeekStreamService(@Value("${deepseek.base-url}") String baseUrl,
@Value("${deepseek.api-key}") String apiKey) {
this.webClient = WebClient.builder()
.baseUrl(baseUrl)
.defaultHeader("Authorization", "Bearer " + apiKey)
.build();
}
public Flux<String> streamChat(String systemPrompt, String userMessage) {
ChatRequest request = ChatRequest.builder()
.model("deepseek-chat")
.messages(List.of(
new ChatRequest.Message("system", systemPrompt),
new ChatRequest.Message("user", userMessage)
))
.stream(true)
.temperature(0.7)
.build();
return webClient.post()
.uri("/chat/completions")
.bodyValue(request)
.accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlux(String.class)
.filter(line -> line.startsWith("data: "))
.map(line -> line.substring(6))
.filter(data -> !"[DONE]".equals(data))
.map(this::parseDeltaContent)
.filter(Objects::nonNull);
}
private String parseDeltaContent(String jsonData) {
try {
JsonNode node = new ObjectMapper().readTree(jsonData);
return node.path("choices").path(0).path("delta").path("content").asText(null);
} catch (Exception e) {
return null;
}
}
}
Controller 层用 text/event-stream 返回:
java复制@GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamChat(@RequestParam String message) {
return deepSeekStreamService.streamChat("你是一个专业助手", message);
}
前端用 EventSource 或 fetch + ReadableStream 就能接收。注意 CORS 配置要放开 text/event-stream 的跨域限制,这是初用流式接口时最容易踩的坑。
流式解析有个常见的兼容性坑:每个 data 块里的 delta 字段可能包含 content、reasoning_content、tool_calls 等不同字段。解析时既要处理内容为空的情况,又要兼容可能出现的多段内容拼接,用 asText(null) 可以在字段不存在时返回 null,避免 NPE。
3.5 结构化输出:让模型返回 JSON 而不是散文
业务系统里调用大模型,很少只是随便聊几句,更多时候是希望模型返回可解析的 JSON 数据。比如让模型从一段文本中提取关键词、生成一个测试用例、判断用户意图等等。如果模型返回的内容里夹杂着解释性文字,解析 JSON 就会失败。
DeepSeek API 支持 response_format 参数,设置为 {"type": "json_object"} 可以强制模型输出 JSON:
java复制public <T> T chatAsObject(String prompt, Class<T> clazz) {
ChatRequest request = ChatRequest.builder()
.model(properties.getModel())
.messages(List.of(
new ChatRequest.Message("system",
"你是一个数据提取助手。请严格返回 JSON 格式,不要包含任何解释文字。"),
new ChatRequest.Message("user", prompt)
))
.responseFormat(Map.of("type", "json_object"))
.stream(false)
.max_tokens(2048)
.build();
String content = chat(request);
return new ObjectMapper().readValue(content, clazz);
}
使用 response_format 时有两个前提:第一,请求消息里必须包含单词 json(在 system prompt 里写明即可),否则 API 会报错;第二,虽然强制了 JSON 格式,但模型偶尔仍可能产生非法 JSON,比如漏括号、多余的逗号,所以解析时要做防御性处理,解析失败时可以设计一个"修正提示"——把原始输出回传给模型,让它修复 JSON。
这里我踩过一个印象很深的坑:生产环境上一次结构化提取任务失败率高达 15%,排查半天发现不是 API 问题,而是我传给模型的目标格式里包含了复杂的嵌套结构,导致模型生成 JSON 时频繁出错。后面把目标数据结构简化成扁平化设计,失败率一下就降到了 1% 以下。所以如果你的 JSON 结构足够复杂,不妨考虑拆分成多个简单的提取步骤,或者用 JSON Schema 去约束它。
4. 异常处理与重试策略
4.1 HTTP 状态码与常见错误对照
DeepSeek API 在异常时会返回标准的 HTTP 状态码,我整理了一份速查表:
| HTTP 状态码 | 含义 | 处理建议 |
|---|---|---|
| 200 | 成功 | 无 |
| 400 | 请求格式错误 | 检查请求体、消息格式、role 取值 |
| 401 | 认证失败 | 检查 API Key 是否正确、是否过期 |
| 402 | 余额不足 | 充值或触发告警通知 |
| 403 | 权限不足 | 检查账号是否有模型访问权限 |
| 404 | 请求路径错误 | 检查 Base URL 和 URI |
| 429 | 请求速率超限 | 触发限流,退避重试 |
| 500 | 服务器内部错误 | 重试,推荐指数退避 |
| 503 | 服务暂不可用 | 重试,注意退避时间 |
Spring 里的统一处理方式是用 RestClient 的 onStatus 方法注册异常回调:
java复制RestClient restClient = RestClient.builder()
.baseUrl(baseUrl)
.defaultHeader("Authorization", "Bearer " + apiKey)
.requestFactory(factory)
.defaultStatusHandler(HttpStatusCode::isError, (request, response) -> {
String body = new String(response.getBody().readAllBytes(), StandardCharsets.UTF_8);
throw new DeepSeekApiException(response.getStatusCode().value(), body);
})
.build();
这样任何非 2xx 响应都会被转换成自定义异常,由全局异常处理器统一转换为业务友好的错误提示。
4.2 重试机制的实现与陷阱
网络请求总会有失败的时候,合理的重试策略能显著提升接口的可用性。但重试不是无脑重来,要考虑幂等性、退避时间和数量限制。
我常用的重试策略:
java复制public String chatWithRetry(String systemPrompt, String userMessage) {
int maxAttempts = 3;
int attempt = 0;
long backoffMillis = 1000;
while (attempt < maxAttempts) {
try {
return chat(systemPrompt, userMessage);
} catch (DeepSeekApiException e) {
int status = e.getStatusCode();
// 4xx 异常不重试,重试也是同样的结果
if (status >= 400 && status < 500) {
throw e;
}
// 5xx 或网络异常,退避重试
if (attempt == maxAttempts - 1) {
throw e;
}
attempt++;
Thread.sleep(backoffMillis * attempt); // 指数退避:1s, 2s, 4s...
} catch (ResourceAccessException e) {
// 网络连接异常,IO 超时,重试
if (attempt == maxAttempts - 1) {
throw e;
}
attempt++;
Thread.sleep(backoffMillis * attempt);
}
}
throw new DeepSeekException("Unexpected error");
}
重试最关键的判断标准是:4xx 错误绝不重试。401(密钥错误)、400(参数错误)、402(余额不足)这些不是重试能解决的,重试只会浪费时间和 API 配额。5xx 和网络超时才是重试的适用场景。
4.3 限流与并发控制
DeepSeek API 有速率限制(RPM/token 速率),超出后会返回 429。在 Spring Boot 里可以用 Bucket4j 或 Resilience4j 做限流,防止上游接口被自己的并发请求打爆。
以 Resilience4j 为例,核心配置:
yaml复制resilience4j:
ratelimiter:
instances:
deepSeek:
limit-for-period: 60
limit-refresh-period: 1m
timeout-duration: 5s
Java 侧使用:
java复制@RateLimiter(name = "deepSeek")
public String chat(String systemPrompt, String userMessage) {
// ...
}
limit-for-period: 60 表示每分钟最多 60 次请求,超过后如果等待时间超过 5 秒就直接拒绝。这个参数要根据你账号的实际配额来调整,不能写死。
还有一个容易忽略的点:max_tokens 上限会影响速率。如果设置的 max_tokens 很大(比如 8000),单次请求消耗的 token 配额多,也许你 1 分钟只打了 50 个请求但总 token 数已经超限了。这种情况下不仅要限制请求次数,还要监控总 token 消耗。
5. 安全与密钥管理实战
5.1 密钥保护:环境变量与加密存储
前面提到了环境变量注入,这里展开讲讲生产环境的方案。对于 Spring Boot 应用,我推荐组合拳:
先看环境变量注入方案。在 Linux 服务器上,可以在 systemd service 文件或容器编排中注入:
bash复制export DEEPSEEK_API_KEY=sk-xxxxxxx
java -jar app.jar
再看配置加密方案。如果密钥需要落盘,比如数据库配置、Nacos 里的配置中心,强烈建议用 Jasypt 对敏感字段加密:
xml复制<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
然后在配置里用加密后的密文:
yaml复制deepseek:
api-key: ENC(加密后的密文)
启动时通过参数传入加解密密钥:
bash复制java -jar app.jar -Djasypt.encryptor.password=你的加解密密码
这样就算配置文件泄露,API Key 本身也不会暴露。我在实际项目中就遇到过配置文件被误传到内网公共仓库的情况,幸好当时用了 Jasypt,避免了一次安全事故。
5.2 防止 Key 从前端泄露
很多初级开发者在做前端直连大模型 API 时,会直接把 API Key 写在 JS 代码里,这是灾难性的。正确做法是:所有对大模型 API 的调用必须由后端代理。
前端的调用路径应该是:浏览器 -> 你的后端接口 -> DeepSeek API -> 后端处理 -> 返回前端。前端永远接触不到 API Key,只调用你自己的后端接口。
这不仅是安全问题,还有业务层面的考虑:后端可以在中间层做用户鉴权、请求日志、内容审核、敏感词过滤、token 消费统计。如果前端直连,这些能力全部丢失。
我在项目里设计了一个 ChatController,先检查当前登录用户的权限和配额,再调用 DeepSeek 服务,记录 token 消耗后返回结果:
java复制@PostMapping("/api/chat")
public ChatResponseDto chat(@RequestBody ChatRequestDto request,
@AuthenticationPrincipal UserPrincipal user) {
// 1. 校验用户是否有调用权限
// 2. 校验用户今日配额是否用完
// 3. 记录请求日志
// 4. 调用 DeepSeek API
// 5. 统计 token 消耗并更新用户配额
return deepSeekService.chatWithQuota(request, user.getId());
}
5.3 内容和数据安全
调用外部大模型 API,必然面临数据传输到第三方服务的问题。如果你处理的是用户隐私数据,需要在产品层面明确告知用户,并在技术上做脱敏处理。比如屏蔽身份证号、手机号、银行卡号等敏感信息后再发送给模型。
我的习惯是在 Service 层加一个敏感信息过滤器:
java复制private String maskSensitiveData(String content) {
// 手机号脱敏
content = content.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
// 身份证脱敏
content = content.replaceAll("(\\d{4})\\d{10}(\\w{4})", "$1**********$2");
return content;
}
模型返回的内容也需要做合规过滤,尤其是面向 C 端用户的产品。可以在后端接一层内容审核,过滤涉政、涉黄、暴恐等违规内容。这类词语过滤配合大模型本身的价值观对齐,能构筑安全底线。
6. 生产环境实践经验与性能优化
6.1 连接池与线程池配置
前面提到,当请求量上来后,SimpleClientHttpRequestFactory 就不够用了。我实测对比过:用 JDK 默认 HTTP 客户端压测,100 并发下错误率明显上升;换成 Apache HttpClient 5 + 连接池后,同样并发下非常稳定。
推荐用 Apache HttpClient 5 作为请求工厂:
java复制@Bean
public RestClient deepSeekRestClient() {
PoolingHttpClientConnectionManager connectionManager =
PoolingHttpClientConnectionManagerBuilder.create()
.setMaxConnTotal(200)
.setMaxConnPerRoute(50)
.build();
CloseableHttpClient httpClient = HttpClients.custom()
.setConnectionManager(connectionManager)
.setDefaultRequestConfig(RequestConfig.custom()
.setConnectionRequestTimeout(Timeout.ofSeconds(10))
.setResponseTimeout(Timeout.ofSeconds(60))
.build())
.setRetryStrategy(new DefaultHttpRequestRetryStrategy(3,
TimeValue.ofSeconds(1)))
.build();
return RestClient.builder()
.baseUrl(baseUrl)
.defaultHeader("Authorization", "Bearer " + apiKey)
.requestFactory(new HttpComponentsClientHttpRequestFactory(httpClient))
.build();
}
setMaxConnTotal(200) 表示连接池最多 200 个连接,setMaxConnPerRoute(50) 表示每个目标域名最多 50 个连接。实际并发量需要结合你的 Web 容器线程池大小来估算,连接数并不是越大越好,过大反而可能触发上游的限流。
6.2 缓存与上下文裁剪
大模型的 API 调用费用和 token 消耗高度相关,优化 token 使用是降本增效的关键。
第一层优化是缓存。对于幂等的请求(比如"写一段周报模板"),可以把结果缓存到 Redis 里,以 模型+消息内容 的哈希值作为 key。我实测过,合理的缓存能让 30% 的请求直接命中,费用直接降一个量级。
第二层优化是上下文长度控制。多轮对话场景中,如果把所有历史记录都发给模型,token 消耗会随着对话轮次线性增长。我的做法是:
- 保留 system prompt 和最近 N 轮对话(通常 6~10 轮);
- 超过长度限制的中间对话,用一条摘要来替代;
- 用
max_tokens限制生成长度,避免模型话痨。
给一个近似 token 估算的方式:中文文本大概是 "字符数 ÷ 1.5",英文文本大概是 "字符数 ÷ 4"。在开发环境下写一个工具类来估算消息列表的 token 总量,避免超限报错:
java复制public static int estimateTokens(List<ChatRequest.Message> messages) {
int total = 0;
for (ChatRequest.Message message : messages) {
String content = message.getContent();
int tokenCount = (int) Math.ceil(content.length() / 1.5);
total += tokenCount + 4; // 每条消息额外的元数据 token
}
return total;
}
这个估算结果不是精确值,但用来判断是否需要裁剪历史记录足够了。
6.3 测试与 Mock 策略
开发阶段反复调用真实 API 会产生费用,而且网络不稳定会影响开发效率。我建议在测试环境里用 WireMock 或 MockServer 模拟 DeepSeek API 的响应。
以 WireMock 为例,写一个 stub:
java复制@BeforeAll
static void setUp() {
WireMockServer server = new WireMockServer(8089);
server.start();
configureFor(8089);
stubFor(post(urlEqualTo("/chat/completions"))
.willReturn(aResponse()
.withHeader("Content-Type", "application/json")
.withBody("""
{
"choices": [
{
"message": {
"role": "assistant",
"content": "这是模拟的回复"
},
"finish_reason": "stop"
}
]
}
""")));
}
这样单元测试跑起来又稳定又快,也不烧钱。只有在集成测试阶段才打真实 API,并且准备好一个专用的测试账号,限制预算和速率,防止同事把额度测爆。
6.4 链路追踪与日志监控
大模型接口比传统接口更难排查问题,因为失败可能发生在网络层、网关层、模型层,而且响应内容是不确定性的。我强烈建议在项目里集成 OpenTelemetry 或 Micrometer Tracing,为每次请求生成 Trace ID。
日志记录的维度至少包括:
java复制log.info("DeepSeek call started, requestId={}, promptTokens={}, userMessageLength={}",
requestId, promptTokens, userMessage.length());
log.info("DeepSeek call finished, requestId={}, completionTokens={}, totalTokens={}, durationMs={}",
requestId, completionTokens, totalTokens, durationMs);
这样出了问题时,可以按 Trace ID 一键关联出请求参数、响应内容、耗时、token 消耗、失败原因。后续做监控告警时,这些指标也是主要数据源:
- P95 响应耗时超过阈值触发告警;
- 5xx 错误率超过 5% 触发告警;
- 日 token 消耗超过预算 80% 触发告警。
7. 常见问题与排查技巧实录
7.1 springfox 3.0.0 兼容性问题
有网友提到基于 Spring Boot 2.6+ 集成 springfox 3.0.0 时遇到问题。这个我有亲身经历:Spring Boot 2.6 开始,Spring MVC 的路径匹配策略从 AntPathMatcher 改成了 PathPatternParser,而 springfox 3.0.0 不兼容新策略,启动时会报空指针异常。
解决方案有两种:一是把路径匹配策略改回 Ant 模式:
yaml复制spring:
mvc:
pathmatch:
matching-strategy: ant_path_matcher
二是放弃 springfox,改用 springdoc-openapi(基于 OpenAPI 3 规范),这个方案我推荐新手直接使用。不过这些与 DeepSeek 集成无关,主要影响的是 API 文档展示,但确实会卡住一些人的工程启动流程,列在这里供参考。
7.2 响应超时的坑
DeepSeek 在处理长上下文或复杂推理时,响应时间可能超过默认的 HTTP 超时。我见过一个案例:同事用默认的 10 秒读取超时,结果每次对话稍长一点就直接报 SocketTimeoutException,反馈"API 不稳定"。把读取超时调到 60 秒后问题消失。
如果你用了 stream: true,超时逻辑又不一样。SSE 流式响应中,模型生成每个 token 之间的间隔不会太长,但总时长可能很长。此时更应该关注的是"连接空闲超时"而不是"总读取超时"。用 WebClient 时,可以通过 readTimeout 与 maxInMemorySize 配合,确保大响应不被截断。
7.3 上下文长度超限问题
DeepSeek 的上下文窗口虽然比很多模型大,但如果你的业务场景里用户输入特别长(比如整篇文章分析),还是可能触达上限。常见的报错信息是"maximum context length exceeded"。
我的排查思路是三步走:第一步,确认 messages 里有没有无意义的累积增长,比如重复把工具调用结果发回模型;第二步,用之前提供的 token 估算工具,在发送前主动检查;第三步,设计截断或摘要逻辑,超长时先让模型做摘要再处理。
7.4 捕获模型返回 JSON 解析失败
结构化输出虽然设置了 response_format 为 JSON,但偶尔仍会出现解析失败。我的防御性做法:
java复制try {
return objectMapper.readValue(content, clazz);
} catch (JsonProcessingException e) {
log.warn("JSON parse failed, raw content: {}", content);
// 尝试修正:把原始内容和错误信息交给模型修复
String fixedContent = chat("请修复以下 JSON 格式错误:" + content);
return objectMapper.readValue(fixedContent, clazz);
}
这个修复策略不是铁保证,但在我实践中大约能救回 50% 的失败案例。更可靠的方案是在 system prompt 里给出明确的 JSON 结构示例,而不是只写"返回 JSON 格式"。经验法则:给模型的结构示例越具体,输出越可靠。
7.5 连接池泄漏问题
如果项目运行一段时间后出现"connection pool exhausted"错误,大概率是连接没有释放。在 Spring 中使用 RestClient + Apache HttpClient 时,只要正确使用了 retrieve().body() 或 exchange(),连接会自动释放。但如果手动操作响应流,比如读取 InputStream 后忘记关闭,就会造成连接泄漏。
排查时可以监控连接池指标,Apache HttpClient 5 暴露了 PoolStats,包括 leased(当前租用)、available(可用)、pending(等待)。如果 leased 长期居高不下,说明代码里肯定有连接没归还。
8. 扩展场景:把 DeepSeek 能力接入实际业务
8.1 上门烹饪预约服务系统中的智能化
热搜词里出现了"基于Spring Boot的上门烹饪预约服务系统",这是个很有意思的场景。这类系统本质上是一个预约平台,涉及厨师管理、用户预约、订单流转,而大模型可以在里面承担多种角色。
比如智能推荐:根据用户的口味偏好、历史订单、预约时间,让 DeepSeek 生成个性化的菜品推荐。或者智能客服:用户在下单前咨询"我们家 5 口人,预算 800 元,能做什么菜",让模型根据规则引擎返回的菜单数据,生成人性化的答复。
这类场景的技术要点是:不能直接把业务数据丢给模型让它自由发挥,而是先用传统代码从数据库查出符合条件的菜单列表,再让模型基于这个列表生成推荐文案。模型负责表达,业务逻辑交给代码,各司其职才不会出错。
还有更高级的玩法:用 DeepSeek 的 Function Calling(函数调用)能力,让模型在对话过程中主动触发"查询订单状态""计算价格"等后端函数。模型决策何时调用哪个函数,系统执行函数并返回结果,模型再组织最终答复。这个模式是智能助理类应用的主流架构。
8.2 婚庆服务预约平台与内容生成
婚庆预约平台的核心是信息展示和预约转化,大模型可以在内容侧大显身手。比如根据新人提供的恋爱故事、预算、人数,自动生成婚礼策划方案;或者生成礼服搭配建议、婚礼邀请函文案。
这里我建议做成一个"内容工坊"模块,用户输入基础信息后,点击生成按钮,后端用 SSE 流式返回生成结果。同样要结合结构化输出,让生成结果同时以 JSON 形式入库,方便后续版本管理。
8.3 智能客服与知识库问答
如果要做垂直领域的知识库问答,RAG(检索增强生成)是绕不开的方案。流程是:用户提问 → 向量检索找到最相关的文档片段 → 把文档片段拼进 prompt → 交给 DeepSeek 生成回答。
Spring Boot 里的工程化实现大致是:文档离线切片并向量化存入向量数据库(如 Chroma、Milvus、Qdrant),在线查询时用 OpenAI Embedding 接口或本地模型做向量化,然后相似度检索,最后组装 prompt 调 DeepSeek。这个方案的好处是模型不需要"记住"知识库内容,每次回答都是基于当前检索到的资料,知识更新只需要更新向量库。
这套架构我建议在业务数据量稳定后再引入向量数据库,前期可以直接用 MySQL 做关键词检索兜底,体验差点但架构简单。做 RAG 时特别注意:单轮检索结果不要盲目全塞进 prompt,控制检索片段在 3~5 段,否则模型会"迷失在海量信息中",回答质量反而下降。
9. 一些个人体会
最后分享一点我跑了几个月生产环境的真实感受。DeepSeek API 的接入难度其实不高,Spring Boot 的生态又提供了非常顺手的工具,真正拉开差距的是工程细节:超时怎么配、重试怎么设计、密钥怎么保护、token 怎么控制、错误怎么排查。这些坑我基本都踩过一遍,文章里写的都是解决方案,不是理论推演。
如果你正要动手做类似的项目,我建议的顺序是:先跑通同步调用,明确业务需要什么格式的输出;再升级到流式响应,优化用户体验;然后根据自己的调用量决定是否需要连接池、限流、缓存。不要一上来就把架构设计得很重,大模型项目的迭代速度极快,轻量起步、按需演进才是务实的路线。
