Spring Boot + Spring AI + DeepSeek实战:Java生态快速集成大模型
2026/9/20 13:05:02 网站建设 项目流程

简介:这是一份基于Spring Boot与Spring AI框架、接入DeepSeek大语言模型的完整实战代码,面向Java后端开发者和AI应用落地人员。资源围绕智能问答、文本生成与语义分析三类典型应用场景,采用前后端分离的模块化设计,后端统一封装模型调用与业务逻辑,前端通过简洁页面完成交互与结果展示,整体结构清晰,适合作为企业级AI应用开发的入门范本。压缩包共12个文件,包括9个Java源文件、1个yml配置、1个xml依赖和1个HTML页面;Java代码覆盖服务封装、提示词构建、模型请求与响应处理等环节,yml与xml负责模型参数和依赖管理,HTML页面用于快速验证功能,整包仅25KB,轻量易读,便于梳理从请求到模型返回的完整链路。已有254人浏览学习,代码遵循Spring Boot工程化实践,能帮助读者掌握大模型与传统后端框架的集成思路,学习模块划分、配置外部化与轻量界面搭建等可复用方法,为后续接入更多AI能力或替换其他大模型提供参考基础。 最近做AI应用集成,我一直被一个问题困扰:Java后端要接大模型,难道非得用Python写一堆胶水代码?后来我把方案落到了Spring Boot + Spring AI + DeepSeek上,前后端完整跑通了,效果比我预期好很多。这篇文章就把这套实战代码和思路整理出来,包含项目结构、核心配置、后端接口、前端页面,以及几个不翻文档根本发现不了的坑。

这个项目解决什么问题?简单说,就是让Java体系内的开发者不用离开Spring生态,也能轻松调通DeepSeek大模型。它既能支撑一个简单的聊天页面,也能作为后续RAG、Function Calling、NL2SQL等功能的地基。适合正在做AI应用集成、想快速DeepSeek API入门、或者打算在业务系统里塞一个“AI助手”按钮的Java工程师。

1. 整体设计思路:为什么选Spring AI做中间层

1.1 直接HTTP调用和Spring AI的取舍

DeepSeek的API本质上是OpenAI兼容协议,所以很多人的第一反应是:直接用HttpClient打个POST请求不就行了?确实能通,但问题在于,一个正经的AI应用不只是“调一次API”。

多轮对话时,你要自己维护messages历史数组,把user、assistant的对话记录来回传。流式对话时,你要手工解析SSE格式的data流,还得处理每段JSON的截断问题。再加上超时重试、并发控制、模型切换,代码会迅速失控。我之前用原生HTTP写过一次,光SSE解析那部分就写了快两百行,后面加需求时根本不想维护。

Spring AI的价值,就是把这些脏活累活抽象成了ChatClient、ChatModel、Message、Prompt这一套标准编程模型。你只需要面向ChatClient写业务逻辑,底层的协议封装、响应解析、流式处理都由框架处理。就像用Spring Data JPA操作数据库,你不需要关心JDBC连接怎么写一样。

很多人用过ccswitch、deepseek harness这类工具给编辑器接入DeepSeek,本质上就是配置base-url、api-key、model三个关键参数。Spring Boot里接DeepSeek也是同一个逻辑,只是多了一层Spring AI帮我们管好了请求生命周期。

1.2 技术栈选型与版本搭配

我这套方案的技术栈是这样搭配的:

  • JDK 17 + Spring Boot 3.2.x:Spring AI 1.0基于Spring Framework 6.1,要求JDK 17起,所以Spring Boot 2.x用户需要先升级。
  • Spring AI 1.0.0稳定版:这是目前市面上用得最稳的版本,2.0还在迭代中,API有不少调整。后面章节我会单独提到版本差异。
  • DeepSeek API:按token计费,个人项目调试成本很低,而且不需要本地GPU资源。
  • 前端原生HTML + JavaScript:我记得这个项目核心是演示后端AI集成的完整链路,前端不做框架约束,用原生页面反而最能看清数据是怎么流动的。

