ARTICLE DETAIL

资讯详情

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

Spring AI整合DeepSeek实战:从对话到生产级应用

Spring AI整合DeepSeek实战:从对话到生产级应用 上个月给团队做企业知识库问答后端是标准 Spring Boot 技术栈当时正在评估 Spring AI 这套框架。最开始我们用 Python 脚本直接调 DeepSeek HTTP 接口原型跑得飞快但一进联调就乱套了多轮上下文要靠自己拼消息数组流式输出要手写 SSE每个用户会话还要手动隔离代码越写越像打满补丁的旧毛毯。我最后的决定是把调用层整体迁到 Spring AI 上。这篇实战指南就是这次迁移的完整复盘主题很明确如何用 Spring AI 整合 DeepSeek 聊天模型把对话能力从“能调通”推进到“能上线”。内容覆盖依赖配置、核心 API、多轮记忆、流式输出与函数调用还有大量实测踩坑适合所有用 Java 技术栈接入 DeepSeek 的团队参考。1. 为什么用 Spring AI“收编”DeepSeek而不是继续手写 RestTemplate1.1 直接调 HTTP 接口的痛点在哪个环节DeepSeek 提供的 API 本质上是 OpenAI 兼容的 HTTP 服务。没接触过的人会觉得很简单一行 curl 就能发起对话curl 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: 你好}] }确实这行命令能立刻看到回复。但一旦接入真实业务系统问题会接二连三地冒出来messages 数组的历史维护完全靠手工每轮对话都要把前 N 轮消息拼进请求重复的 JSON 拼接代码散落在业务层里。流式输出需要自己解析 SSE 数据格式从 event stream 里逐段提取 content还得处理中断、重连。不同用户之间要按照 session 隔离对话历史等于从零写一套内存缓存。错误处理、重试策略、超时控制全部需要自己设计且每个项目重复实现一遍。这些工作单独看都不难但凑在一起就是一笔不小的隐性成本。而且一旦决定换模型厂商比如从 DeepSeek 切到别的 OpenAI 兼容服务又要重写一层适配。这才是最让人头疼的地方。1.2 Spring AI 在 LLM 调用这一层做了哪些抽象Spring AI 本质上是给 Java 生态做了一套大模型客户端抽象。它把调用哪个模型、发什么消息、拿什么结果规范成几个核心接口ChatModel最底层的模型调用入口负责把 Prompt 发给模型并拿到 ChatResponse。ChatClient更上层的流式构建器支持 system、user、advisors、functions 的链式装配平时开发最常用的是它。Advisor在请求前后插入横切逻辑比如上下文记忆、日志、审核、限流等。ChatMemory统一管理多轮聊天的历史消息解决AI 失忆问题。这套抽象最大的价值在于解耦。业务代码不再和具体模型供应商绑定你可以在不改变调用方式的情况下从 DeepSeek 切到 OpenAI或者切到本地部署的推理服务。举个实际例子用 ChatClient 写完的对话服务后续如果要接其他 OpenAI 兼容网关只需要改 base-url 和 api-key 这一层配置方法体内的调用逻辑基本不动。对一个要长期维护的项目来说这个价值远比省几步代码重要。1.3 为什么不推荐绕开框架自己封装DeepSeek 官方在 Python 生态里有 SDK但 Java 生态其实并没有官方维护的 Java SDK。市面上常见做法无非三种直接用 OpenAI 的 Java SDK 改 base-url能用但依赖库和 Spring 容器是两套体系需要自己管理对象生命周期。自己封装 OkHttp 或 RestTemplate能跑但工程化能力全部要自己造轮子。用 Spring AI 官方抽象模型客户端由 Spring 容器管理自动配置、可观测、可替换都给你准备好了。我个人的建议是如果你的团队已经有 Spring Boot 项目Spring AI 是工程化代价最小的路线。后续要加日志、限流、记忆、工具调用都只是挂一个 Advisor的事而不是继续往业务代码里堆补丁。2. 环境准备与依赖配置版本选对后面少踩一半坑2.1 JDK、Spring Boot 与 Starter 的选择Spring AI 依赖 Spring Boot 3.x我项目里实际用的版本组合是 JDK 17 Spring Boot 3.3.x Spring AI 1.0.0。为什么强调 JDK 17因为 Spring Boot 3.x 本身要求 JDK 17 起步Spring AI 沿用了这一要求。如果你的项目还停留在 JDK 8就得先解决基础环境升级的问题这不算 Spring AI 特有的门槛但确实要提前评估。Maven 依赖我建议直接引入 Spring AI 的 OpenAI Starter因为 DeepSeek 提供的是 OpenAI 兼容接口不需要专门找DeepSeek 专用 starterdependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId version1.0.0/version /dependency这里有个特别容易忽略的点Spring AI 的官方模块很多有 spring-ai-starter-model-openai、spring-ai-starter-model-ollama 等。接 DeepSeek 时选 OpenAI starter 就够了别看到DeepSeek字样就满世界找专用包。另外阿里也维护了一个 Spring AI Alibaba 分支更偏向阿里云百炼那一套如果你不是主力用百炼对 DeepSeek 接入来说官方核心 starter 已经完全够用。我后来也注意到 Spring AI 的版本迭代非常快社区里已经能看到 2.0.x 的讨论。核心 API 整体延续但部分配置项前缀和包名可能有变化。如果你用的是 2.0.1 这类新版本最稳的做法是直接看官方迁移说明不要照抄老博客里的配置。2.2 application.yml 里的关键配置与每个字段的作用在 application.yml 里这样配置spring: ai: openai: api-key: ${DEEPSEEK_API_KEY} base-url: https://api.deepseek.com chat: options: model: deepseek-chat temperature: 0.7 max-tokens: 2048主要配置项的作用如下配置项作用注意事项base-url目标 API 地址默认是 OpenAI 地址不改成 DeepSeek 直接 401/404api-key身份凭证放到环境变量别写死在 yml 里model模型名称deepseek-chat 是通用对话deepseek-reasoner 是推理模型temperature随机性控制越低越稳定reasoner 模型建议保持默认不要乱调max-tokens单次回答的最大 token 数设太高会增加成本普通问答 1024 就够这里要特别提醒一点base-url 不要随手加/v1因为 Spring AI 的 OpenAI 兼容自动配置会自己拼接路径。如果你配了 https://api.deepseek.com/v1它后面可能再拼一个 /v1/chat/completions最终变成 /v1/v1/chat/completions直接报 404。这个坑我实测踩过排查了半天才发现是路径重复。DeepSeek 现在的 API 也支持 https://api.deepseek.com/v1 作为 base-url但为了配合框架的自动拼接我建议严格按照官方对 Spring AI 对接的说明来写不要想当然。以我用的 1.0.0 版本为例配 https://api.deepseek.com 最省心。3. 跑通第一个对话从 ChatModel 到 ChatClient 的核心 API3.1 最简调用注入 ChatClientSpring AI 的自动配置会帮你把 ChatClient 建好直接注入就能用Service public class ChatService { private final ChatClient chatClient; public ChatService(ChatClient.Builder builder) { this.chatClient builder.build(); } public String chat(String userMessage) { return chatClient.prompt() .user(userMessage) .call() .content(); } }这段代码已经是一次可用的 DeepSeek 对话。看着简单但内部实际包含了请求构建、模型调用、响应解析、异常映射等完整链路。这个简单正是抽象的意义。如果不想用 ChatClient直接用 ChatModel 也可以private final ChatModel chatModel; ChatResponse response chatModel.call(new Prompt(你好)); String content response.getResult().getOutput().getText();两种方式我都用过感受是ChatClient 更适合组合业务场景因为 system、user、advisors、functions 可以一路链式装配下去ChatModel 则留给需要精细控制的底层场景。我的习惯是对外服务统一走 ChatClient。3.2 给对话加上 system prompt聊天机器人几乎都要设定 persona通过 ChatClient 可以很自然地组合String reply chatClient.prompt() .system(你是企业知识库助手回答问题时只能引用知识库内容避免编造。) .user(userMessage) .call() .content();这里 system 消息会成为 DeepSeek 会话里的 system role。很多初级项目习惯把 system prompt 拼在 user 消息里效果会打折扣。因为模型对 system 和 user 两个角色的处理权重不同保持角色分离能减少用户输入对系统设定的污染。换句话说用户越界改 prompt 的成本会高很多。3.3 参数层面的细节temperature、topP、maxTokens在 prompt 上可以覆盖全局默认参数chatClient.prompt() .system(systemPrompt) .user(userMessage) .options(ChatOptions.builder() .temperature(0.3) .topP(0.9) .maxTokens(1024) .build()) .call() .content();温度越低回答越稳定适合知识问答温度高则更有创造性适合文案生成。DeepSeek 的 top_p 和 OpenAI 含义一致表示从概率累加达到该阈值的 token 里做采样。注意在 Spring AI 1.0.0 里是 maxTokens新版可能叫 max-tokens以你用的版本为准。还要注意 maxTokens 不要设置超过模型上限否则部分模型会直接报参数错误。我实际经验是知识问答场景 1024 完全够用把它们设成 4096 并不会提升质量只会让模型更话痨成本却成倍增加。4. DeepSeek 不是普通 OpenAI 克隆两个模型的正确分工4.1 deepseek-chat 与 deepseek-reasoner 的差异DeepSeek 在 API 层面对外主要提供两个模型deepseek-chat通用对话模型响应快、成本低适合大多数聊天、问答、客服、内容生成场景。deepseek-reasoner强化推理模型会先产生一段思路再回答适合数学、代码、复杂逻辑推理类问题但响应更慢、成本更高。我画过一张简单的选型表给团队参考维度deepseek-chatdeepseek-reasoner响应速度快明显更慢成本低更高适用场景日常对话、知识问答数学、代码、复杂推理是否建议调 temperature可调建议保持默认函数调用兼容性稳定触发不稳定不建议业务依赖选型上我的建议很直接默认用 deepseek-chat只有遇到需要深度推理的任务再切 reasoner。无脑上 reasoner 会让用户体验和账单都很难看。4.2 调用 reasoner 时的 reasoning_content 处理reasoner 的响应里除了正常的 content还会带一段 reasoning_content也就是思维链。问题在于 Spring AI 的 OpenAI 解析模块主要按 OpenAI 格式解析 content对额外字段要么直接忽略要么在转换时报错。我在实测里遇到的情况是直接配 reasoner 并用默认 ChatClient 拿结果有时能拿到最终回答但思考过程丢失某些版本里还会因为不可识别字段导致序列化异常。稳妥的做法两种如果你使用的版本支持自定义响应解析把 reasoning_content 也提取出来记入日志或按需返回给前端。如果不想折腾就把 reasoner 用于后台推理任务只保存最终 content思维链仅用于排查问题。我在知识库场景里采用的是方案二因为用户只关心最终答案思维链属于内部信息没必要全部暴露出去。4.3 别拿 chat 模型去冒充推理模型有些同学为了省成本给 deepseek-chat 也加上请一步一步思考这类提示。这种提示在简单题目上有点作用但不会让 chat 模型具备 reasoner 那样的深度推理能力反而会额外消耗输出 token、拖慢响应。要推理就用 reasoner要日常对话就用 chat两者的分工本来就是设计好的。我见过不少花式调 prompt 试图压榨模型的做法收益率真的不高不如老老实实按模型定位来。5. 多轮对话与上下文记忆不要让 AI 每次都失忆5.1 最朴素的做法手动维护 messages 数组直接调 HTTP 接口时多轮对话就是把历史 messages 一直传下去[ {role: system, content: 你是企业知识库助手。}, {role: user, content: 你好}, {role: assistant, content: 你好有什么可以帮你}, {role: user, content: 我们公司的请假流程是什么} ]问题大家都懂历史越长token 越大。如果无限制拼接迟早撞上上下文窗口然后报错或者费用飙升。所以需要滑动窗口策略比如只保留最近 10 条消息。这个策略看起来简单但放在业务代码里要做的事情真不少每个 session 一个列表、并发安全、过期清理、窗口裁剪全都得自己写。5.2 用 ChatMemory 统一管理历史Spring AI 把这套逻辑抽象成了 ChatMemory 接口并提供 InMemoryChatMemory 这种开箱即用的实现Configuration public class ChatConfig { Bean public ChatMemory chatMemory() { return new InMemoryChatMemory(); } }在调用时结合 MessageChatMemoryAdvisorpublic String chatWithMemory(String sessionId, String userMessage) { MessageChatMemoryAdvisor advisor new MessageChatMemoryAdvisor(chatMemory, sessionId, 10); return chatClient.prompt() .advisors(advisor) .user(userMessage) .call() .content(); }这样做的收益很直观会话历史不用你在业务代码里拼内存里的消息列表由 advisor 自动管理并按你设置的历史窗口裁剪。对于单机应用、中小规模并发这个方案完全够用。要上分布式可以把 ChatMemory 换成 Redis 实现扩展点非常清晰这比自己写一个 List 塞在 Controller 里要可靠得多。5.3 会话隔离本身就是需求上面代码里的 sessionId 非常关键。每个用户传自己的 sessionIdChatMemory 内部会按这个维度隔离历史记录。实际项目中我建议直接用用户 ID 或登录态生成 sessionId而不是前端传什么就信什么否则用户 A 修改参数就可能读到用户 B 的对话历史这是隐私事故。我把这条写进了团队的代码规范里。5.4 上下文太长时的取舍即使有滑动窗口仍然会出现长文本场景。两个优化方向只保留最近几轮同时精简 system prompt把不必要的大段背景说明挪到检索阶段。对超出窗口的历史做摘要把前面较长的对话用模型生成一段摘要作为 system 的一部分继续参与后续对话。摘要方案会增加一次模型调用但效果明显适合需要长期记忆的知识库场景。我做过一个客户服务机器人用户会在同一个会话里问很多轮摘要方案能把历史压到很小的体积同时保留核心信息体验比滚动丢弃好很多。不要一次性把所有历史都塞进去那只是把问题从对话崩溃转移到账单爆炸。6. 流式输出、结构化返回与函数调用进入生产环境的三个门槛6.1 流式输出的实现与连接管理聊天产品没有打字机效果体验会很生硬。Spring AI 对流式支持得比较完整FluxString stream chatClient.prompt() .system(systemPrompt) .user(userMessage) .stream() .content();返回的是一个 Flux 在 WebFlux 接口里可以直接返回给前端走 SSE 协议逐步推到页面。如果你用的是 Spring MVC也可以把 Flux 里的内容逐条写入 response效果一样。流式最大的坑不在写法而在连接管理。DeepSeek 的流式请求在 HTTP 层走 SSE网络超时、连接复用、客户端提前断开都会影响体验。我建议超时时间调到 60 秒以上不要用默认几秒的连接超时否则长回答会突然断。前端如果断开服务端要能感知并中断流避免后台继续消耗 token。监控每秒 token 速度便于评估体验和成本异常时能快速定位。我在一次压测里就吃过亏大量并发流式请求直接把默认连接池打满表现是前端忽快忽慢后来调大了连接池并加了应用层限流才稳定下来。6.2 结构化输出让模型按你的对象格式返回聊天模型默认返回自由文本但很多业务需要直接拿到 JSON 对象。Spring AI 里可以用 BeanOutputConverterBeanOutputConverterCargoInfo converter new BeanOutputConverter(CargoInfo.class); CargoInfo info chatClient.prompt() .system(把用户内容解析为结构化数据只输出 JSON。) .user(userMessage) .options(ChatOptions.builder().model(deepseek-chat).build()) .call() .entity(converter);这里要特别提示DeepSeek 的 JSON 输出能力与 OpenAI 的 response_format 并不完全一致。Spring AI 的 BeanOutputConverter 会偏向要求模型输出严格的 JSON并附带 schema 提示。实测里简单对象结构问题不大一旦 schema 复杂DeepSeek 可能不严格遵守。解决思路是把输出结构控制得尽量简单并在 system prompt 里反复强调只输出 JSON不要解释。如果仍然不稳定就退一步先接收 String再手动用 Jackson 解析必要时给模型一个示例 JSON。生产环境里稳定比写法优雅更重要。6.3 函数调用让 DeepSeek 能动手而不是只动嘴函数调用是接入真实业务的核心能力。Spring AI 里通过 Description 注解把 Java 方法注册给模型Bean Description(查询指定城市的当前天气) public FunctionWeatherRequest, WeatherResponse currentWeather() { return req - weatherClient.apply(req.city()); }在 prompt 里启用这个函数String result chatClient.prompt() .system(当用户询问天气时使用 currentWeather 函数获取实时数据。) .user(北京今天天气如何) .functions(currentWeather) .call() .content();原理很简单模型会先决定该调用 currentWeatherSpring AI 拿到函数名后执行 Java 方法再把结果回传给模型生成最终回答。整个链路对业务代码透明这是我很喜欢的一点。实测提醒三条DeepSeek 的 Function Calling 在 deepseek-chat 上表现稳定reasoner 上不一定按预期触发业务里尽量用 chat。函数描述一定要写清楚模型判断是否调用函数主要看 Description写得太模糊会被忽略。不要让函数体阻塞太久模型在等待函数结果期间是有超时的尤其在流式场景里更明显。7. 实测踩坑清单与性能调优建议7.1 常见报错与根因现象原因处理方式401 Unauthorizedapi-key 配错或没配检查环境变量去开放平台重新生成404 / Invalid URLbase-url 配置有误或路径重复改回 https://api.deepseek.com400 model 相关错误模型名写错确认 deepseek-chat / deepseek-reasoner429 Too Many Requests触发限流退避重试应用层收敛并发超时连接或读取超时过短调大 connect/read 超时输出被截断max_tokens 不够或上下文过长提高 max_tokens或裁剪上下文这些坑里404 和超时是我见过最多的两类。404 不一定是路径写错更常见的是 base-url 多带了一层 /v1导致 Spring AI 自动拼接变成 /v1/v1。排查时先看请求日志里的完整 URL一眼就能确认是不是这个原因。安全提醒一条不要把 api-key 写在 application.yml 里提交到代码仓库尤其是公开仓库。密钥泄露的后果是账单失控。用环境变量或配置中心管理团队里做好 key 的轮换机制。7.2 超时、重试与并发控制Spring AI 底层基于 RestClient 或 WebClient超时可以在自定义 Client 时设置。我的建议值connectTimeout5 秒readTimeout60 秒流式场景更高或者禁用读超时重试策略对 429 和 5xx 做指数退避重试对 4xx 不重试并发控制上DeepSeek 接口有配额限制。如果团队在压测或做活动建议在应用层加轻量限流比如 Semaphore 控制最大并发请求数避免集体 429。也可以放在网关层处理总之要在上游先挡一道。比如我项目里控制单实例最多同时 20 个推理请求超出直接返回排队提示效果很稳定。7.3 性能与成本优化的组合拳设置合理的 max_tokens普通问答 1024 足够避免模型输出冗长废话。精简 system prompt把固定知识背景放到检索阶段而不是全部堆给模型。对重复性请求做结果缓存知识库问答里很多问题会反复出现按问题 hash 缓存能省下不少 token。日志里记录每次请求的 prompt_tokens 和 completion_tokens月底对账和优化靠数据说话。我在项目里接了一个简单的 token 统计模块把每次调用的 token 数据落到库里几天下来就能看出哪些场景在浪费钱。没有数据支撑的优化都是盲调。8. 扩展方向Agent、工作流与管理界面聊到最后ChatClient 只是第一步。Spring AI 这套抽象的价值在于它不会停在一问一答。我看到的几个常见扩展方向把函数调用组合成 Agent让模型在循环里反复调用函数完成复杂任务Spring AI 的 Agent 生态和 Advisor 机制都在快速成长团队可以直接站在框架的肩膀上探索。工作流编排社区里已经有把 Dify 之类的工作流定义转成 Spring AI Java 代码的开源项目如果你更习惯可视化编排、又必须交付 Java 代码这类转换工具值得关注。接入企业微信、微信公众号很多团队希望用公众号聊天窗口接 DeepSeek。本质上就是写一个接收消息的 Controller把文本交给 ChatClient再通过公众号接口回复。图片、语音等消息类型则先做预处理再进入对话链路。本地模型混合如果对数据出域有要求可以本地部署 DeepSeek 的蒸馏版本用同一套 ChatClient 切换 base-url 即可业务代码基本不用动只是换了个模型底座。这些方向我在团队里陆续验证过效果都还不错。最后再分享一条实操经验无论后面做得多么花哨先把系统提示 多轮会话 流式输出 预算监控这四个基础能力打牢。基础稳固了后面接 Agent、接工作流都是水到渠成。
返回列表