ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Spring Boot+Vue3接入DeepSeek:Spring AI全栈实战

2026/9/25 2:52:26 拓冰建站 浏览量
Spring Boot+Vue3接入DeepSeek:Spring AI全栈实战 简介面向后端开发者与AI应用初学者的 Spring Boot Spring AI DeepSeek 集成实战代码包展示如何通过 Spring Boot 与 Spring AI 框架接入 DeepSeek 大模型构建智能问答、文本生成和语义分析等功能。项目采用前后端分离与模块化设计前端页面负责交互展示后端 Java 实现业务逻辑与模型调用适合作为企业级 AI 应用开发起点也便于后续扩展更多 AI 服务。压缩包共 12 个文件约 25KB包含 9 个 Java 源码文件、1 个 YAML 配置文件、1 个 POM 依赖配置和 1 个 HTML 页面覆盖模型接入配置、服务调用封装、前端展示等完整链路。目录结构清晰代码量适中便于快速理解 Spring AI 的集成方式与调用流程。已有 254 人学习下载。通过学习这份代码可掌握 Spring AI 与 DeepSeek 的实际整合方法包括依赖管理、自动配置、语义分析调用等关键环节为后续自主开发 AI 应用提供可直接参考的实现模板。1. 一套能直接跑的 DeepSeek 全栈对话项目Spring Boot 后端、Vue3 前端照着拆就行项目要接 DeepSeek 时网上一搜多是 Python 脚本Java 这边要么裸用 HTTP 客户端手搓要么东拼西凑几个轮子前期跑通很快一旦上流式输出、多轮上下文、工具调用代码立刻失控。这套 Spring Boot 与 Spring AI 深度实战的完整代码把后端 AI 能力和前端交互都打包进同一个工程后端用 Spring AI 的 OpenAI 兼容层连 DeepSeek前端用 Vue3 消费流式接口适合刚完成前后端分离改造、又要快速上 AI 功能的 Java 团队逐行参考。这篇文章按这套代码的拆解顺序来写从依赖配置到接口联通再到排错照着可以复现。2. 环境与骨架Spring AI 依赖版本与 DeepSeek 配置项逐条拆解2.1 为什么选 Spring AI而不是 HttpClient 手搓 OpenAI 接口我见过不少项目直接拿 RestTemplate 调 DeepSeek 的/chat/completions同步调用确实能跑但很快会遇到三件麻烦事流式响应要自己解析text/event-stream多轮对话要自己维护 messages 数组工具调用要自己拼tool_calls和tool消息。这三块代码不难但每一块都有边界情况比如流式中途断线、上下文超长截断、tool call 结果未及时返回导致模型报错。手搓到后面业务代码里全是 JSON 拼接和状态机维护成本很高。Spring AI 在这里的价值是给 Java 生态提供了类似 JDBC 对数据库那样的统一抽象。它把 ChatClient、Prompt、Message、Memory 都封装好了底层接哪个模型只是配置问题。DeepSeek 官方提供 OpenAI 兼容接口所以可以直接用 Spring AI 的 OpenAI starter把base-url指向 DeepSeek 的地址。和 LangChain 那套重框架相比Spring AI 更轻能复用 Spring 的 Bean 管理和配置体系对已有 Spring Boot 项目来说接入成本最低。2.2 依赖文件与配置文件OpenAI 兼容协议接入 DeepSeek这套代码的基础依赖如下Spring AI 从 1.0 正式版开始统一了包名使用spring-ai-starter-model-openaidependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-webflux/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId version1.0.0/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency /dependencies依赖说明这里没有引入spring-boot-starter-web而是直接用 WebFlux原因是流式接口返回FluxString更适合在 WebFlux 下跑。如果老项目已经用了 Spring MVC也可以把 webflux 依赖去掉Controller 里直接返回Flux同样能工作但要避免两个 Web 框架同时生效带来的路由歧义。核心配置写在application.ymlspring: application: name: spring-ai-deepseek-demo ai: openai: base-url: https://api.deepseek.com/v1 api-key: ${DEEPSEEK_API_KEY} chat: options: model: deepseek-chat temperature: 0.7 max-tokens: 1024 server: port: 8080参数说明base-url指向 DeepSeek 的 OpenAI 兼容端点注意末尾的/v1要不要保留不同版本的处理逻辑不一样后面避坑章节会展开。api-key不要写死在配置文件里用环境变量注入。模型名写deepseek-chat这是 DeepSeek 官方对话模型的接入名不要随手填成gpt-3.5-turbo或别的。temperature控制在 0.7 左右既能保持回答稳定又不至于太死板。max-tokens决定单次回答最大长度要结合业务调整。3. 后端实现拆解ChatClient 同步、流式与上下文的三条链路3.1 同步调用 ChatClient最小可用版本与参数意义ChatClient 是 Spring AI 1.0 的主力入口有点类似 JdbcTemplate所有与大模型交互的操作都从它发起。这套代码里用 Builder 创建一个带默认系统提示的客户端RestController RequestMapping(/api/chat) public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient.Builder builder) { this.chatClient builder .defaultSystem(你是一名 Java 编程助手回答保持简洁代码示例优先。) .defaultOptions(ChatOptions.builder() .model(deepseek-chat) .temperature(0.7) .maxTokens(1024) .build()) .build(); } PostMapping(/sync) public String sync(RequestParam String message) { return chatClient.prompt() .user(message) .call() .content(); } }这段代码的逻辑是通过ChatClient.Builder注入一个带默认配置的客户端defaultSystem设置了系统提示词defaultOptions固定了模型和生成参数。每次请求时prompt().user(message)组装用户消息.call()发起同步调用.content()取出模型返回的文本。同步调用适合接口内部需要立刻拿到结果的场景比如生成标题、摘要、分类标签。如果前端页面需要用户看到逐字输出就必须走流式接口。3.2 流式输出WebFlux 转发 SSE 并保留打字机节奏DeepSeek 的流式返回本身就是 SSE 格式Spring AI 把底层解析做掉了我们拿到的是一串String文本块。后端要做的是把这些文本块再以 SSE 格式转发给前端GetMapping(value /stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxString stream(RequestParam String message) { return chatClient.prompt() .user(message) .stream() .content() .map(chunk - data: chunk \n\n); }逻辑说明stream().content()返回FluxString每到一个 chunk 就执行一次map按 SSE 协议包装成data:xxx加空行的格式。前端 EventSource 收到这种格式会自动解析成event.data。这里没有做跨域处理因为前后端联调时建议走 Vite 代理或 Nginx 同源部署避免浏览器跨域限制。如果你在项目里看到SseEmitter那是 Spring MVC 的另一种 SSE 实现方式和 WebFlux 的Flux二选一即可。使用Flux的好处是天然支持背压模型输出慢时不会把内存撑爆。流式输出还有一个隐藏点如果同时用到工具调用tool_calls不会出现在content()流里需要走 ChatModel 底层去解析这是后话。3.3 多轮上下文MessageChatMemoryAdvisor 的记忆与截断策略模型本身是无状态的多轮对话需要把历史消息一起发过去。手搓的方案是自己在 Redis 里存 messages 数组每次请求前拼接。Spring AI 提供了 ChatMemory 和 Advisor 机制可以少写不少胶水代码Configuration public class ChatConfig { Bean public ChatMemory chatMemory() { return new InMemoryChatMemory(); } Bean public ChatClient chatClient(ChatClient.Builder builder, ChatMemory chatMemory) { return builder .defaultSystem(你是电商客服助手回答要耐心。) .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory)) .build(); } }逻辑说明InMemoryChatMemory是进程内实现按会话 ID 存储消息列表适合单机演示和中小型项目。MessageChatMemoryAdvisor是拦截器每次请求时自动把历史消息取出来拼进 prompt并把当前问答追加回记忆区。参数说明MessageChatMemoryAdvisor有个窗口大小参数默认保留最近 20 条消息超出后自动丢弃最旧的。这个值要结合模型上下文长度和业务场景调整客服机器人可以给到 30问诊类场景建议 10 以内。使用InMemoryChatMemory的另一个注意点是多实例部署时会丢记忆因为每个实例的内存是独立的。生产环境需要把 ChatMemory 的存储换成 Redis 或数据库Spring AI 提供了ChatMemory接口自己实现并不复杂。4. 前端联调拆解Vue3 怎么消费 SSE 流式数据4.1 接口约定GET SSE 为什么在这里是成立的Vue3 连接后端的方式最常规的是 axios 调 JSON 接口。但流式对话场景不太适合 axios因为 axios 基于 XHR没有原生的流式读取能力虽然可以用onprogress事件勉强读到部分数据但兼容性和体验都不好。SSE 场景下浏览器原生提供的 EventSource 是最省事的方案。EventSource 有几个限制只能发 GET 请求不能自定义请求头。正因如此这套代码把 API Key 放在后端前端只通过 URL 传用户消息不接触任何密钥。用户消息通过encodeURIComponent转义后放在 query 参数里后端RequestParam直接接收。中断生成时EventSource 实例调用close()即可断开不需要像 WebSocket 那样维护连接状态。4.2 Vue3 消费 SSEEventSource 的正确打开方式前端封装一个组合式函数专门处理流式连接import { ref } from vue export function useChatStream() { const content ref() let eventSource null function connect(message) { content.value const url /api/chat/stream?message${encodeURIComponent(message)} eventSource new EventSource(url) eventSource.onmessage (event) { content.value event.data } eventSource.onerror () { eventSource.close() content.value \n[连接中断] } return () eventSource.close() } return { content, connect } }逻辑说明new EventSource(url)建立连接后后端推送的每个data:块都会触发onmessageevent.data就是文本块直接追加到响应内容里。这里要注意 URL 必须用encodeURIComponent处理中文否则浏览器会报编码错误或后端收到乱码。使用时的注意点EventSource 默认自带断线重连机制但重连会重新请求同一个 URL消息会从空开始。如果业务上需要恢复历史记录前端要先把历史对话保存在 localStorage 或后端重连时带上上下文 ID。4.3 打字机效果与 Markdown 渲染流式数据边到边显示本身就有打字机的效果但网络抖动时文本块会一下子涌进来视觉上不流畅。常见做法是加一个定时器做平滑输出function typewriter(text, onTick, interval 20) { let index 0 const timer setInterval(() { index 1 onTick(text.slice(0, index)) if (index text.length) { clearInterval(timer) } }, interval) return () clearInterval(timer) }参数说明interval是每次输出的间隔毫秒数20ms 大约是每秒 50 个字适合正常阅读节奏。如果回答内容很长可以提高到 30ms避免用户等待过久。注意这个函数接收的text是已经收集完成的完整文本实际项目里可以等流式结束后再调用也可以边收边渲染。模型回答通常是 Markdown 格式直接显示在页面上会露出**加粗**等原始符号。组件里引入 markdown-it 做渲染import MarkdownIt from markdown-it const md new MarkdownIt({ html: false, linkify: true, breaks: true }) function renderMarkdown(rawText) { return md.render(rawText) }逻辑说明html: false禁止渲染原始 HTML防止 XSS这是接入模型输出时必须开的开关。linkify让链接自动可点击breaks把换行符转成br。代码高亮需要额外引入 highlight.js在 markdown-it 的 render 规则里处理code块否则代码块只是一堆灰底文字。5. 常见问题与排查五个让流式对话翻车的典型配置5.1 base-url 配置错误请求打到不存在的接口上现象后端启动不报错但调用/api/chat/sync时返回 404控制台显示请求地址类似https://api.deepseek.com/v1/v1/chat/completions。原因DeepSeek 官方兼容地址本身带有/v1前缀而 Spring AI 的 OpenAI 客户端有的版本会自动拼接/chat/completions有的版本则把base-url当作完整前缀不做处理。两段/v1叠在一起路径就错了。解决先不要猜打开浏览器开发者工具或后端日志看完整的请求 URL。如果多了一段/v1就把base-url改成https://api.deepseek.com如果少了一段就保留/v1。我自己的习惯是先拿 curl 跑通接口再参照 curl 的 URL 去配 Spring AI。5.2 Nginx 开启缓冲SSE 变成了一次性吐出来现象本地联调时打字机效果正常部署到服务器后前端等很久才一次性显示完整回答。原因Nginx 默认开启proxy_buffering会把后端流式响应的内容攒在一起等到连接结束才转发给客户端。SSE 的实时性完全被破坏。解决在 Nginx 的 location 里关掉缓冲location /api/chat/stream { proxy_pass http://backend; proxy_buffering off; proxy_cache off; proxy_set_header X-Accel-Buffering no; }配置说明proxy_buffering off让 Nginx 收到一块就转发一块X-Accel-Buffering no是告诉上游服务器不要对响应做额外缓冲。修改配置后记得nginx -s reload。5.3 上下文无限增长请求体超过模型窗口限制现象对话进行到十几轮后后端开始报 400 或 413提示 token 超限有时是模型返回maximum context length exceeded。原因MessageChatMemoryAdvisor虽然提供了窗口截断但如果窗口设得过大或者历史消息里包含大量代码块很快就会触达 DeepSeek 的上下文上限。还有一种是禁用 advisor 后自己手动拼接历史每次请求把所有消息全发过去。解决显式设置 advisor 的窗口大小例如new MessageChatMemoryAdvisor(chatMemory, 20)并在压测时监控请求体大小。对历史消息做压缩早期对话摘要成一句话再放进上下文这是多轮对话上线前必须做的优化。5.4 deepseek-reasoner 的思考内容被丢掉现象切换到大模型的深度思考模式后前端只能看到最终答案看不到推理过程而官方网页端有完整的思考链展示。原因DeepSeek 的 reasoner 模型在返回里带了reasoning_content字段Spring AI 的 OpenAI 兼容解析默认只映射contentreasoning_content在转换时被丢弃了。解决如果只是做日常对话建议继续用deepseek-chat。如果你确实需要把思考过程展示给用户需要自定义响应解析器在 ChatModel 层拦截原始响应并把reasoning_content单独取出来。很多团队把思考链当成产品亮点这条建议值得投入精力。5.5 前端用 fetch 读流拿到一段 Buffer 乱码现象没用 EventSource改走fetchReadableStream后页面显示的不是文字而是Uint8Array或乱码。原因fetch 拿到的响应体是二进制流需要用TextDecoder手动解码。EventSource 内部默认按文本处理所以没有这个问题。解决在读取循环里加一个解码器const reader response.body.getReader() const decoder new TextDecoder(utf-8) while (true) { const { done, value } await reader.read() if (done) break const text decoder.decode(value, { stream: true }) // 按 SSE 格式解析 text }逻辑说明decoder.decode(value, { stream: true })表示当前数据块可能是不完整的多字节字符需要留到下一块一起解码。全部读完后再调用一次decoder.decode()冲刷缓冲区。前端手写 SSE 解析器不是不能做但 EventSource 已经覆盖了大部分场景不建议重复造轮子。6. 收尾建议冒烟测试、模型切换与断连重连的最后一公里6.1 二十行冒烟测试先跑通三类请求接入任何大模型我一般会先写一个测试类把同步和流式两条链路都验证一遍再动业务代码SpringBootTest class DeepSeekSmokeTest { Autowired private ChatClient chatClient; Test void syncChatShouldReturnContent() { String reply chatClient.prompt() .user(只回复两个字收到) .call() .content(); Assertions.assertNotNull(reply); } Test void streamChatShouldEmitChunks() { FluxString flux chatClient.prompt() .user(从1数到5) .stream() .content(); StepVerifier.create(flux) .expectNextCount(1) .verifyComplete(); } }StepVerifier依赖reactor-test测试时能看到流式数据是否真的分块返回。这一层跑通后再去调前端问题定位会清晰很多。6.2 一键切换模型厂商的配置姿势DeepSeek、通义、智谱这类平台大多提供 OpenAI 兼容接口切换时只动配置不动代码spring: ai: openai: base-url: ${AI_BASE_URL:https://api.deepseek.com} api-key: ${AI_API_KEY:} chat: options: model: ${AI_MODEL:deepseek-chat}环境变量里把AI_BASE_URL、AI_API_KEY、AI_MODEL三项改掉即可。注意不同厂商对temperature、max_tokens的取值范围有差异切换后跑一遍冒烟测试最稳。6.3 前端断连重连接管重试而不是依赖默认行为EventSource 默认会自动重连但重连后消息是重新开始流的用户可能看到重复内容。更可控的做法是自己管理重连次数let retryCount 0 const MAX_RETRIES 3 function connect(url, onData) { const es new EventSource(url) es.onmessage (event) { retryCount 0 onData(event.data) } es.onerror () { es.close() if (retryCount MAX_RETRIES) { retryCount setTimeout(() connect(url, onData), 2000) } } }重试间隔用 2 秒最多重试 3 次。超过次数后提示用户手动重发而不是无限重连把服务器打满。从那以后我每接一家大模型供应商都先把同步、流式、上下文这三条链路用冒烟测试跑通再让前端介入联调只要对方兼容 OpenAI 协议这套流程基本能复用到任何一家模型上。希望帮到你。本文还有配套的精品资源点击获取