还有一个选择要点:Spring AI官方并没有单独的"deepseek-starter",但DeepSeek兼容OpenAI协议,所以引入的是spring-ai-starter-model-openai,然后把base-url指向DeepSeek的接口地址就行。这也是这套方案最巧妙的地方,切换模型厂商往往只改一行配置。

2. 环境准备与项目骨架搭建

2.1 Maven依赖与BOM版本管理

需要用Maven方式构建Spring Boot项目,第一步就是把依赖坐标搞对。Spring AI的依赖包很多,最好用BOM统一管理版本,避免各个子模块版本不一致导致奇怪的兼容性问题。

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.5</version> <relativePath/> </parent> <properties> <java.version>17</java.version> <spring-ai.version>1.0.0</spring-ai.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>${spring-ai.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-openai</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> </dependencies>

这里有两个容易踩的坑。第一,Spring AI 1.0.0正式版已经发布到Maven Central,不需要额外配置仓库,但如果你用的是RC版或者M版本,需要在pom里加Spring的里程碑仓库。第二,artifactId是starter-model-openai,不是starter-model-deepseek,因为DeepSeek目前走的是OpenAI兼容通道。我第一次找DeepSeek专用starter找了大半天,结果发现方向就错了。

2.2 配置文件的落地与目录规范

配置文件这步最关键,直接决定能不能连上DeepSeek。建议把API密钥放到环境变量里,不要硬编码在yml中,否则代码一旦泄露密钥就暴露了。

spring: application: name: spring-ai-deepseek-demo ai: openai: base-url: https://api.deepseek.com api-key: ${DEEPSEEK_API_KEY} chat: options: model: deepseek-chat temperature: 0.7 max-tokens: 2048 model: chat: openai

注意base-url只需要填写到域名根路径,不需要带 /v1,Spring AI的OpenAI客户端会自动拼接。model这里我用的deepseek-chat,对应DeepSeek的通用对话模型,便宜且响应快。如果你要深度推理场景,可以换成deepseek-reasoner,但后面我会讲到它有一个大坑。

项目目录我建议遵循Spring Boot标准分层,同时单独拆出一个包放AI相关的配置和客户端,这样后续扩展RAG、Function Calling时不会污染业务代码。

src/main/java/com/example/ai/ ├── AIApplication.java ├── config/ │ └── ChatConfig.java ├── controller/ │ └── ChatController.java ├── service/ │ └── ChatService.java └── common/ └── ChatRequest.java src/main/resources/ ├── application.yml └── static/ └── index.html

3. 后端核心代码实现

3.1 构建可复用的ChatClient实例

Spring AI的用法里,ChatClient是个门面,它整合了模型调用、提示词模板、参数传递的全过程。我们通过自动注入的ChatClient.Builder来构建一个带默认系统提示词的实例。系统提示词很关键,它决定了整个对话的基调,相当于给AI设定角色。

@Configuration public class ChatConfig { @Bean public ChatClient chatClient(ChatClient.Builder builder) { return builder .defaultSystem("你是一位专业的Java技术博主,回答问题时请保持简洁、准确、条理清晰。") .build(); } }

我演示用的是系统提示词,但如果你做的是客服机器人,建议把系统提示词设计得更详细,包括语气、禁忌词、回复长度等。Spring AI也支持通过prompt().system()动态指定,适合多场景复用同一个ChatClient的情况。

3.2 普通对话接口与多轮上下文处理

后端接口设计上,我提供一个同步对话接口和一个流式对话接口。同步接口适合简单问答,流式接口适合聊天页面。这里需要处理一个重要问题:DeepSeek API本身是无状态的,多轮对话时你必须把历史消息传给它。

最简单可靠的方式是前端把历史对话数组传过来,后端原样透传给大模型。消息对象只用三个字段:role(user/assistant)、content、name(可选)。Spring AI的Message接口封装了这个约定,我们直接用UserMessage、AssistantMessage组装即可。

public record ChatRequest(String message, List<Map<String, String>> history) { }
@RestController @RequestMapping("/api/chat") public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient chatClient) { this.chatClient = chatClient; } @PostMapping("/sync") public Map<String, String> syncChat(@RequestBody ChatRequest request) { String reply = chatClient.prompt() .user(request.message()) .call() .content(); return Map.of("reply", reply == null ? "" : reply); } }

