
如果你所在的后端团队今年开始讨论“给系统接一个大模型”Spring AI 加 DeepSeek 这套组合大概率会出现在备选清单的前排。先说清楚这篇不是给任何模型厂商做推广纯粹是从 Java 工程化角度聊聊怎么把 DeepSeek 的聊天能力接进 Spring 生态并且接得稳、接得省心。我做过的几个内部系统之前接大模型都是自己用 RestTemplate 写调用刚开始挺爽后来随着需求从“能聊”变成“要流式、要多轮、要工具调用”那套手写代码越来越难维护。换到 Spring AI 之后最大的感受是它把模型接入这件事从“拼 HTTP 接口”变成了“声明一个组件”门槛一下子降下来了。这篇文章就把我整合 DeepSeek 的全过程拆开讲包括依赖怎么选、配置怎么写、代码怎么组织、参数怎么调以及那些文档里不会明说的坑。1. 项目概览Spring AI 与 DeepSeek 能碰撞出什么1.1 为什么选 Spring AI 而不是自己封装 HTTP 调用很多团队一开始都会纠结一个问题DeepSeek 的 API 本身是 OpenAI 兼容格式我用 HttpClient 直接调不就行了为什么要多引入一套 Spring AI单看“发一次请求、拿一次回复”这个最小场景手写确实够了。但你把需求列表拉长看流式返回要解析 SSE 分帧多轮对话要维护 Message 序列工具调用要做函数注册和结果回填不同模型供应商的接口差异要抽象……这些如果全自己写工作量不是线性增长是几何级数。Spring AI 的价值在于它把“对接大模型”这件事做成了类似 JDBC 的适配层。你面向的是 ChatClient 这种统一门面底层无论是 DeepSeek、还是其他兼容 OpenAI 协议的模型代码主体都不需要改。这跟你用 JDBC 写 SQL、底层换数据库连接池是一个道理——业务代码不绑定具体厂商。另外Spring AI 是 Spring 官方生态的一部分自动配置、依赖注入、配置项绑定这些机制都跟 Spring Boot 天然融合。你不需要自己造一套配置加载逻辑也不需要担心 Bean 生命周期管理。对于已经在用 Spring Boot 的团队引入成本非常低。1.2 DeepSeek 在聊天模型里的定位从纯技术接入角度看DeepSeek 最友好的地方在于它的 API 协议是 OpenAI 兼容的。这意味着 Spring AI 不需要为它单独写一套接入实现直接复用 OpenAI 模块把 Base URL 指过去、模型名换成 DeepSeek 的模型标识就行。DeepSeek 提供两个主要模型标识一个是 deepseek-chat面向通用对话场景日常问答、内容生成、代码辅助都能胜任另一个是 deepseek-reasoner偏推理型适合数学、逻辑、复杂分析这类需要深度思考的任务。我这边的项目主要用 deepseek-chatreasoner 偶尔用在需要高准确率的子任务上。这个“OpenAI 兼容 模型标识切换”的设计是整个项目最关键的一个决策点。它省去了一个专门的 starter也让后续横向切换模型供应商变得非常轻量。如果你接的是其他兼容 OpenAI 协议的服务思路一模一样只是改几个配置字符串的事。1.3 这套方案能解决的实际场景我实际落地过的场景包括内部知识库问答助手、工单自动分类与摘要、代码仓库的提交信息规范化、以及对外的智能客服后端服务。这些场景有一个共同特征都需要跟现有 Spring Boot 业务系统深度集成比如查订单状态、读用户信息、写数据库记录。Spring AI 在这类场景里的优势是它的 Function Calling 机制。模型在对话过程中如果需要实时数据可以通过注册好的函数回调拿到业务系统的结果再组织成自然语言回复。这个能力让“聊天机器人”变成“能办事的助手”而不只是一个会喷段子的聊天窗口。至于适合谁来参考我觉得如果你是做后端开发的对 Spring Boot 已经熟悉想快速给系统接入 LLM 能力这篇文章的路径可以直接复制。如果你刚接触 Spring AI也能通过后面的配置和代码一步步跟下来。2. 环境准备与依赖搭建2.1 版本选型JDK、Spring Boot 与 Spring AI版本选型是第一步也是最容易被忽视的一步。Spring AI 的版本迭代节奏很快API 在 0.8.x 和 1.0.x 之间有过一些调整自动配置类、ChatClient 的包名和构建方式都有差异。我推荐直接使用 1.0.x 的稳定版本配合 Spring Boot 3.2 及以上JDK 用 17 或 21。为什么强调 JDK 17因为 Spring Boot 3.x 系列基线就是 JDK 17你如果还在 JDK 8 上跑要么先升级基础环境要么就只能用旧版的 Spring Boot 2.7 加上早期的 Spring AI 0.8.x但那套组合的自动配置能力和后续扩展性都差一些。长期维护的角度我建议新项目直接上 JDK 17 Spring Boot 3.2 Spring AI 1.0.x。这里多说一句不要在版本上追求“最新”。Spring AI 的 SNAPSHOT 版本和 M 系列预览版虽然能体验新特性但生产环境遇到诡异问题的时候你很难判断是配置错了还是框架本身的 Bug。锁定一个稳定版本读对应版本的官方文档能省掉大量排查时间。2.2 最小依赖配置用 Maven 的话需要引入 Spring AI 的 BOM 和 OpenAI 模块的 starter。之所以引入 OpenAI 模块而不是某个 DeepSeek 专属模块就是前面说的DeepSeek 走的是 OpenAI 兼容协议复用 OpenAI 适配器即可。dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version1.0.0/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai-spring-boot-starter/artifactId /dependency /dependencies如果你的项目还用到 WebFlux需要另加 spring-boot-starter-webflux因为流式返回那块要用到 Flux。如果只是普通 MVC 项目那 spring-boot-starter-web 就够了流式响应用 SSE 的响应体也能处理。2.3 基础配置 application.yml 与密钥管理依赖引入之后最核心的配置就是把 Spring AI 的 OpenAI 适配器指向 DeepSeek 的地址。base-url 填 DeepSeek 官方提供的 API 地址api-key 填你在控制台申请到的密钥model 填 deepseek-chat。spring: ai: openai: base-url: https://api.deepseek.com/v1 api-key: ${DEEPSEEK_API_KEY} chat: options: model: deepseek-chat temperature: 0.7 max-tokens: 2048这里有一个原则性的经验API Key 绝对不要硬编码在配置文件里也不要提交到 Git 仓库。我见过不止一个团队因为图省事把 key 写在 application.yml 里结果仓库权限稍微没控住key 就泄露了账单哗哗地涨。正确做法是用环境变量或者配置中心来托管本地开发环境放 .env 文件线上走配置中心或 K8s Secret。base-url 这里有个细节DeepSeek 的接口地址既可以填不带 /v1 的根地址也可以填带 /v1 的完整地址两者都能通。Spring AI 的 OpenAI 模块内部有默认的路径拼接逻辑我实际使用下来填完整地址更稳省得路径拼接出幺蛾子。2.4 写一个最简单的连通性验证配置写完之后我习惯先做一步“最小连通性验证”而不是直接写业务代码。随便起一个 Spring Boot 应用在启动类或者测试类里注入 ChatClient调一次最简单的问答确认整条链路是通的。验证通过的标准有三个请求没有抛鉴权异常、返回内容非空、响应时间在合理范围内。只要这三个都满足就说明 base-url、api-key、模型名这三个最关键的配置项都没问题。后续再往业务侧扩展遇到问题可以更聚焦地排查。3. 核心编码从零配置一个可对话的聊天模型3.1 ChatClient 注入与第一个 Hello 请求Spring AI 1.0.x 里的核心对象是 ChatClient它是所有对话操作的统一入口。你不需要手动 new直接注入 ChatClient.Builder 然后构建即可。Service public class ChatAssistantService { private final ChatClient chatClient; public ChatAssistantService(ChatClient.Builder builder) { this.chatClient builder .defaultSystem(你是一个乐于助人的中文助手回答简洁准确。) .build(); } public String ask(String question) { return chatClient.prompt() .user(question) .call() .content(); } }这里 ChatClient.Builder 会从 Spring 容器里自动读取前面在 application.yml 中配置的 base-url、api-key、model 等属性构建出一个可用的客户端。defaultSystem 方法可以设置系统级提示词相当于给模型定了一个统一的人设和回答风格所有对话默认都会带上这段系统消息。call().content() 是同步阻塞调用适合接口内部需要拿到完整结果再返回的场景。如果你在 Controller 里直接暴露这个方法注意同步调用会占用 Tomcat 工作线程并发高的时候要评估线程池容量。3.2 用 Prompt 的 Options 在代码里动态调整参数全局配置里的 temperature、max-tokens 是默认值但实际业务中不可能所有场景都吃同一套参数。比如做知识库问答希望回答尽量稳定temperature 调低一些做文案头脑风暴希望输出有发散性temperature 调高一些。Spring AI 允许在每次构建 Prompt 时动态覆盖这些参数优先级高于全局配置。public String askWithOptions(String question, double temperature, int maxTokens) { return chatClient.prompt() .user(question) .options(OpenAiChatOptions.builder() .withTemperature(temperature) .withMaxTokens(maxTokens) .build()) .call() .content(); }这里要注意的是 options 的类型是跟具体模型供应商相关的。因为底层接的是 DeepSeek 的 OpenAI 兼容接口所以用 OpenAiChatOptions 来构造。如果哪天换了别的供应商可能需要换成对应的 Options 实现类。这也是 Spring AI 抽象层的一个小小代价统一门面之下供应商特有参数还是各写各的。3.3 流式输出给接口加分的关键一步同步调用在模型生成完整个回复后才返回用户要等好几秒。为了体验绝大多数对话类接口都应该做成流式输出让文字逐个蹦出来。Spring AI 的 stream 方法返回的是 Flux天然适配响应式编程。public FluxString askStream(String question) { return chatClient.prompt() .user(question) .stream() .content(); }如果你的服务是 WebFlux直接在 Controller 里返回 Flux 就能自动以 Server-Sent Events 的方式推送。如果是传统 MVC也可以把 Flux 转换成 SseEmitter或者在前端用 EventSource 去接。实测下来切到流式之后用户体感上的“首字延迟”大幅缩短满意度提升非常明显。但流式也有代价因为它是一段一段地返回在网关层、代理层容易被缓冲。后面我会单独讲这个坑这里先记住一个原则——流式接口的响应头必须设置 Cache-Control: no-cacheContent-Type 要是 text/event-stream否则中间层可能给你攒到一起再返回流式效果直接消失。3.4 系统提示词与角色化助手聊到系统提示词很多人以为就是给模型写一句“你是助手”就完事了其实它对回复质量的影响远超想象。我做过一个对比同一道问题不带系统提示词和带一段精心设计的角色约束回复的格式规范性和专业度差别非常大。在 Spring AI 里系统消息和用户消息是分开的都可以显式构造public String askWithRole(String question, String systemPrompt) { return chatClient.prompt() .messages( new SystemMessage(systemPrompt), new UserMessage(question) ) .call() .content(); }如果你没有一个动态系统提示词的需求直接用 3.1 里的 defaultSystem 就行。如果有比如不同角色进入工作台看到不同人设的助手那就在每次请求时动态传入系统消息。我给一个实用的提示词结构参考角色定位、任务目标、约束条件、输出格式。四段写清楚模型的回复基本就能稳定在你想要的范围里。比如“你是一名资深的 Java 代码评审专家请指出以下代码的安全隐患和性能问题按严重程度从高到低输出”这种明确指令比“请帮我看看代码”效果好得多。4. 进阶功能参数调优与多轮对话管理4.1 关键生成参数逐个拆解很多人配置模型参数都是凭感觉temperature 随手填个 0.7 就完事。这里我把几个关键参数讲透你调起来就有方向感了。temperature 控制的是随机性取值范围一般是 0 到 1 或 0 到 2不同服务商区间不一样。它好比给骰子灌铅温度越低骰子越沉每次输出越倾向于选最高概率的路径温度越高输出越发散各种意想不到的词语都可能蹦出来。知识库问答、信息抽取这类任务我建议调到 0.1 到 0.3文案生成、创意写作可以放到 0.7 到 0.9。max-tokens 控制生成的最大长度。这里有个容易踩的误区max-tokens 只是截断上限不是模型一定回复这么长。但如果你设得太短比如只有 200长一点的回答就会被硬生生切断看起来像是模型“话没说完”。top-p 是核采样参数控制候选词的累积概率范围。OpenAI 兼容协议里官方一般建议不要同时大改 temperature 和 top-p因为两者本质都在调节输出的确定性同时调容易互相干扰。我自己的习惯是优先调 temperaturetop-p 保持默认或锁定在 0.9 左右。下面是我常用的参数速查参数作用推荐场景我常用的值temperature输出随机性客服/知识问答/抽取0.1 - 0.3temperature输出随机性文案/创意生成0.7 - 0.9max-tokens最大生成长度按业务回复长度512 - 2048top-p候选词累积概率与temperature配合默认或0.9presence_penalty话题重复惩罚内容多样性要求高0.5左右4.2 多轮会话的上下文管理聊到多轮对话很多初期的实现方案是前端把整个聊天记录都传回来后端一股脑拼进 Prompt。这个方案在小场景下能用但有两个问题一是越聊越长Token 成本直线上升二是上下文太长会超出模型窗口限制直接报错或者把早期的内容“挤”出注意力范围。Spring AI 提供了 ChatMemory 这类的会话记忆组件但真正用起来还是要自己做取舍。我的做法是维护一个最近 N 轮的滑动窗口把超期的消息丢弃只保留最近最有价值的上下文。N 的取值要看业务普通客服场景 10 轮左右绰绰有余复杂分析场景可以放宽到 20 轮。Token 预算是必须要算的一笔账。DeepSeek 的上下文窗口有限系统提示词加上最近几轮历史消息再加上当前用户消息总 Token 数建议控制在窗口的 70 以下。多退少补留出生成回复的空间。如果单条消息特别长还可以对历史消息做截断比如只保留每条消息的前 200 个 Token。4.3 Function Calling让模型真正“动手”如果说 ChatClient 是聊天的基础那 Function Calling 就是让聊天变成“办事”的关键。原理其实不复杂模型本身不执行任何代码但在它可以读取的上下文中声明“你有一个工具可以用来查询订单状态”当对话内容需要这个能力时模型会输出一个结构化的调用意图Spring AI 拦截到之后替它调用注册好的函数再把结果作为上下文喂给模型。注册函数的方式比较干净先定义标准的 Java Function然后注册成 BeanBean public FunctionOrderQueryRequest, OrderQueryResponse queryOrderStatus() { return request - orderService.queryStatus(request.orderId); } public record OrderQueryRequest(String orderId) {} public record OrderQueryResponse(String orderId, String status) {}然后构建 ChatClient 的时候把函数挂上去this.chatClient builder .defaultFunctions(queryOrderStatus) .build();这样用户在对话里说“帮我查一下订单 A10086 的状态”模型就会触发 queryOrderStatus 函数拿到订单状态之后组织成自然语言回复。做 Function Calling 最大的坑是函数的入参定义一定要清晰描述要详细。模型靠函数名和参数描述来理解什么时候该调用、参数怎么填写得含糊就容易出现该调不调、不该调乱调的情况。4.4 给模型调用加一层兜底与降级外部大模型接口和内部 RPC 不一样它的可用性、延迟都不由你控制。所以接入生产环境之前必须想清楚模型超时了怎么办模型返回的内容不符合预期怎么办模型服务方限流了怎么办我通常会做三层兜底第一层是超时控制连接超时设 5 秒读取超时设 60 秒避免一个慢请求把线程池拖垮第二层是异常兜底捕获 OpenAI 相关的异常返回一条友好的降级文案给前端比如“服务暂时繁忙请稍后重试”第三层是熔断限流用 Resilience4j 给模型调用加一个熔断器连续失败率达到阈值就直接短路不再把流量打给模型服务。Spring AI 的自动配置本身支持通过 spring.ai.openai.http 相关配置调整 HTTP 客户端的超时和连接池这块我放到踩坑部分一起说。5. 踩坑实录常见问题与排查思路5.1 鉴权失败401/403 先别怀疑代码我敢说一半以上的接入问题出在 API Key 上。Spring AI 的自动配置看起来一切正常一调接口就报 401很多人就开始怀疑是不是依赖版本不对。实际上最常见的原因是环境变量没有生效或者配置文件里的 key 带了空格。排查思路就三步先确认环境变量真的传到应用里了再用 curl 直接打一次接口验证 key 有效性最后检查配置前缀是否正确。curl 验证是最直接的curl --location https://api.deepseek.com/v1/chat/completions \ --header Content-Type: application/json \ --header Authorization: Bearer sk-你的key \ --data {model: deepseek-chat, messages: [{role:user, content:ping}]}如果 curl 通了但 Spring AI 不通那问题就在配置加载层。检查 application.yml 里 spring.ai.openai.api-key 有没有被环境变量正确覆盖不要在 yml 里写死空字符串占位符Spring 的占位符解析有时候会静默失败。5.2 流式输出被网关缓冲“吞掉”流式接口接好之后本地测试一切正常一上测试环境就变成“等好久然后一次性返回全部内容”。这个问题十有八九是网关或反向代理做了响应缓冲。解决方案是在网关层关掉缓冲。以最常见的 Nginx 为例需要设置 proxy_buffering off同时保证响应头里有 Cache-Control: no-cache。响应类型要设置成 text/event-stream不然一些代理组件不认识这个 Content-Type会按普通文本处理导致缓冲行为不可控。如果你的服务前面还有一层自研网关或者API网关组件也要检查它是否支持流式透传。有些网关为了统一日志会把整个响应体读入内存再转发这种设计对 SSE 流式极不友好可能需要放行白名单。5.3 上下文过长导致超时或截断多轮对话越聊越长突然某次请求报超时或者回复内容明显变差先看是不是 Token 数爆了。Spring AI 本身不会主动帮你算历史消息的 Token 数超了窗口它会直接报错。我的建议是在进入模型调用之前加一道 Token 估算逻辑。不需要很精确按 1 个汉字约等于 1 到 1.5 个 Token 估算即可超预算就把最老的 UserMessage 和 AssistantMessage 摘掉。这个步骤做在前置拦截器或者 AOP 切面里不用侵入每个业务方法。还有一种情况是 max-tokens 设置得太小导致模型生成到一半被截断返回的 content 不完整。复现方法很简单把 max-tokens 调大再看。如果恢复正常说明就是长度设少了。5.4 连接池耗尽与重试风暴没有显式配置连接池的时候很多人以为“反正是 HTTP 调用没什么好配的”。但高并发下请求的一发不可收拾会导致连接池排队进而引发超时重试重试又进一步压垮连接池形成雪崩。Spring AI 底层走的是 RestClient 或 WebClient连接池参数可以通过配置进行调整。一个比较保守的起步配置spring: ai: openai: http: connect-timeout: 5s read-timeout: 60s如果你用的是 WebClient 那套还可以额外配置 max-in-memory-size防止极端情况下响应体过大把内存打爆。连接池的调优核心思路是不要让一个下游服务的抖动拖垮整个应用。熔断、限流、重试必须分开控制不要一遇到超时就盲目重试尤其是对生成式大模型一个请求的耗时本就长无脑重试的代价很大。5.5 空回复但账单没少扣的诡异现象有一种情况排查起来非常隐蔽接口正常返回模型也没报错但 content 是 null或者只返回了几个字而账单里 Token 消耗却不少。我遇到过几次要么是 max-tokens 设太小导致输出被截断成空要么是模型的 reasoning 内容被计入了 Token 但没进 content。DeepSeek 的 reasoner 模型会把思考过程单独放在 reasoning_content 字段里如果你只是取 content就会看到空回复但 Token 已经被扣掉了。解决方案是取消息内容时多判断几个字段把 reasoning_content 也读出来或者统一改用 deepseek-chat 模型避免这类问题。这个坑在日志里尤其隐蔽因为你可能只打了 content完全不知道底层发生了什么。6. 上线前的性能与成本优化建议6.1 响应性能优化流式优先与缓存策略性能优化第一条仍然是流式。同步等待完整回复的用户体验太差而且占用的线程资源也更久。走流式之后Tomcat 工作线程可以尽早释放吞吐量能明显改善。第二条是缓存。很多场景下用户的提问高度重复比如内部知识库里“休假政策是什么”这种问题。直接在模型调用前面加一层缓存命中就走缓存不要重复去烧 Token。缓存键的设计我用的是“语义指纹”而不是原始字符串因为同义问法的表达差异很大。简单做法是把用户问题先让模型做成向量再检索重一点的方案是直接接向量数据库。6.2 Token 成本控制从提示词到历史消息全面收紧Token 成本是大模型应用上线后最大的运营成本项而且这东西不像服务器资源看不见摸不着每个月账单出来才肉疼。控制成本从三处下手。第一处是系统提示词能精简就精简。我见过一个团队的系统提示词写了一千多字每次请求都带日请求量一大这个固定成本非常可观。第二处是历史消息滑动窗口能留 5 轮就别留 20 轮代价只是回复连贯性稍微下降。对于多数场景这个性价比很高。第三处是模型选择。轻量任务用 deepseek-chat复杂推理任务才切 deepseek-reasoner。reasoner 的 Token 消耗明显更高不要全局一把梭。6.3 可观测性模型调用也要有日志与告警模型调用这种外部依赖光有业务日志是不够的。我把每一次模型调用的关键信息都记为一条结构化日志至少包含用户ID或业务标识、模型名、输入 Token 数、输出 Token 数、耗时、是否命中缓存、是否走降级路径。有了这些数据才能对成本和性能做量化分析。告警规则我建议设两条一条是模型调用错误率超过阈值另一条是平均响应耗时超过阈值。这两条能在模型服务侧出问题的时候第一时间报警而不是等用户来投诉才发现系统已经在降级边缘。最后分享一个我在多个项目里反复验证过的体会Spring AI 整合 DeepSeek真正困难的部分不在“能不能通”而在“通得稳不稳”。连通性验证半小时就能做完但参数调优、上下文窗口管理、函数调用设计、熔断降级、成本控制每一项都需要结合自己的业务场景反复打磨。先小流量跑起来把同步接口换成流式把死板的问答升级成能调函数的助手一步步演进会比憋一个大而全的方案更靠谱。希望这篇实战记录能帮你少走几个我走过的弯路。