ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Spring Boot 集成 DeepSeek:从基础调用到流式与上下文管理

Spring Boot 集成 DeepSeek:从基础调用到流式与上下文管理 最近好几个开发群都在问同一个问题Spring Boot 项目里怎么把 DeepSeek 的 API 接进来。这个问题表面看很简单DeepSeek 的接口走的又是 OpenAI 兼容格式拿 HttpURLConnection 硬调也能通但真正落到工程里你会遇到模型怎么选、参数怎么配、流式怎么接、超长上下文怎么处理、接口超时与限流怎么兜底这一连串问题。这篇文章不是贴一段代码就完事而是把从零搭一个可用调用工程的全过程、踩过的坑和最终沉淀下来的方案完整拆给你看。适合准备把 DeepSeek 接入业务系统、想做 AI 功能集成或者对 Spring Boot 调用第三方 API 感兴趣的同学看完可以直接照搬而且能明白每一步为什么要这么做。1. 整条调用链路的设计为什么用 Spring Boot 来封装 DeepSeek API1.1 这组技术组合到底能解决什么问题把 DeepSeek 的接口接到业务系统需求往往不是什么“我要跑一个 AI 程序”而是更务实的场景内部工单系统要接入智能摘要客服后台要做一个问答助手运营平台要批量生成文案或者是给现有的 Java 老系统加一个“AI 能力层”。这些场景下你不可能让业务方自己去拼 HTTP 请求、管 API Key、处理重试和异常必须有一个独立的服务模块把这些琐碎事情全部封装好对外暴露一个清晰的 REST 接口让上游系统像调用普通后端服务一样调用 AI 能力。Spring Boot 在这里的位置就是“胶水层”。它的核心职责有三个一是把 DeepSeek API 的调用细节全部收敛在内部包括鉴权、超时、重试、日志二是把请求和响应映射成 Java 对象让业务代码不再跟 JSON 字符串打交道三是把 AI 能力以 REST 接口、消息队列消费者或者定时任务的形式输出方便其他系统集成。换句话说DeepSeek 是大脑Spring Boot 是连接大脑和业务系统的那套神经系统。1.2 方案选型的几个关键取舍先说为什么选 DeepSeek 而不是其他大模型。很多团队在做内部功能时根本不需要那么复杂的模型管理平台DeepSeek 的 API 价格合理兼容 OpenAI 协议Java 社区里现成的 SDK 和资料又多接入成本很低。更重要的是它的上下文窗口做得比较大对“把业务文档塞进提示词里让模型总结”这类常见需求非常友好不用总是想着做复杂的外部知识库方案。再说为什么用 Spring Boot 而不是直接用 Python 脚本。团队技术栈是 Java服务要部署在现有的 Spring Cloud 体系里要做鉴权、限流、监控要跟公司内部的统一配置中心、日志平台打通这种情况下 Python 脚本很难融入基础设施。Spring Boot 本身对 HTTP 调用、JSON 序列化、线程池、链路追踪的支持都很成熟写出来的东西好维护招人也容易。这个选择基本没什么可犹豫的。还有一个细节取舍调用 DeepSeek API 的 HTTP 客户端到底用 RestTemplate、RestClient 还是 WebClient。老项目里大量用的是 RestTemplate但它已经进入维护模式功能也偏老。Spring Boot 3.2 之后官方推荐的是 RestClientAPI 设计简洁同步调用时体验很好。WebClient 则适合流式输出和异步场景底层基于 Reactor可以优雅地消费 SSE 流。我的建议是同步调用主用 RestClient需要接流式输出时切换到 WebClient两者可以共存不用非此即彼。1.3 链路设计与目录规范整条链路的调用顺序是这样的外部请求进来先经过 Controller 层做参数校验然后交给 Service 层组装消息列表Service 层调用封装好的 DeepSeek ClientDeepSeek Client 负责拼接请求、加鉴权头、发起 HTTP 调用、处理异常和重试、解析响应最后把结果一层层返回。这个链路里最关键的是隔离DeepSeek 相关的所有细节都收在 client 层和 config 层业务 Service 只依赖一个简单的 ChatResponse 对象这样以后如果换模型服务商改动范围可以控制得很小。目录结构我一般推荐这种分层方式com.example.deepseekdemo ├── config # 配置类WebClient、RestClient、属性绑定 ├── controller # 对外 REST 接口 ├── service # 业务逻辑组装消息、调用、后处理 ├── client # DeepSeek API 封装请求、响应模型、错误处理 ├── dto # 请求/响应 DTO ├── common # 常量、异常、工具类 └── properties # 配置属性绑定类很多应届生或者刚转 Java 的同学喜欢把所有类都堆在几个包里这在小 demo 里问题不大但项目一旦开始迭代包结构混乱会让“找类”变成一件很痛苦的事。上面这套结构不是银弹但按“配置、接口、业务、客户端、模型”分层以后哪怕是后来接手的人也能一眼看出某个功能应该改哪个位置。2. 动手前的准备API Key、模型选型与参数细节2.1 API Key 的正确获取和安全存放方式调用 DeepSeek API 第一步是去开放平台创建 API Key。这一步本身没什么难度但 Key 的保管方式却是很多项目翻车的重灾区。我见过有人把 API Key 直接写在 application.yml 里提交到 Git 仓库结果在内网代码扫描时被安全团队点名也见过 Key 写在前端代码里等于把账号免费开放给全网。正确的做法是把 Key 放到环境变量或者配置中心里代码仓库里只保留占位符。比如在 application.yml 中这样写deepseek: api-key: ${DEEPSEEK_API_KEY}然后在本机启动服务时通过环境变量注入在服务器上通过公司的配置中心或密钥管理系统注入。Spring Boot 对这类占位符解析是原生支持的不需要额外写代码。另外如果公司有多个环境建议每个环境用独立的 Key这样即便测试环境的 Key 泄露了生产环境也不受影响。还有一个容易忽略的点DeepSeek 的请求头认证方式是Authorization: Bearer 你的Key注意 Bearer 后面有个空格拼错了会一直报 401。很多人排查半天发现是字符串拼接少了空格这类低级错误最好在封装 client 的时候就固化成代码逻辑避免每次调用都手拼。2.2 模型选型chat 模型、reasoner 模型与 v4 系列DeepSeek 开放平台目前可选的模型按用途分大致有两类。一类是通用对话模型日常问答、文本生成、内容总结都走它响应速度快价格便宜另一类是推理增强模型适合数学推导、逻辑分析、复杂代码生成这类需要深度思考的任务它会在回答前先内部生成一段推理过程所以响应时间更长对参数调整也更敏感。最近官方开放的 v4 系列模型把上下文窗口拉到了百万级 token像deepseek-v4-pro、deepseek-v4-flash这类模型名分别对应更强的推理能力和更快的响应速度。实际选型的时候不要无脑上最大的模型而是先想清楚场景如果是高并发的文本分类、信息抽取选响应快的模型就够了如果是要做论文阅读、复杂业务规则推断再上推理型模型。模型名配置在deepseek.model里切换成本很低建议放到配置项里而不是写死在代码中。关于 deepseek-reasoner 这类推理模型有一个经验要提醒temperature 参数建议固定在一个合理的值附近不要随便调高。因为推理模型本身已经在内部做了大量探索你再把随机性拉满反而容易得到不稳定的答案。通用对话模型则可以按场景调整 temperature创意写作调高一点数据抽取调低一点。2.3 必须搞清楚的请求参数附参数速查表请求体里的参数看起来不多但每个都影响最终效果。messages 是消息列表每一条消息有 role 和 content 两个字段role 可以是 system、user、assistant。system 消息用来设定模型的角色和行为边界比如“你是一个严谨的客服助手”user 是用户输入assistant 是历史回复。很多人在多轮对话里忘了把历史 assistant 消息传回去导致模型每次都是“失忆”状态体验很差。max_tokens 控制的是单次回复最多生成的 token 数不是上下文总长度这两个概念特别容易混淆。比如上下文窗口是 1048576 tokens但 max_tokens 设 100模型就只能回复一个很短的答案。temperature 控制随机性top_p 是核采样概率两者效果有重叠一般建议固定其中一个来调不要同时大幅度调整。下面是一份常用的参数速查表方便对照配置参数类型作用建议值modelstring选择模型按场景选 chat 或 reasonermessagesarray会话消息列表保留 system 与多轮历史temperaturenumber控制随机性0.7 通用0.2 抽取1.0 创意max_tokensinteger单次最大回复长度512~2048 常用top_pnumber核采样概率1.0 或与 temperature 二选一调streamboolean是否流式返回实时交互开 truestoparray停止标记按需设置这套参数如果只是自己调试怎么配都行但一旦做成产品功能就必须把参数配置收口到配置文件里由后端统一管控不要让上游调用方随便传。原因很简单不同业务对随机性、回复长度的要求不一样如果每个调用方都能随意传参最终模型输出质量会变得不可控出了问题也很难排查是谁改的。3. Spring Boot 实操从 Maven 工程到一次完整调用3.1 Maven 工程结构、依赖与构建配置创建一个标准的 Spring Boot 工程我习惯用 Maven 方式因为公司内部的项目基本都以 Maven 为主私服、插件、父子工程都方便。Java 版本建议 17 以上Spring Boot 版本使用 3.2 或更新版本因为 RestClient 和更完善的 WebClient 支持都需要较新的基线。先看 pom.xml 的核心依赖parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.3.4/version relativePath/ /parent properties java.version17/java.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-webflux/artifactId /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies很多新手会疑惑web 和 webflux 两个 starter 都加会不会冲突实际上 Spring Boot 项目里如果同时引入两者默认仍然是以 Spring MVC 作为 Web 层webflux 的引入主要是为了使用 WebClient 这个响应式 HTTP 客户端。这是一套非常常见的组合不会导致启动冲突放心用。Lombok 可以省掉一堆 Getter/Setter如果是团队规范禁止 Lombok也可以手动生成后面代码里涉及到的地方对应改一下就行。3.2 配置文件、HTTP 客户端选择与底层差异配置我建议拆成两部分一部分是 Spring Boot 原生配置比如服务端口、应用名另一部分是自定义的 DeepSeek 配置单独用ConfigurationProperties绑定。自定义配置放在application.yml里是下面这个样子server: port: 8080 spring: application: name: deepseek-api-demo deepseek: base-url: https://api.deepseek.com api-key: ${DEEPSEEK_API_KEY} model: deepseek-chat temperature: 0.7 max-tokens: 2048 connect-timeout: 10s read-timeout: 60s配置类可以这样写Data ConfigurationProperties(prefix deepseek) Component public class DeepSeekProperties { private String baseUrl; private String apiKey; private String model; private Double temperature; private Integer maxTokens; private Duration connectTimeout; private Duration readTimeout; }这里把超时时间单独拆出来是因为大模型接口有一个特点非流式调用时如果模型在深度思考响应可能要几十秒甚至更久默认的 HTTP 超时经常不够用。connectTimeout 和 readTimeout 要区分对待前者设置 10 秒足够了后者建议至少 60 秒如果是推理模型还要更长。超时配置如果不单独拿出来直接写死在代码里到时候线上调参数还得重新发版太折腾。HTTP 客户端的核心配置我直接用 RestClient 处理同步调用。RestClient 的 API 风格和 WebClient 很像但返回结果是同步的对大多数业务系统来说心智负担更低Configuration public class DeepSeekRestClientConfig { Bean public RestClient deepSeekRestClient(DeepSeekProperties properties) { return RestClient.builder() .baseUrl(properties.getBaseUrl()) .defaultHeader(HttpHeaders.AUTHORIZATION, Bearer properties.getApiKey()) .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .requestFactory(getRequestFactory(properties)) .build(); } private ClientHttpRequestFactory getRequestFactory(DeepSeekProperties properties) { SimpleClientHttpRequestFactory factory new SimpleClientHttpRequestFactory(); factory.setConnectTimeout(properties.getConnectTimeout()); factory.setReadTimeout(properties.getReadTimeout()); return factory; } }这么封装之后所有请求都会自动带上鉴权头和超时配置Service 层不需要关心这些细节只负责传消息进来。如果你希望同步调用和流式响应都使用同一套配置可以再额外定义一个 WebClient Bean道理是一样的只是返回类型从业务对象变成 Flux。3.3 请求体与响应体的 DTO 封装请求和响应的 DTO 是整条调用链里最容易写错的部分。DeepSeek API 的字段是 snake_case 风格比如max_tokens、finish_reason、prompt_tokens而 Java 命名习惯是驼峰如果直接拿 Map 接收再手动取值代码会非常难维护。正确做法是定义清晰的 DTO用 Jackson 的JsonProperty做字段映射。请求体 DTOData Builder NoArgsConstructor AllArgsConstructor public class ChatMessage { private String role; private String content; }Data Builder NoArgsConstructor AllArgsConstructor public class ChatRequest { private String model; private ListChatMessage messages; private Double temperature; JsonProperty(max_tokens) private Integer maxTokens; JsonProperty(stream) private Boolean stream; }响应体 DTO 稍微复杂一点主要包含 id、choices、usage 三块。choices 数组里是模型返回的内容usage 里是 token 消耗统计Data Builder NoArgsConstructor AllArgsConstructor public class ChatResponse { private String id; private String object; private Long created; private String model; private ListChoice choices; private Usage usage; Data Builder NoArgsConstructor AllArgsConstructor public static class Choice { private Integer index; private ChatMessage message; JsonProperty(finish_reason) private String finishReason; } Data Builder NoArgsConstructor AllArgsConstructor public static class Usage { JsonProperty(prompt_tokens) private Integer promptTokens; JsonProperty(completion_tokens) private Integer completionTokens; JsonProperty(total_tokens) private Integer totalTokens; } }这里有个细节值得说很多新人在做响应解析时直接把 choices 里第一个元素的 message.content 作为“答案”忽略了 choices 其实是个数组。虽然大多数情况下数组里只有一个元素但如果你搞传参、多候选之类的高级用法数组里可能会多出几项。代码里统一取第一个元素的逻辑没问题但最好加个空判断和日志避免数组为空时抛空指针还一脸懵。3.4 Service 层实现与 Controller 暴露 REST 接口Service 层负责把业务输入转换成模型需要的消息列表。一个最简洁的调用示例如下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 ChatResponse chat(ListChatMessage messages) { ChatRequest request ChatRequest.builder() .model(properties.getModel()) .messages(messages) .temperature(properties.getTemperature()) .maxTokens(properties.getMaxTokens()) .stream(false) .build(); return restClient.post() .uri(/chat/completions) .body(request) .retrieve() .body(ChatResponse.class); } }调用路径是/chat/completions这也是兼容 OpenAI 协议的标准路径。如果 DeepSeek 平台同时支持带/v1前缀的地址baseUrl 里配成不带 v1 的域名路径里补全即可原理一样。Controller 层按照 RESTful 接口规范来设计动词用 POST路径体现资源语义请求体用 DTO 接收。比如RestController RequestMapping(/api/v1/chat) public class ChatController { private final DeepSeekService deepSeekService; public ChatController(DeepSeekService deepSeekService) { this.deepSeekService deepSeekService; } PostMapping public ChatResponse chat(RequestBody ListChatMessage messages) { return deepSeekService.chat(messages); } }这样对外暴露的接口很简单调用方只需要知道“往 /api/v1/chat 发一个消息列表拿到回答”完全不需要理解 DeepSeek 的 API 细节。实际项目里我还会在 Controller 层加一层参数校验比如 messages 不能为空、content 长度不能超过限制这些校验能有效避免把脏数据传给大模型既省 token 又降低报错概率。说到接口规范路径里的v1前缀是 API 版本管理的基本做法。业务系统一旦上线很难保证接口不变化有版本号以后即便是破坏性升级老调用方也能继续用旧版本不用被迫跟着改。这个习惯建议从一开始就养成后面省很多事。4. 进阶流式输出与超长上下文处理4.1 流式输出SSE在交互场景里的价值第一次做 AI 功能的人通常会忽略流式输出等到产品经理说“回答太慢了用户一直盯着转圈”的时候才意识到问题。深层原因是人的耐心阈值太低超过两三秒没有反馈就会焦虑而大模型非流式调用动辄十几秒这个体验在面向用户的场景里完全不可接受。流式输出的本质是让模型一边生成一边把内容推给客户端典型实现是 SSEServer-Sent Events。它和 WebSocket 不同SSE 是单向的服务器主动往客户端推数据恰好匹配大模型“生成一段推一段”的场景而且基于普通 HTTP 协议不需要额外维护连接状态Spring Boot 对它有原生支持实现成本很低。流式响应还有一个隐藏的好处用户可以更早地看到输出内容哪怕结果还没完整生成也能先判断方向对不对如果不对可以提前终止省的继续烧 token。这个体验价值和成本价值都值得你为它单独写一套接口。4.2 Spring Boot 里接入 SSE 流式响应的两种写法第一种写法是在 Service 层用 WebClient 消费 DeepSeek 的流式响应把返回的每条数据直接透传给前端。核心是把请求里的stream字段设成 true然后用bodyToFlux(String.class)接收 SSE 字节流public FluxString chatStream(ListChatMessage messages) { ChatRequest request ChatRequest.builder() .model(properties.getModel()) .messages(messages) .temperature(properties.getTemperature()) .maxTokens(properties.getMaxTokens()) .stream(true) .build(); return webClient.post() .uri(/chat/completions) .bodyValue(request) .accept(MediaType.TEXT_EVENT_STREAM) .retrieve() .bodyToFlux(String.class) .map(this::parseContentFromSse); }这里要注意DeepSeek 返回的 SSE 数据并不是每一行都是干净的 JSON中间有大量data: {...}这样的前缀以及最后可能有一条data: [DONE]表示结束。所以parseContentFromSse里需要做两件事过滤掉非 data 开头的行把data:前缀去掉以后再解析 JSON 取choices[0].delta.content。流式响应的结构和非流式不一样内容字段在 delta 里不在 message 里这个和很多人的第一直觉不同。Controller 层的写法是设置produces MediaType.TEXT_EVENT_STREAM_VALUE让 Spring 以 SSE 的方式把 Flux 输出给前端PostMapping(value /stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxString chatStream(RequestBody ListChatMessage messages) { return deepSeekService.chatStream(messages); }第二种写法是使用 SseEmitter适合在不引入 WebClient 和响应式编程的老项目里做流式输出。SseEmitter 的思路是Controller 先返回一个 SseEmitter 对象给前端此时 HTTP 连接保持打开然后你在自己的业务线程里往 emitter 发送数据最后调用 complete 结束。这种方式更贴近传统 Servlet 编程习惯代码直观但并发控制、线程管理都要自己留意不如 WebClient 方案简洁。前端如果用的是 fetch可以直接解析text/event-stream的响应体如果用的是 EventSource API要注意 EventSource 只支持 GET 请求而大模型对话一般需要传大量消息体所以实践中更多还是用 fetch 加 POST 方式手动解析 SSE 流这个细节在前后端联调时经常成为沟通成本点。4.3 1048576 tokens 上下文限制与消息窗口管理新手在调 DeepSeek API 时最常撞见的一个报错大概长这样400 Bad Request this models maximum context length is 1048576 tokens. However, your messages resulted in 1048600 tokens. Please reduce the length of the messages.第一次看到这个报错的人通常会以为是模型出了问题其实不是。这个错误的意思是你把太多历史消息一股脑塞进了请求里导致整个上下文超出了模型窗口。只要上下文窗口是 1048576 tokens换算成中文大概是几十万字到一百多万字普通对话根本碰不到上限会触发这个错误多半是你在循环里不断拼接历史消息或者把整本业务文档直接丢进了提示词。要解决这个问题核心思路是“消息窗口管理”也就是在调用 API 之前对 messages 列表做裁剪保留 system 指令和最近几轮对话丢掉中间的冗长内容。我总是用一句话给团队讲这个逻辑和 AI 聊天和和人聊天是一样的你不能把十年前的对话全背一遍再问今天的问题得做取舍。实现上可以先做一个简单的 token 估算工具。中文字符一个大约占 1 到 2 个 token英文字符大约 4 个字符占 1 个 token工程上可以用字符数除以 2作为估算值虽然不精准但够用来做提前拦截public static int estimateTokens(String text) { if (text null || text.isEmpty()) { return 0; } return (int) Math.ceil(text.length() / 2.0); }然后写一个裁剪方法从消息列表末尾往前叠保留最近的对话直到接近阈值。为什么要从后往前因为离当前问题越近的内容对回答影响越大system 指令则单独保留并放在最前面public ListChatMessage trimMessages(ListChatMessage messages, int maxContextTokens) { ListChatMessage result new ArrayList(); int totalTokens 0; for (int i messages.size() - 1; i 0; i--) { ChatMessage msg messages.get(i); int tokens estimateTokens(msg.getContent()); if (totalTokens tokens maxContextTokens) { break; } result.add(msg); totalTokens tokens; } Collections.reverse(result); return result; }调用前先执行一次裁剪如果消息总 token 数超过了阈值就把超出的部分截掉宁可丢上下文也不要让请求直接 400。这里有一个取舍直接截断可能让模型丢失关键信息所以生产环境里更推荐的做法是配合向量检索来压缩上下文只把和当前问题最相关的片段传给模型。但对于大多数中小业务先做好滑动窗口已经能解决 90% 的问题。另外如果业务确实需要超长文本处理还可以考虑分段总结再拼接摘要的方式先在前面加一轮“请总结上一段内容”拿到摘要后取代原文再喂给最终对话。这个思路实现起来不难但效果比单纯截断好得多。5. 高频错误排查与工程化避坑5.1 认证与令牌类错误调用 DeepSeek API 最常见的认证报错是 401 Unauthorized返回信息一般写着 invalid api key。看到这个别急着怀疑平台先按顺序排查检查环境变量是否真的注入了检查 Key 有没有复制完整尾部很容易多一个空格或换行检查请求头 Authorization 是不是标准的Bearer xxx格式。我遇到过好几次Docker 部署时环境变量没传进去代码里读到的 Key 是 null拼出来的 header 就成了Bearer null平台自然直接拒绝。另一种认证类错误是 403 Forbidden这种通常不是 Key 本身的问题而是账号权限、套餐额度或者 IP 白名单限制。如果你是在公司网络环境下调用先确认平台是否需要配置出口 IP如果是在多个环境共用同一个 Key先确认这个 Key 有没有被平台的风控策略锁掉。这类错误在本地调得好好的、一上测试环境就挂多半就是网络或白名单问题。5.2 参数与上下文超限错误400 错误里除了前面说的上下文超限还有一类是参数校验不通过比如 model 名字写错、messages 里缺少 content、temperature 传了超出范围的值。DeepSeek 支持的模型名是固定的像 deepseek-chat、deepseek-reasoner 以及 v4 系列写成deepseek-V4或者多打一个空格都会直接报错。这种问题排查最快的办法是把发给平台的完整请求体打印出来和官方文档对照一眼就能看出来。上下文超限的 400 错误处理方案在前面已经说了核心是消息窗口管理。这里再补充一个实操技巧把每次请求的usage.total_tokens记录下来按用户维度做统计。这样你能知道每个用户平均消耗多少 token如果某个用户频繁触达窗口上限说明你的裁剪策略阈值没设对或者对话轮数太深需要提示用户开新会话。5.3 网络超时、限流与重试策略调用第三方 API 永远要把对方当成“随时可能出故障”的系统来设计。DeepSeek 服务稳定归稳定但高并发时段也可能出现 429请求过多或者 502/503网关异常。这类错误不是你的代码 bug但如果你不做任何处理故障就会直接暴露给业务方。推荐的兜底策略是重试加指数退避第一次失败后等 500 毫秒再试第二次等 1 秒第三次等 2 秒同时加一个最大重试次数超过就放弃并返回友好错误。Spring Retry 可以很优雅地实现这个逻辑Service public class DeepSeekService { Retryable( retryFor {WebClientResponseException.class}, noRetryFor {IllegalArgumentException.class}, backoff Backoff(delay 500, multiplier 2.0, maxDelay 10000), maxAttempts 3 ) public ChatResponse chat(ListChatMessage messages) { // 调用逻辑 } }注意不是所有异常都值得重试。400 这种参数错误重试一万次还是 400还浪费 token429 和 5xx 才值得重试。所以在设计重试规则时要把“确定性的参数错误”排除在重试范围之外这个noRetryFor配置就是干这个的。另外重试时要防止同一个请求被同时执行多次可以加一层简单的分布式锁或者让调用方传入请求幂等号不过大多数内部系统没这个必要知道有这层考虑就行。5.4 附带提醒Actuator 端点的暴露要收敛很多 Spring Boot 项目都会引入 Actuator 做健康检查和监控但默认配置会把所有端点暴露出来包括/actuator/env、/actuator/heapdump这样的敏感端点。如果你把服务直接暴露到内网或公网又没有做认证别人可以通过这些端点看到环境变量、配置信息甚至导出堆内存进行分析这比 API Key 硬编码进代码还要危险。生产环境里至少要做到两点一是只暴露必要的端点比如 health、info、metrics、prometheus二是给 Actuator 端点加认证或者放到独立的管理端口。配置文件里可以这样收敛management: endpoints: web: exposure: include: health,info,metrics,prometheus endpoint: health: show-details: never如果接了 Prometheus 监控再用 Micrometer 统计大模型调用的延迟、错误率、token 消耗量就能在监控面板上直观看到服务状况。这个组合在热词里经常出现实际用起来也确实香把 DeepSeek 调用延迟和成功率做成指标一旦接口变慢或者被限流告警能第一时间触发不用等用户来反馈。5.5 错误速查表把开发调试过程中高频遇到的情况整理成一个表遇到报错直接对号入座错误现象可能原因处理方式401 UnauthorizedAPI Key 为空、复制不全、Bearer 拼接错误检查环境变量、请求头格式403 Forbidden账号额度不足、IP 白名单检查套餐和出口 IP400 model 不存在模型名拼写错误对照官方文档确认模型名400 context length 超限消息太长超窗口裁剪历史消息做窗口管理400 参数校验失败temperature、max_tokens 超出范围打印请求体逐字段排查429 Too Many Requests触发限流指数退避重试必要时提额度502/503/504服务端异常或超时重试、降级、告警这个表是我在实际调试里总结出来的覆盖面不一定全但把常见的坑都列进来了。建议你接手一个 Spring Boot 调 DeepSeek 的项目时先把这个表贴到团队文档里能省掉很多重复排查的沟通成本。我自己在实际项目里把 DeepSeek 接入 Spring Boot 以后最大的感受是真正的复杂度根本不在“调一次 API”本身而在于工程化细节。API Key 的安全存放、超时时长的设置、流式响应的对接、上下文窗口的管理、重试降级的策略每一个环节都值得单独打磨。最后再分享一个小技巧开发阶段可以在配置里加一个开关让 DeepSeek 请求和响应的完整日志打到日志文件里定位问题的时候非常管用但上线前务必把日志级别调回来否则你的日志系统会被 token 内容撑爆还会有敏感信息泄露风险。调试日志是一把双刃剑用好了事半功倍用不好就是把隐私往外送。
返回列表