上面的代码只处理单轮对话。要做多轮,可以在prompt()调用时传入完整的历史消息列表,让模型根据上下文生成回复。Spring AI的ChatClient支持通过messages()方法传入List ,这样每次请求都携带完整上下文。因为我这个演示项目里,历史让前端维护,后端接口保持简洁,所以只接收message字段。实际业务系统中,你可以引入ChatMemory抽象来管理会话历史,避免前端每次传一长串历史数组。

3.3 流式对话接口的SSE返回

流式对话是大模型应用体验的分水岭。用过ChatGPT的人都知道,等待一整段文字出来才显示,那种体验是灾难性的。这里用Spring AI的stream()方法,返回Flux 响应流,配合Spring Web的SSE支持,每个token生成后立刻推给前端。

@PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<String> streamChat(@RequestBody ChatRequest request) { return chatClient.prompt() .user(request.message()) .stream() .content() .map(content -> content == null ? "" : content); }

这里有一个容易忽略的细节,返回类型必须是Flux ,不能是其他类型,否则Spring Web不会按SSE格式输出。还有一点,produces要明确指定为TEXT_EVENT_STREAM_VALUE,不然浏览器解析流式响应时容易出问题。

4. 前端页面实现:从表单到流式渲染

4.1 页面基础结构与消息展示

前端我特意没有用Vue、React这类框架,纯原生HTML就能跑通。整个页面就三块:消息展示区、输入框、发送按钮。消息区动态渲染用户和AI的对话气泡,这个逻辑比较简单,但有一个地方要注意:用户消息立即插入,AI回复则在流式接收过程中逐字追加,而不是等到全部接收完成后再一次性渲染。

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Spring AI + DeepSeek 聊天</title> <style> body { max-width: 800px; margin: 40px auto; font-family: system-ui, sans-serif; } #messages { border: 1px solid #e5e7eb; border-radius: 12px; padding: 20px; height: 500px; overflow-y: auto; margin-bottom: 16px; } .message { margin-bottom: 12px; padding: 10px 14px; border-radius: 10px; white-space: pre-wrap; word-break: break-word; } .user { background: #2563eb; color: white; margin-left: 40px; } .ai { background: #f3f4f6; color: #111827; margin-right: 40px; } #inputArea { display: flex; gap: 8px; } #messageInput { flex: 1; padding: 12px; border-radius: 8px; border: 1px solid #d1d5db; font-size: 14px; } #sendBtn { padding: 12px 24px; background: #2563eb; color: white; border: none; border-radius: 8px; cursor: pointer; } </style> </head> <body> <h2>Spring AI + DeepSeek Chat</h2> <div id="messages"></div> <div id="inputArea"> <input id="messageInput" type="text" placeholder="输入你的问题,按回车发送..."> <button id="sendBtn">发送</button> </div> </body> </html>

4.2 使用fetch处理SSE流式响应

流式接收这块是整个前端最核心的部分。Spring AI返回的SSE格式每一段是这样的:

data:{"content":"你好"}\n\n

注意这里每段事件之间用空行分隔。前端用fetch拿到响应体后,通过ReadableStream读取字节流,再按行拆解,找出data开头的数据。这里有一个必须注意的细节,中文在UTF-8编码下可能被截断在流的边界上,所以必须用TextDecoder的stream模式解码,否则会出现乱码。

