尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Spring Boot集成DeepSeek API:非流式到流式调用实践与错误排查

发布时间:2026/9/28 18:13:33

资讯中心
01
ARTICLE

Spring Boot集成DeepSeek API:非流式到流式调用实践与错误排查

Spring Boot集成DeepSeek API:非流式到流式调用实践与错误排查
我在第一版的代码里遇到了一个很典型的问题单独用 RestTemplate 请求 DeepSeek API接口能通但一个错误提示几乎让人崩溃——400 This models maximum context length is 1048576 tokens。那篇文章断在这里估计很多读者也卡在这里。这次我把完整的 Spring Boot 实现思路、错误排查和流式升级方案一次性整理出来作为一个可直接上手的参考。记得第一次接到这个需求的时候团队后端是 Spring Boot 3.2 JDK 17产品要在一个新模块里接入大模型能力要求不能动现有前端架构也不能把整个服务推倒重来。最终选择是在 Spring Boot 项目里直接调用 DeepSeek API。原因很简单它的接口协议跟 OpenAI 的 Chat Completions 高度兼容Java 端不需要专门 SDK自己封装一个 REST 客户端就行。这篇文章就从选型开始讲环境搭建、非流式与流式调用、常见 400/401 报错以及 Java 21 虚拟线程在性能上的配合。目标读者是需要在 Spring Boot 服务里接入大模型 API 的后端同学希望能帮你们少踩几个我踩过的坑。1. 为什么在 Spring Boot 里接 DeepSeek而不是单独开一个 Python 代理服务1.1 什么情况下适合直接在 Java 服务里调用大模型 API我之前见过不少团队一提到大模型调用条件反射式地认为应该用 Python 写一个独立服务再让 Spring Boot 去调它。理由是 Python 生态里 LangChain、LlamaIndex 这些框架成熟方便做 agent。对于一个有大量 prompt 工程、工具调用、复杂记忆管理的项目这个思路没问题。但如果你的需求只是“给现有业务加一个 AI 接口”比如文本摘要、智能分类、生成回复、问答匹配那在 Spring Boot 里直接调模型 API 是最省事的方案。直接集成的好处有三个链路短Java 服务直接请求模型不用让请求多跳一层 Python 网关排查问题少一个环节。部署简单不引入新的语言运行时Docker 镜像、监控、日志这些基础设施全部复用现有体系。统一异常处理Java 服务已经有的统一返回体、重试机制、熔断降级可以直接用在这次调用上。判断标准我一般就一个如果核心价值在“编排逻辑”比如大模型只是其中一个环节后面还要接数据库、搜索引擎那可以考虑独立代理服务如果核心价值是“把模型能力嵌入业务”那直接在业务服务里写一个 Client 就够了。1.2 DeepSeek API 的三个特点决定了它在 Java 后端很好接第一个特点是协议兼容性。DeepSeek 的/chat/completions接口风格跟 OpenAI 基本一致Java 侧用RestTemplate或WebClient就可以调不需要额外安装任何模型 SDK。这意味着所有基于 OpenAI 协议封装好的 Spring AI Starter、开源客户端稍微改一下 Base URL 和模型名就能用。第二个特点是中文任务效果好。DeepSeek 系列模型在中文理解、抽取、文本润色上表现稳定这对国内业务场景很重要。很多情况下用同样 prompt 去对比其他模型DeepSeek 在中文长文本上的忠实度更好。第三个特点是上下文窗口大。DeepSeek 的上下文高达 1048576 tokens也就是 1M 级别。这恰好解决了我之前遇到的一个痛点产品让我做长文档摘要动辄几千行文本普通 128K 窗口要分段多次调还得拼接结果。DeepSeek 可以一次把文档塞进去简化了很多工程逻辑。1.3 既然有 Spring AI为什么我还推荐先手写Spring AI 是 Spring 官方做的大模型抽象层思路类似JdbcTemplate用ChatClient屏蔽厂商差异。如果你的服务是 Spring Boot 3.x 起步又想快速接入 OpenAI 兼容接口Spring AI 确实是个选项。它的配置类似spring: ai: openai: base-url: https://api.deepseek.com api-key: ${DEEPSEEK_API_KEY} chat: options: model: deepseek-chat实际用起来请求和响应封装都比较整洁。但我个人的建议是第一次接 DeepSeek 先用原生 HTTP 写一个最小实现让先跑通。原因有两个一是 Spring AI 的版本迭代快有时会调整 API 包名如果你不熟悉它的抽象出问题后很难判断是模型报错还是框架封装问题二是手写版本逻辑透明请求体、响应体、错误处理都在自己手里后期迁移到 Spring AI 或自定义 Agent 框架都更容易。2. 动手前先把环境准备好JDK 版本、Maven 依赖和密钥配置2.1 Spring Boot 3.x 与 JDK 的选择DeepSeek 官方对 Java 没有特殊要求所以选型完全取决于服务现状。我建议 Spring Boot 3.2 及以上JDK 17 起步。如果团队已经在用 Java 21那就更好后面要说的虚拟线程优化会用得上。Spring Boot 3.2 以上版本里spring-boot-starter-web默认基于 Spring MVC 6配合 RestTemplate 足够处理普通 REST 请求如果要流式输出还需要再引入 WebFlux 依赖。2.2 pom.xml 依赖怎么加HTTP 客户端选哪个如果只做非流式请求spring-boot-starter-web就够了因为里面包含了RestTemplate所需的 Spring MVC 核心。如果你想做 SSE 流式我建议加上 WebFlux用WebClient去消费事件流。不要指望RestTemplate在流式场景下体验好能跑但解析麻烦。我这边实际用到的依赖如下parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.5/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-webflux/artifactId /dependency /dependencies有人会问同时引入 spring-boot-starter-web 和 spring-boot-starter-webflux 会不会冲突实际上 Spring Boot 会自动识别 Web MVC 作为主配置WebClient只是作为客户端工具使用不影响现有的 Controller 运行。当然如果你不想引 WebFlux也可以用RestTemplate加ResponseExtractor手动读流但代码会繁琐一些。2.3 API Key 配置的正确方式环境变量 ConfigurationPropertiesAPI Key 不能硬编码在 Java 代码里也不能直接写在 application.yml 里提交到 Git。最稳妥的做法是用环境变量注入。配置文件这样写deepseek: api-key: ${DEEPSEEK_API_KEY} base-url: https://api.deepseek.com model: deepseek-chat max-tokens: 2048然后定义一个配置属性类ConfigurationProperties(prefix deepseek) public class DeepSeekProperties { private String apiKey; private String baseUrl; private String model; private int maxTokens; // getter / setter }在主类上加上EnableConfigurationProperties(DeepSeekProperties.class)之后在Service里注入就可以。这样密钥与环境绑定部署时通过配置中心或 CI/CD 注入即可。2.4 先用 curl 冒烟测试把问题控制在请求之外我踩过一个坑代码写了半天结果报 401才发现是环境变量没生效。其实用 curl 先验证一次一分钟就能定位问题。启动服务之前先跑一下export DEEPSEEK_API_KEY你的key curl -X POST https://api.deepseek.com/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer $DEEPSEEK_API_KEY \ -d {model: deepseek-chat, messages: [{role: user, content: 你好}], max_tokens: 20}如果能正常返回 JSON说明 Key、网络、模型名都正常。后面写 Java 代码遇到的 4xx/5xx 就可以明确归类到代码层。curl 这一步一定要做否则你无法区分是密钥问题还是代码问题。3. 非流式调用RestTemplate chat/completions 的最小可用实现3.1 DTO 设计请求、消息、响应我用 Java 21 的 record 来简化代码。数据结构上只需要三个核心类型请求体、消息体、响应体。public record DeepSeekMessage(String role, String content) {} public record DeepSeekChatRequest( String model, ListDeepSeekMessage messages, Double temperature, Integer max_tokens, Boolean stream ) { public static DeepSeekChatRequest of(String model, String userContent) { return new DeepSeekChatRequest( model, List.of( new DeepSeekMessage(system, 你是一个严谨的技术助手), new DeepSeekMessage(user, userContent) ), 0.7, 2048, false ); } } public record DeepSeekMessageResponse( String role, String content ) {} public record DeepSeekChoice( DeepSeekMessageResponse message ) {} public record DeepSeekUsage( int prompt_tokens, int completion_tokens, int total_tokens ) {} public record DeepSeekChatResponse( ListDeepSeekChoice choices, DeepSeekUsage usage ) {}temperature是采样温度一般文本生成场景设 0.7 左右代码生成或小任务可以更低比如 0.2。max_tokens控制单次回答最大长度默认 2048 够用。stream非流式下设为false即可。3.2 RestTemplate 连接超时配置与 Service 封装RestTemplate 需要一个 Bean这个 Bean 要配置连接超时和读取超时。DeepSeek 生成长文本时响应时间可能超过 30 秒读取超时不能设太短否则会出现“任务还在生成客户端已经超时断开”的情况。我自己的经验值是连接超时 5 秒读取超时 60 秒。Configuration public class RestTemplateConfig { Bean public RestTemplate deepSeekRestTemplate() { SimpleClientHttpRequestFactory factory new SimpleClientHttpRequestFactory(); factory.setConnectTimeout(5000); factory.setReadTimeout(60000); return new RestTemplate(factory); } }Service 层代码如下Service public class DeepSeekApiClient { private final RestTemplate restTemplate; private final DeepSeekProperties deepSeekProperties; public DeepSeekApiClient(RestTemplate restTemplate, DeepSeekProperties deepSeekProperties) { this.restTemplate restTemplate; this.deepSeekProperties deepSeekProperties; } public DeepSeekChatResponse chat(String userContent) { HttpHeaders headers new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setBearerAuth(deepSeekProperties.getApiKey()); DeepSeekChatRequest request DeepSeekChatRequest.of( deepSeekProperties.getModel(), userContent ); HttpEntityDeepSeekChatRequest entity new HttpEntity(request, headers); ResponseEntityDeepSeekChatResponse response restTemplate.exchange( deepSeekProperties.getBaseUrl() /chat/completions, HttpMethod.POST, entity, DeepSeekChatResponse.class ); if (response.getStatusCode().is2xxSuccessful()) { return response.getBody(); } throw new DeepSeekApiException(DeepSeek API 调用失败: response.getStatusCode()); } }setBearerAuth方法会自动处理Bearer前缀比自己拼字符串更保险。我见过有人写成Bearer key时不小心在Bearer后面加了两个空格结果 API 返回 401。用现成方法最稳。3.3 Controller 暴露接口跑通第一个请求Controller 层尽量保持轻薄不要在里面拼请求体只做参数接收和结果返回RestController RequestMapping(/api/ai) public class AiController { private final DeepSeekApiClient deepSeekApiClient; public AiController(DeepSeekApiClient deepSeekApiClient) { this.deepSeekApiClient deepSeekApiClient; } PostMapping(/chat) public DeepSeekChatResponse chat(RequestBody ChatRequest request) { return deepSeekApiClient.chat(request.message()); } public record ChatRequest(String message) {} }启动服务后用 Postman 或 curl 发一个 POST 请求{message: 用一句话解释什么是 RESTful API}如果一切正常返回的 JSON 会包含choices[0].message.content你从前端把这段 content 展示出来就好。到这里基本的最小闭环已经完成。4. 线上最常遇到的 5 个 400/401 错误附排查链路4.1 maximum context length is 1048576 tokens 不是让你把 max_tokens 调成 1M这个 400 错误我在群聊和社区里见过太多次热词里也明确出现了api error: 400 this models maximum context length is 1048576 tokens。howevere...。如果你的请求里包含了大量历史消息或者把max_tokens设成了一个很大的数比如直接照抄上下文窗口长度那么prompt_tokens max_tokens一旦超过模型上限就会触发这个错误。我排查过一次线上问题产品要做长文档分析代码里把用户传来的整份合同直接塞进messages又把max_tokens设置成了 4096。结果算出来总长度超过限制接口直接 400。处理方式很简单// 错误示范 new DeepSeekChatRequest( deepseek-chat, messages, 0.7, 1048576, false ); // 正确做法 new DeepSeekChatRequest( deepseek-chat, messages, 0.7, 2048, false );如果你的业务场景是长文档摘要建议先做切片。比如把超过 5000 字的内容按章节拆成多段每次只提交一段给模型最后再让模型合并结果。不要以为上下文窗口大就无脑全塞超长输入不仅慢还会因为中间内容被截断而出现摘要遗漏。4.2 api_key_required鉴权报错先查 Authorization 头另一个高频错误是{code:api_key_required,message:api key is required in authorization header}这个错误看着是没带 API Key实际原因很多。我总结了一条排查链路按顺序走基本十分钟能定位echo $DEEPSEEK_API_KEY看环境变量是否真的存在。curl 用同一个 Key 试一次确认 Key 本身有效。检查 Java 代码里HttpHeaders是否真的设置了Authorization头方法是不是setBearerAuth。确认 deployment 环境没有把配置覆盖成空字符串。留意日志里是否无意中打印了headers如果打印了也只会泄露风险不会让请求成功。从经验看大部分情况是配置中心覆盖了本地环境变量或者 CI/CD 部署时没传入这个环境变量。把错误信息当成“一定没带 key”来排查容易被带偏。4.3 model 名称与 messages 格式慢慢对文档别自己发明参数如果你用的模型名不准确或者 messages 少了必要字段也可能收到 400。我列一个对照表错误现象常见原因修正方式Model not found / invalid model写成 deepseek-v3 或 deepseek-coder使用deepseek-chat或deepseek-reasonermessages might be empty数组传了空列表至少传一条 user 消息system 角色位置不对system 放在 user 后面部分模型建议 system 在最前按 system、user、assistant 顺序排列Unsupported parameter传了 OpenAI 专有字段比如 n、logprobs先删掉再逐个打开DeepSeek 虽然兼容 OpenAI 协议但并不是所有 OpenAI 参数都支持。例如response_format在部分模型上支持有限seed也不一定每次都生效。这些参数一旦不被模型支持返回的 400 提示可能很隐晦。我建议只保留必要参数model、messages、temperature、max_tokens、stream其余的一步一步试。4.4 tool_calls 返回后必须马上补一条 tool 消息再请求如果你在做 function calling会经常遇到choices[0].message.tool_calls。很多新手的误区是拿到工具调用结果后就只回给前端不再提交给模型。但协议要求第二轮请求的messages里必须包含三样东西原始 user 消息assistant 消息其中包含tool_calls字段一条 role 为tool的新消息tool_call_id对应要执行的工具调用 IDcontent是本地执行结果所以 DTO 需要扩展不能只保留 role 和 content。示例扩展public record DeepSeekMessage( String role, String content, ListDeepSeekToolCall tool_calls, String tool_call_id ) { public DeepSeekMessage(String role, String content) { this(role, content, null, null); } } public record DeepSeekToolCall( String id, String type, DeepSeekFunction function ) {} public record DeepSeekFunction( String name, String arguments ) {}第二轮的请求消息类似ListDeepSeekMessage messages List.of( new DeepSeekMessage(user, 这周杭州天气怎么样), new DeepSeekMessage(assistant, null, List.of(toolCall), null), new DeepSeekMessage(tool, {result:明天杭州有雨最高温度 28℃}, null, call_123) );我发现很多人会漏掉“assistant 消息必须原样放回”这一点。第二次请求如果不带 assistant 的tool_calls模型会不知道这个 tool 是谁调用的上下文断裂就会报类似messages tool calls need immediate results的错。处理方式就是手写一个“循环调用”拿到tool_calls- 执行本地方法 - 追加 tool 消息 - 重新请求直到模型返回正常content为止。4.5 超时、连接池和重试策略别把 400 当 500 处理DeepSeek 这种大模型 API 有几个和普通接口不一样的地方。第一是响应时间波动大。简单问答可能 1 秒返回长文本生成可能要 50 秒。如果你用默认的 RestTemplate默认读取超时是无穷大在生产上不推荐但如果你设置成 5 秒又会频繁超时。建议长文本场景下读取超时给 60 秒以上。第二是连接池问题。SimpleClientHttpRequestFactory不维护连接池每次请求都新建 TCP 连接QPS 上来之后性能会急剧下降。建议用 Apache HttpClient 或 OkHttp 作为底层实现。举个例子Bean public RestTemplate deepSeekRestTemplate() { CloseableHttpClient httpClient HttpClients.custom() .setConnectionManager(PoolingHttpClientConnectionManagerBuilder.create() .setMaxTotal(50) .setDefaultMaxPerRoute(20) .build()) .build(); HttpComponentsClientHttpRequestFactory factory new HttpComponentsClientHttpRequestFactory(httpClient); factory.setConnectTimeout(5000); factory.setConnectionRequestTimeout(5000); factory.setReadTimeout(60000); return new RestTemplate(factory); }第三是重试策略。注意400 类错误是参数或上下文问题重试多少次都一样不应该重试超时、5xx 可以考虑重试。幂等性也要考虑同一个问题发给模型虽然结果是概率性的但从业务角度生成答案这个动作本身可重复只是会消耗 token。我的做法是超时错误最多重试 1 次而且要退避比如间隔 1 秒避免把服务打爆。5. 流式输出与性能优化WebClient SSE、虚拟线程、缓存降级5.1 什么时候必须用流式非流式调用适合“前端不着急等完整结果再一起展示”的场景。但如果你做的是对话机器人、客服助手、文档生成编辑器用户等 20 秒才看到第一句话这个体验很难接受。流式响应SSE可以做到模型每生成一小段就立即推给浏览器实现打字机效果。DeepSeek 的流式调用不复杂请求体里stream: true服务端返回text/event-stream格式每行是一个data:数据块直到最后data: [DONE]。前端用EventSource或 fetch 流式读取后端用 WebClient 的bodyToFlux(String.class)逐行解析。5.2 WebClient 解析 SSE 的代码实现先简化请求构建。因为stream是 true我会显式构造一个请求对象public FluxString streamChat(String userContent) { DeepSeekChatRequest request new DeepSeekChatRequest( deepSeekProperties.getModel(), List.of(new DeepSeekMessage(user, userContent)), 0.7, 2048, true ); return webClient.post() .uri(/chat/completions) .header(HttpHeaders.AUTHORIZATION, Bearer deepSeekProperties.getApiKey()) .contentType(MediaType.APPLICATION_JSON) .bodyValue(request) .retrieve() .bodyToFlux(String.class) .filter(line - line.startsWith(data: )) .filter(line - !line.contains([DONE])) .map(this::parseDelta); }parseDelta方法解析响应块中的增量内容private String parseDelta(String line) { String json line.substring(data: .length()); DeepSeekStreamResponse chunk objectMapper.readValue(json, DeepSeekStreamResponse.class); if (chunk.choices() null || chunk.choices().isEmpty()) { return ; } DeepSeekStreamChoice choice chunk.choices().get(0); if (choice.delta() null) { return ; } return choice.delta().content() null ? : choice.delta().content(); } record DeepSeekDelta(String content) {} record DeepSeekStreamChoice(DeepSeekDelta delta) {} record DeepSeekStreamResponse(ListDeepSeekStreamChoice choices) {}Controller 直接返回FluxServerSentEventStringSpring 会帮你包装成 SSE 格式GetMapping(value /api/ai/chat/stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxServerSentEventString stream(RequestParam String message) { return deepSeekApiClient.streamChat(message) .map(content - ServerSentEvent.builder(content).build()); }这里有一个容易被忽略的地方bodyToFlux(String.class)拿到的是一行一行的数据HTTP 自动分块后JSON 里不一定会一次性到齐。所以解析时最好判断字符串是否满足data:前缀不满足就先跳过。我曾经因为没判断前缀在超大响应把多行拼接成半个 JSON 时崩溃过。5.3 Java 21 虚拟线程阻塞调用场景下的低成本升配如果你用的还是 RestTemplate 这种阻塞式客户端在高并发下需要注意线程池占满问题。一个请求阻塞 30 秒意味着 Tomcat 的线程池里有一个线程被“挂起”如果并发 100 个这类请求再叠加其他业务接口线程池可能直接耗尽。Spring Boot 3.2 之后配合 JDK 21可以直接开启虚拟线程把每个请求处理线程从“重量级操作系统线程”变成“轻量级虚拟线程”。开启方式非常顺滑spring.threads.virtual.enabledtrue相当于给 Tomcat 的 worker 线程换了个实现。对 DeepSeek 这种 IO 密集型的阻塞调用收益非常明显。我测过一个小场景原本一个长文本生成接口占用 100 个 Tomcat 线程后会开始出现排队开启虚拟线程后同样负载下几乎没有线程池告警。有几个坑要记住虚拟线程并不是万能的CPU 密集型计算不会因为虚拟线程变快另外如果代码里用了synchronized去占一块共享资源虚拟线程阻塞在那也会被挂起本质上没有减少等待。所以启用前先确认瓶颈确实是“等待远程 IO”。5.4 缓存和降级才是线上稳定的关键模型接口的性能再好也不如不调。我见过一项数据分析需求每天要生成几百次文本内容重复率还不低。一开始是每次调用 API管理后台一刷新就发起一次请求token 消耗快接口响应又慢。后来加了缓存对同一批 key 的结果直接复用性能立刻提升一个量级。用 Spring Cache 加 Caffeine 实现就很合适Cacheable(value deepseek, key #message) public String chatWithCache(String message) { return chat(message).choices().get(0).message().content(); }配置spring: cache: type: caffeine注意多轮对话不能无脑缓存因为上下文不同同样一句“你好”在不同会话里可能期待不同回答。我的策略是只对无状态单轮请求开缓存并且缓存 key 加上模型版本和 prompt 版本避免升模型后返回旧结果。降级方面可以用 Resilience4j 给 DeepSeek 调用加熔断。当模型接口连续失败超过阈值就直接走 fallback 返回兜底文案而不是把异常抛给用户。代码逻辑类似CircuitBreaker(name deepseek, fallbackMethod chatFallback) public DeepSeekChatResponse chat(String userContent) { // 调用 DeepSeek API } public DeepSeekChatResponse chatFallback(String userContent, Exception e) { return new DeepSeekChatResponse( List.of(new DeepSeekChoice(new DeepSeekMessageResponse(assistant, AI 服务繁忙请稍后再试))), new DeepSeekUsage(0, 0, 0) ); }线上重要接口一定要有兜底因为模型服务商的稳定性再好也可能存在热点时段超时、限流。有了降级机制用户至少能看到合理的提示而不是一个刺眼的 500。5.5 给新手的接入顺序建议如果你现在正打算在 Spring Boot 项目里接 DeepSeek我的建议是先跑通非流式调用再做流式最后再考虑虚拟线程、缓存、熔断这些优化。别一上来就上 WebFlux也不要理想化地直接做 function calling。按这个顺序来每一步都能独立验证出问题也能快速定位。好记的推进顺序就是非流式跑通一个接口 - 封装异常和处理 400/401 - 换连接池和超时 - 加缓存降级 - 按需升级到流式 - 再决定要不要开虚拟线程。这样你每一步的收益都很明确而且不会出现“一次集成太多不知道哪里出错”的困境。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。