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

Spring Boot 接入 DeepSeek:从 HTTP 调用到生产级流式与上下文管理

发布时间:2026/9/26 8:52:16

资讯中心
01
ARTICLE

Spring Boot 接入 DeepSeek:从 HTTP 调用到生产级流式与上下文管理

Spring Boot 接入 DeepSeek:从 HTTP 调用到生产级流式与上下文管理
1. 我为什么要在 Spring Boot 项目里接 DeepSeek 而不是直接用网页版大概一个月前我在内部管理系统里接到一个很现实的需求业务人员每天要翻好几份制度文档、查几十条历史工单才能回答客户那些重复度极高的问题。团队的预期是界面里直接有一个“智能助手”入口员工提问系统回答答案最好还能带上业务口径。技术栈没有悬念Spring Boot 3.2 Java 17已有的用户体系、权限模型、工单数据全在里面。所以问题就变成了怎么把大模型能力接进一个正经的 Java 后端项目。可能有人会问直接让员工用 DeepSeek 网页版不就行了吗在很多场景下确实行但一旦涉及内部数据、权限控制、回答口径网页版就完全不够。比如我希望助手只能回答公司知识库范围的问题不希望它拿到某个能访问全部数据的通用入口我希望提问的人是谁、能看到哪些业务信息由我们自己的后端来控我也希望问题的答案能沉淀到工单记录里而不是留在浏览器外面。这些需求决定了必须在 Spring Boot 里写代码而不是引导用户去开另一个页面。这篇文章适合两类人。一类是刚接触大模型 API 的 Java 后端工程师想用最少代码把 DeepSeek 接入 Spring Boot先跑通再谈优化另一类是已经接过 OpenAI 兼容接口但想确认流式输出、上下文记忆、限流重试这些细节在 Spring Boot 里到底怎么落地的人。我会按我从零到上线的实际路径来写包括中间踩过的坑而不是只给一个看起来无懈可击的“完美代码”。先说结论Spring Boot 接入 DeepSeek本质上就是发起一次 HTTP 请求把用户问题拼成 messages 数组发给官方接口再把返回内容解析成业务对象。真正的复杂度来自三块网络请求怎么设计、上下文怎么保存、生产环境怎么扛住异常。下面逐一展开。2. 接入前的技术选型协议、客户端、密钥这三个坑先讲清楚2.1 先搞懂 DeepSeek API 的调用形态别被“SDK”绕进去DeepSeek 官方接口走的是 Chat Completions 模式和 OpenAI 的接口风格兼容。也就是说你不需要引入什么特殊依赖就用普通的 HTTP 客户端就能调通。很多教程喜欢让你引入大而全的 SDK但我个人建议第一版先不要这么做。SDK 虽然省事但会带来一层抽象出了问题你还要再翻一层文档手写一个轻量 HTTP 客户端反而能让你把所有请求参数看得明明白白。一个最基本请求的 JSON 长这样{ model: deepseek-chat, messages: [ { role: system, content: 你是一个严谨的企业内部知识库助手 }, { role: user, content: 报销单最长可以追溯多久之前的 } ], temperature: 0.5, stream: false }请求地址是https://api.deepseek.com/chat/completions鉴权方式在请求头里带Authorization: Bearer 你的API Key。注意官方文档提到v1这个路径前缀只是约定跟模型版本没关系如果不想写/v1直接用根路径也能通。我自己习惯把 base-url 配置成https://api.deepseek.com然后具体接口路径写在常量里方便后续切换环境。响应结构里最有用的是choices[0].message.content它就是模型生成的正文。如果你开了流式响应会变成一行一行的 SSE 事件这个我们放到第五章专门说。2.2 RestTemplate、WebClient、OkHttp到底怎么选这是接入前绕不过去的选择。我做了个对比方便你按自己的场景来挑客户端适用场景优点需要注意的点RestTemplate同步、一次性返回Spring Boot 自带配置简单代码量最少本质是阻塞 IO长请求会占线程WebClient流式响应、少量异步非阻塞支持 SSE 和响应式背压多一个依赖概念略多OkHttp同时需要同步/流式拦截器强大超时控制细需要自己解析 SSE 格式我的实际选择是普通问答用 RestTemplate流式输出用 WebClient。理由很简单RestTemplate 在 Spring Boot 里开箱即用我不用为一次对话专门引入一套响应式体系而流式场景里WebClient 的bodyToFlux能直接按行读取 SSE 数据省掉很多手写解析逻辑。至于 OkHttp除非你已经比较熟悉否则没必要在 Spring Boot 里再维护另一套 HTTP 工具。2.3 密钥管理不要在配置文件里写死 Key接入大模型最容易被忽略的是 API Key 安全。我见过有人在application.yml里直接写api-key: sk-xxxx还提交到了 Git 仓库。API Key 一旦泄露会被盗刷额度所以这一关在代码阶段就要养成习惯。最简单的做法是配置里写占位符值从环境变量读取deepseek: api-key: ${DEEPSEEK_API_KEY} base-url: ${DEEPSEEK_BASE_URL:https://api.deepseek.com} model: ${DEEPSEEK_MODEL:deepseek-chat}再进一步生产环境可以用配置中心或密钥管理服务统一管理开发环境可以用.env文件。另外一个容易被忽略的点是日志脱敏后面自定义拦截器时我会把请求头里的Authorization从日志里过滤掉这条经验虽然基础但真的救过我一次。3. 最小可运行版本一个 Service 加一个 RestTemplate 其实就够3.1 配置类把超时和认证放在一起管第一版我不想写太复杂的封装但有两个细节必须一开始就处理好超时时间和认证请求头。DeepSeek 生成一个回答通常要 520 秒如果没有设置合理的 read timeout默认情况下请求很容易在中间断开。我这里把连接超时设为 3 秒读取超时设为 60 秒。Configuration public class DeepSeekClientConfig { Bean public RestTemplate deepSeekRestTemplate(DeepSeekProperties properties) { SimpleClientHttpRequestFactory factory new SimpleClientHttpRequestFactory(); factory.setConnectTimeout(3000); factory.setReadTimeout(properties.getReadTimeoutMs()); RestTemplate restTemplate new RestTemplate(factory); restTemplate.getInterceptors().add((request, body, execution) - { request.getHeaders().setBearerAuth(properties.getApiKey()); return execution.execute(request, body); }); return restTemplate; } }这里把认证逻辑放在拦截器里而不是每次调用的时候手动传 Key好处是后续新增任何请求方法都不用重复写认证代码。DeepSeekProperties是一个简单的ConfigurationProperties绑定上面deepseek.*配置项即可。3.2 DTO请求体和响应体别用 Map 硬怼刚开始我图快直接用MapString, Object拼 JSON结果响应解析时到处靠类型强转非常痛苦。如果你打算长期维护这个模块建议从第一天就定义 record。public record ChatMessage(String role, String content) { } public record ChatRequest( String model, ListChatMessage messages, Boolean stream, Integer maxTokens, Double temperature ) { } public record ChatResponse( String id, String object, Long created, String model, ListChoice choices ) { public record Choice(Integer index, ChatMessage message, String finishReason) { } }record 是 Java 16 之后非常合适写 DTO 的方式配合 Jackson 反序列化基本不用额外配置。注意 DeepSeek 返回的message.role通常是assistantmessage.content才是正文。3.3 封装 DeepSeekClient一个对外方法就够用有了配置和 DTO客户端就非常薄了Service public class DeepSeekClient { private static final String CHAT_COMPLETIONS_PATH /chat/completions; private final RestTemplate restTemplate; private final DeepSeekProperties properties; public DeepSeekClient(RestTemplate restTemplate, DeepSeekProperties properties) { this.restTemplate restTemplate; this.properties properties; } public ChatResponse chat(ChatRequest request) { String url properties.getBaseUrl() CHAT_COMPLETIONS_PATH; return restTemplate.postForObject(url, request, ChatResponse.class); } }postForObject会把ChatRequest自动序列化成 JSON再把响应反序列化成ChatResponse。这里有个 Java 新手容易踩的坑RestTemplate默认的 Jackson 转换器只认识JsonProperty或字段名。如果你把 record 字段命名为maxTokens而 JSON 里是max_tokens需要再配一个 Jackson 的命名策略。Bean public ObjectMapper objectMapper() { return JsonMapper.builder() .propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE) .build(); }或者在 DTO 字段上加JsonProperty(max_tokens)。我推荐前者因为 DeepSeek 和 OpenAI 这类接口都统一用 snake_case全局配一次比每个字段都注解干净得多。3.4 真正的业务调用把用户输入转成业务问题到这一步接一个/chat接口就很简单了Service public class ChatService { private final DeepSeekClient deepSeekClient; Value(${deepseek.model:deepseek-chat}) private String model; public String ask(String userText) { ChatRequest request new ChatRequest( model, List.of( new ChatMessage(system, 你是企业内部知识库助手请用简洁的中文回答。), new ChatMessage(user, userText) ), false, 1024, 0.5 ); ChatResponse response deepSeekClient.chat(request); return response.choices().get(0).message().content(); } }Controller 就是一个标准 POST 接口。第一次跑通的时候看到返回值打印在测试用例里其实感触挺深一个大模型接入代码量也就这么多。真正麻烦的是接下来要让对话“记得住上下文”并让响应“流着吐出来”。4. 多轮对话与上下文记忆从“一问一答”升级成真正助理4.1 messages 数组就是全部上下文很多第一次接入的人容易误解以为 DeepSeek 服务端会自动记住你的历史提问。其实它是无状态的每次请求传多少 messages模型就基于多少上下文回答。所以实现记忆的策略核心就是把历史消息塞回 messages。拿业务场景举例用户问“报销单最长追溯多久”你回答了。她接着又问“那发票抬头错了还能改吗”新问题没有上下文的话模型根本不知道“那”字指的是报销单。正确做法是ListChatMessage messages List.of( new ChatMessage(system, systemPrompt), new ChatMessage(user, 报销单最长追溯多久), new ChatMessage(assistant, 按公司规定当年发生的费用应在次年一季度前完成报销。), new ChatMessage(user, 那发票抬头错了还能改吗) );注意 role 的顺序必须是严格交替的不能连续两个 user。这也意味着当你保存历史时要连用户问题带助手回答一起存否则组装出来的 messages 会不符合接口规范。4.2 用 Redis 存会话历史比你想的简单我建议把每个会话的 messages 直接存成 Redis 中的 List以chat:session:{sessionId}为 key。核心伪代码如下String key chat:session: sessionId; ListOperationsString, String ops redisTemplate.opsForList(); // 取出之前所有消息 ListString historyJson ops.range(key, 0, -1); ListChatMessage history new ArrayList(); for (String json : historyJson) { history.add(objectMapper.readValue(json, ChatMessage.class)); } // 拼上当前用户问题 ChatMessage userMsg new ChatMessage(user, userText); history.add(userMsg); // 调 DeepSeek 拿到回答 ChatResponse response deepSeekClient.chat(new ChatRequest( model, history, false, 2048, 0.6 )); // 把用户问题和助手回答都写回 Redis ChatMessage assistantMsg new ChatMessage(assistant, response.choices().get(0).message().content()); ops.rightPush(key, objectMapper.writeValueAsString(userMsg)); ops.rightPush(key, objectMapper.writeValueAsString(assistantMsg)); ops.expire(key, Duration.ofHours(2));为什么要设过期时间因为业务系统的会话不可能永远占着内存两小时不交互基本可以视为新会话。存 JSON 而不是像user:xxx|assistant:yyy这种分隔符格式是为了后续如果需要按字段扩展比如存finishReason、timestamp不用改存储结构也能兼容。4.3 长对话必须做裁剪不然 Token 费用很快失控messages 越长Token 消耗越大。官方模型上下文窗口是有限的而业务场景里用户可能聊几十轮。我常用的策略有三层从轻到重保留最近 N 轮消息比如 20 轮再之前的直接丢弃。把更早的消息浓缩成一段摘要作为 system 提示词附在最前面。对超长单条消息做截断。第一层最容易实现代码里只需要在组装请求前判断 history 的大小第二层效果更好但需要额外调用一次模型去总结历史成本和延迟都会增加。初期建议先做第一层等数据量大了再考虑摘要。Token 估算也不用太精确中文大概 1 个 token 1 个字多一点英文大概 4 个字符 1 个 token。如果你要自己统计可以用content.length()粗略除 2。真正要记得是20 轮对话大概几千字对应几千 token按现在 API 的价格不算贵但如果不裁剪聊一天就能把额度吃出明显开销。4.4 系统提示词的业务价值不可忽视多轮对话之后模型容易忘记自己的角色定位。所以我选择把 system 提示词每次请求都带上并且放在整个 messages 的第一位。同时把公司核心口径写进去比如“回答报销问题时必须优先引用制度编号”。这个做法不只是在规范回答风格也能显著减少用户问偏时模型跑题的概率。5. 线上踩坑记录超时、限流、SSE 长连接和各种错误码5.1 RestTemplate 默认超时是个大坑如果不设置 read timeoutSpring Boot 里的RestTemplate默认行为其实比较宽松长连接很容易在无响应时一直占着 Tomcat 线程。我们线上第一次出现假死就是用户问了一个需要模型思考很久的问题HTTP 请求迟迟不返回线程池被打满。后来我把读取超时设成 60 秒表面上只是加了一行配置实际上救回了整个服务可用性。注意60 秒不是越长越好。如果模型长时间没返回说明服务端可能已经过载提前失败让用户重试体验比卡死要好。5.2 流式输出Spring MVC 用 SseEmitter 就能做如果要让回答像 ChatGPT 那样一个字一个字蹦出来就必须用 SSE 流式模式。DeepSeek 请求体里设置stream: true后响应不再是完整 JSON而是一行一行的data: {choices:[{delta:{content:你}}]} data: {choices:[{delta:{content:好}}]} data: [DONE]在 Spring Boot 里我建议用 WebClient 发起流式请求然后通过 SseEmitter 把数据推给前端。这里给一个最小实现思路PostMapping(/chat/stream) public SseEmitter streamChat(RequestBody StreamChatRequest request) { SseEmitter emitter new SseEmitter(120_000L); webClient.post() .uri(baseUrl /chat/completions) .bodyValue(new ChatRequest( model, buildMessages(request), true, null )) .retrieve() .bodyToFlux(String.class) .map(this::parseSseContent) .doOnComplete(emitter::complete) .doOnError(emitter::completeWithError) .subscribe(content - { try { emitter.send(content); } catch (Exception e) { throw new RuntimeException(e); } }); return emitter; }坑来了SseEmitter 默认超时很短如果不调用emitter.complete()连接会一直挂着。前端那边记得收到[DONE]后就关闭后端这边要在doOnComplete里正常完成。还有就是如果中间断网WebClient 会抛异常Redis 里可能还留着未完成的半个回答需要靠幂等或重试机制兜底。5.3 HTTP 错误码你最好一张表背下来我整理了一份线上真实遇到的错误码放到这里方便排查状态码含义处理建议401API Key 无效检查密钥和鉴权头402账户余额不足先充值再重试但不要无限重试422请求参数不合法回读 messages看是否连续 user 或字段拼错429触发限流指数退避重试或降低频率500服务端内部错误标记为临时故障可重试一次503服务过载熔断降级避免继续打请求我最开始 429 和 503 都当成业务异常直接抛给前端导致用户看到一堆“系统错误”。后来改成针对不同码做不同策略429 就加延迟重试503 就返回“当前服务繁忙请稍后再试”体验好了很多。5.4 连接被截断和脏数据ResponseErrorHandler 里做文章Spring Boot 里RestTemplate在遇到非 2xx 状态码时默认会抛HttpClientErrorException。问题在于DeepSeek 返回的错误响应本身可能包含一些供排查用的信息你如果不解析就不知道该去查哪个方向。所以我习惯自定义一个ResponseErrorHandler把响应体能读出来记录到日志里再转成业务异常。另外一个脏数据问题是流式响应可能被网络层拆包或者粘包。你bodyToFlux(String.class)拿到的可能不是完整的一行 SSE 数据。这时候不要自己写无脑 split最好按\n\n分组再做逐行过滤确保每行以data:开头才处理。6. 生产环境我还会额外做的限流、熔断、审计与合规兜底6.1 给用户和集群都加限流不只是 API 调用接入 DeepSeek 只是第一步生产环境真正考验人的是容量控制。一个用户反复点击“发送”按钮可能几秒内就把团队的每日配额打爆。所以我在 Service 层加了两层限流业务层每个用户每分钟最多调用 N 次用 Redis 做计数器超过就返回“提问太频繁”。学科层整个应用对 DeepSeek API 每秒请求不超过某个阈值用简单的令牌桶实现。令牌桶不用引特别重的框架Guava 的RateLimiter就够用但要注意它不是分布式的多实例部署还得用 Redis 做分布式限流。如果团队不大、实例不多可以先单机限流再加集群上限性价比最高。6.2 熔断服务端过载时别把自己拖死当 DeepSeek 返回大量 503说明服务端可能正在过载此时继续重试只会雪上加霜。我会用 Resilience4j 在客户端做熔断连续失败超过阈值就进入半开状态只放少量请求试探恢复后再继续全量调用。这个设计的核心价值是保护我们自己的服务。毕竟大模型是外部依赖某一个下游不稳定不能让整个 Spring Boot 应用跟着雪崩。关键都是 Engineering Tradeoff接受一小部分请求在降级时得不到“智能回答”但换回整个业务系统可用。6.3 审计日志和内容合规上线前一定要过一遍业务系统接入大模型后用户输入的内容会发给第三方服务这就涉及数据边界问题。建议至少做三件事记录每次调用的用户、会话、问题摘要、返回状态但不记录完整 Prompt能避免敏感信息落入日志。在入口判断用户输入明显涉及攻击性、广告类的内容直接拦截不传给模型。在 system 提示词里明确告诉模型“只回答与业务相关的问题拒绝无关内容”这能显著降低“模型答非所问”的合规风险。我特别强调这一点是因为大模型接口本身是能力工具它不会自动理解公司内部的数据合规边界。规则只能由接入方来定义别指望模型自觉。6.4 后续扩展从单点接入到平台化能力当第一版接入跑通后你大概率会发现其他团队也想用。这时候单写一个 Client 类就不够了可以把 DeepSeek 调用封装成一个独立的 Maven 模块或者做成一个基础的LLMService接口后面如果还要接别的模型只换实现不换业务代码。我还会把配置中心加上模型名称的动态切换比如把deepseek-reasoner这种思考模型用在复杂问题分析上把deepseek-chat用在普通问答上切换只改配置不动代码。这类需求早点设计后面能少拆很多代码。接入 DeepSeek 这件事技术难度真的不大但它把你的业务系统从“规则问答”提升到了“理解问答”的高度过程中暴露出来的工程问题——超时、熔断、上下文、合规——每一样都值得认真对待。至少从我的经验来看把这些细节打磨完再回看最初那个“一行 HTTP 调用”的雏形你会觉得真正的交付量藏在这些看不见的工程决策里。
02
RELATED NEWS

相关资讯

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

03
WHY YAOTU

想打造同款高转化官网?

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

◈

场景化定制

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

◐

营销型架构

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

▲

全周期服务

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

免费获取你的建站方案

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