const sendBtn = document.getElementById('sendBtn'); const messages = document.getElementById('messages'); const input = document.getElementById('messageInput'); function appendMessage(role, text) { const div = document.createElement('div'); div.className = 'message ' + role; div.textContent = text; messages.appendChild(div); messages.scrollTop = messages.scrollHeight; return div; } sendBtn.addEventListener('click', sendMessage); input.addEventListener('keydown', (event) => { if (event.key === 'Enter') sendMessage(); }); async function sendMessage() { const message = input.value.trim(); if (!message) return; input.value = ''; appendMessage('user', message); const aiMessageDiv = appendMessage('ai', ''); const response = await fetch('/api/chat/stream', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: message }) }); const reader = response.body.getReader(); const decoder = new TextDecoder('utf-8'); let buffer = ''; let reply = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n\n'); buffer = lines.pop(); for (const line of lines) { if (line.startsWith('data:')) { const data = line.substring(5).trim(); if (data && data !== '[DONE]') { try { const parsed = JSON.parse(data); const content = parsed.content || ''; reply += content; aiMessageDiv.textContent = reply; messages.scrollTop = messages.scrollHeight; } catch (e) { console.warn('解析SSE数据失败:', data); } } } } } }

这个read循环就是整个流式教程的精髓所在。buffer的处理很关键,因为一次网络读取可能包含多条data,也可能一条data只读了一半,所以需要先用split('\n\n')切分完整事件,再把剩余的半截数据留在buffer里等下一次读取,这样才能保证SSE解析不出错。

5. 核心参数调优与可观测性建设

5.1 对话参数中每个值背后的含义

很多人在application.yml里抄了一堆参数,但不理解它们到底影响什么。我挑最常用的几个说一下。

temperature控制随机性,数值越低回答越确定,适合代码生成、SQL转换这类要求精确的场景;数值越高回答越发散,适合头脑风暴。top_p和temperature是互补关系,它控制候选词的概率累加范围,一般两者只挑一个调就行。max_tokens限制生成的最大token数,注意DeepSeek的deepseek-chat模型上下文窗口比较大,但这个值是限制单次回复长度的,不是上下文长度。presence_penalty惩罚重复内容,数值越高模型越不愿意重复已说过的词,适合长文本生成。

参数推荐值适用场景注意事项
temperature0.7通用对话、文案生成代码生成建议降到0.1-0.3
max_tokens2048普通问答长文总结时调到4000以上
top_p1.0默认值与temperature二选一调整
presence_penalty0.0默认值长文生成可设为0.3-0.5

你可以一边调整参数一边通过前端页面对比输出效果,不要一次性大改。我在调试过程中最常用的做法是,固定max_tokens不变,先把temperature从0.1到1.0每隔0.2测一轮,找到稳定输出和创意输出的分界线,再根据业务场景落到具体值。

5.2 Actuator与Micrometer监控AI调用链路

生产环境不能只“能跑”,你还得看到AI调用的耗时、token消耗、错误率。Spring Boot Actuator加上Micrometer这套组合,正好能把这些指标暴露出来。Spring AI内部通过Micrometer Observation机制对所有AI调用埋点,注册一个ObservationHandler就能自动收集指标。

management: endpoints: web: exposure: include: health,info,metrics

这里我必须强调一个安全细节。很多网上教程让开发者把actuator的端点全部暴露出来,比如management.endpoints.web.exposure.include=*,这在生产环境非常危险。一旦你的Spring Boot应用对公网开放,攻击者可以通过/env端点读取环境变量,通过/heapdump下载堆内存文件,甚至能扒出数据库密码和API密钥。spring boot actuator相关的安全漏洞这些年出过不少,核心原因就是端点暴露过度。正确做法是只暴露必要的health、info、metrics,并把管理端口和业务端口分开,或者加上Spring Security鉴权。

如果你想看AI调用的具体指标,可以注册一个简单的MeterHandler类,继承ObservationHandler接口,把Spring AI产生的指标都记录下来。然后通过actuator的metrics端点查询类似spring.ai.client.operation的指标数据,就能看到每次pipeline的调用次数、耗时分布、token用量。这个对于一个要上线的AI应用来说,属于必须做的一步。

6. 常见问题与排查实录

6.1 DeepSeek reasoner的reasoning_content报错

这个坑是我花了两天才爬出来的。DeepSeek的deepseek-reasoner模型(推理模式)在返回结果时,除了正常的content字段,还有一个reasoning_content字段,用于存放模型的推理过程。

问题在于,Spring AI的OpenAI客户端基于OpenAI标准协议实现,这个协议里没有reasoning_content字段。当你连续多轮对话时,客户端的消息列表里只保留了content和role,丢失了reasoning_content。而DeepSeek服务端要求,推理模型的reasoning_content在多轮对话时必须原样回传,否则直接返回HTTP 400错误,提示“reasoning_content in thinking mode must be passed back to the api”。

我当时的实际排查步骤是:先用curl手动调试API,单轮对话正常,多轮对话必现400;然后打开Spring AI的日志,对比请求体和服务端要求,发现缺少reasoning_content字段。

解决方案有两个。最简单的就是不要用deepseek-reasoner,改用deepseek-chat,日常问答完全够用。如果一定要用推理模型,就不能依赖Spring AI的原生客户端,需要自定义一个WebFilter或者拦截器,在请求发出前把历史消息中的reasoning_content补回去,这个实现起来工作量不小。

6.2 依赖下载失败与Spring AI版本API差异

如果你使用的是雷打不动的Spring Boot 3.2.x,Spring AI版本又用的是M1、RC1这类早期版本,很容易拉不到依赖。这类版本发布在Spring里程碑仓库里,需要单独配置。

<repositories> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> </repository> </repositories>

另外要注意,Spring AI 2.0相比1.0在API上有不少调整。1.0时代我们用的是ChatClient.Builder注入,到了2.0,流式响应类型、ObservationHandler的处理方式都有变化。如果你的项目从1.0升级到2.0,不要指望代码无缝迁移,先去看官方Migration Guide,否则一堆编译错误会让你怀疑人生。

6.3 中文乱码与SSE半字截断

这个问题前面提过,我再详细展开一下。用SSE做流式对话时,如果只做一次response.text(),然后直接按行解析,最新一个字经常变成乱码。原因是服务端在返回中文时,一个字可能占据多个字节,一次网络传输刚好把一个字的字节切开了,导致解码失败。解决方式很简单,前面代码里的TextDecoder加上{ stream: true }参数就能正确处理跨块字符。

还有一个更隐蔽的问题,如果你的后端接口返回的SSE中content带着双引号转义("),前端解析时字符串被切割错位,这是JSON序列化格式不一致导致的。你可以统一在服务端设置消息格式,或者前端解析时做一次en/decoding处理,实际项目中两种都见过,我的建议是后端直接输出纯文本格式的SSE,不要套JSON,最省事。

6.4 超时、重试与密钥安全

DeepSeek接口的默认超时时间是固定的,但深度推理时响应可能非常慢,尤其是deepseek-reasoner,简单问题也可能思考半分钟。如果你没有设置超时时间,前端请求很容易断开。建议在配置里显式增加超时配置:

spring: ai: openai: client: connect-timeout: 30s read-timeout: 60s

密钥安全再啰嗦一遍,不要在前端代码里写api-key,不要在后端代码里硬编码,用环境变量注入,而且生产环境的密钥尽量使用独立的key,控制额度,这样即使泄露也能快速吊销,不会影响主账号。

7. 个人经验与后续扩展方向

这套Spring Boot + Spring AI + DeepSeek全栈代码跑通之后,我最大的感触是:Java生态接入大模型,不需要为了调API去专门搭一套Python服务,Spring AI把模型调用的复杂度收敛得很好,前端、后端、配置加起来不到三百行代码就能出一个真正能用的AI聊天应用。

在实际调试中,我建议你先把同步接口跑通,再切换流式接口,不要一上来就搞SSE,否则前端解析逻辑出问题时,你分不清是后端Flux的问题还是前端解析的问题。另外,每次调整prompt或者参数时,在代码里加一个简单的日志输出,把最终发送给DeepSeek的请求体打出来,排查问题时效率会高很多。

这个项目后续的扩展方向其实很清晰。想给文档做问答,就往Spring AI里接入向量数据库做RAG;想让模型能查数据库、调接口,就研究Function Calling;如果业务偏阿里系,可以关注spring-ai-alibaba,它对NL2SQL这类场景做了不少开箱即用的支持。总而言之,地基已经打好了,往上盖什么楼,就看你的业务需求了。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询