Spring AI 1.x 系列【6】集成 DeepSeek + 智谱 GLM,实现多模型一键切换的 AI 聊天助手

Spring AI 1.x 系列【6】集成 DeepSeek + 智谱 GLM,实现多模型一键切换的 AI 聊天助手

文章目录

1. 项目介绍

从零搭建一套 AI 聊天助手,基于 Spring AI 同时集成 DeepSeek 和 智谱 GLM 两大主流模型,实现前端一键切换模型流式对话等完整功能。

1.1 功能演示

顶部下拉框一键切换 DeepSeek / 智谱GLM 模型:

在这里插入图片描述

支持消息流式输出:

在这里插入图片描述

1.2 技术栈

核心技术栈:

  • 前端Thymeleaf + SSE 流式输出。
  • 后端Spring Boot 3.5.x + Spring AI 1.1.2
  • AI模型DeepSeek Chat、智谱 GLM-4

2. 环境准备

2.1 申请 API Key

DeepSeek:前往 DeepSeek 开放平台 创建 API Key
智谱 AI:前往 智谱开放平台 创建 API Key

2.2 创建工程

工程结构如下:

在这里插入图片描述

2.3 Maven 核心依赖

pom.xml 中引入 Spring AI 相关依赖,同时支持 DeepSeek 和智谱:

<?xml version="1.0" encoding="UTF-8"?><projectxmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>com.study</groupId><artifactId>study-spring-ai</artifactId><version>0.0.1-SNAPSHOT</version><relativePath>../pom.xml</relativePath></parent><groupId>com.example</groupId><artifactId>ai-chat-demo</artifactId><version>0.0.1-SNAPSHOT</version><name>ai-chat-demo</name><description>ai-chat-demo</description><properties><java.version>17</java.version><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!--DeepSeek--><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-starter-model-deepseek</artifactId></dependency><!--智谱AI--><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-starter-model-zhipuai</artifactId></dependency><!-- Thymeleaf for web UI --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>

3. 后端实现

3.1 配置文件

配置两大模型的 API 信息:

server:port:8081spring:application:name: ai-chat-demo ai:chat:client:enabled:false# DeepSeek 配置deepseek:api-key: 你的 DeepSeek API Key base-url: https://api.deepseek.com model: deepseek-chat # 智谱 GLM 配置zhipu:api-key: 你的 智谱 API Key base-url: https://open.bigmodel.cn/api/paas model: glm-4

3.2 对话客户端配置类

创建两个独立的 ChatClient Bean,分别对应 DeepSeek 和智谱:

@ConfigurationpublicclassChatClientConfig{@Bean("zhiPuAiChatClient")publicChatClientzhiPuAiChatClient(ZhiPuAiChatModel zhiPuAiChatModel){returnChatClient.builder(zhiPuAiChatModel).build();}@Bean("deepSeekChatClient")publicChatClientdeepSeekChatClient(DeepSeekChatModel deepSeekChatModel){returnChatClient.builder(deepSeekChatModel).build();}}

3.3 对话生成访问接口

通过 model 参数动态选择模型,兼容普通接口和流式接口:

importorg.springframework.ai.chat.client.ChatClient;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.beans.factory.annotation.Qualifier;importorg.springframework.http.MediaType;importorg.springframework.stereotype.Controller;importorg.springframework.web.bind.annotation.*;importreactor.core.publisher.Flux;importjava.util.Map;importjava.util.UUID;@ControllerpublicclassChatController{// 注入 DeepSeek 和智谱的 ChatClientprivatefinalChatClient deepSeekChatClient;privatefinalChatClient zhiPuAiChatClient;// 构造方法注入多个 ChatClient(替换原有单一注入)@AutowiredpublicChatController(@Qualifier("deepSeekChatClient")ChatClient deepSeekChatClient,@Qualifier("zhiPuAiChatClient")ChatClient zhiPuAiChatClient){this.deepSeekChatClient = deepSeekChatClient;this.zhiPuAiChatClient = zhiPuAiChatClient;}// 首路由,返回聊天页面@GetMapping("/")publicStringchatPage(){return"chat";}/** * 非流式生成接口(支持模型切换) * * @param message 用户消息 * @param model 模型名称(deepseek/zhipu,默认deepseek) * @return 模型回复 */@GetMapping("/ai/generate")@ResponseBodypublicMap<String,String>generate(@RequestParam(value ="message", defaultValue ="你好")String message,@RequestParam(value ="model", defaultValue ="deepseek")String model){try{// 根据模型名称获取对应的 ChatClientChatClient targetClient =getChatClientByModel(model);String response = targetClient.prompt().user(message).call().content();returnMap.of("generation", response,"usedModel", model);// 新增返回使用的模型,方便前端确认}catch(Exception e){returnMap.of("generation","错误: "+ e.getMessage(),"usedModel", model);}}/** * 流式生成接口(支持模型切换) * * @param message 用户消息 * @param model 模型名称(deepseek/zhipu,默认deepseek) * @return 流式响应 */@GetMapping(value ="/ai/generate/stream", produces =MediaType.TEXT_EVENT_STREAM_VALUE)@ResponseBodypublicFlux<String>generateStream(@RequestParam(value ="message", defaultValue ="你好")String message,@RequestParam(value ="model", defaultValue ="deepseek")String model){try{// 根据模型名称获取对应的 ChatClientChatClient targetClient =getChatClientByModel(model);return targetClient.prompt().user(message).stream().content().onErrorResume(e ->Flux.just("错误: "+ e.getMessage()));}catch(IllegalArgumentException e){// 模型名称错误时返回提示returnFlux.just("错误: "+ e.getMessage());}}/** * 创建新会话,返回新的会话ID * * @return 新会话ID */@GetMapping("/api/conversation/new")@ResponseBodypublicMap<String,String>newConversation(){returnMap.of("conversationId",UUID.randomUUID().toString());}/** * 核心:根据模型名称获取对应的 ChatClient * * @param model 模型名称(deepseek/zhipu) * @return 对应的 ChatClient * @throws IllegalArgumentException 模型不支持时抛出异常 */privateChatClientgetChatClientByModel(String model){returnswitch(model.toLowerCase()){case"deepseek"-> deepSeekChatClient;case"zhipu","glm"-> zhiPuAiChatClient;default->thrownewIllegalArgumentException("不支持的模型:"+ model);};}}

4. 前端页面

resources/templates 目录下创建聊天界面 chat.html ,重点涉及:

  • 接收后端流式响应(SSE/Server-Sent Events)处理,实现实时打字效果。
  • 模型切换下拉框,传递 model 参数。
<!DOCTYPEhtml><htmllang="zh-CN"xmlns:th="http://www.thymeleaf.org"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>AI聊天助手</title><style>*{margin: 0;padding: 0;box-sizing: border-box;}body{font-family:'Arial','Microsoft YaHei', sans-serif;background:linear-gradient(135deg, #f9e7d8 0%, #f5d6b8 50%, #e8c8a0 100%);height: 100vh;overflow: hidden;position: relative;}.chat-container{width: 95%;max-width: 900px;height: 90vh;background:rgba(255, 255, 255, 0.95);border-radius: 20px;box-shadow: 0 10px 30px rgba(222, 184, 135, 0.15);display: flex;flex-direction: column;overflow: hidden;position: relative;margin: 20px auto;border: 2px solid #e6b89c;}.chat-header{background:linear-gradient(90deg, #e69c68 0%, #d98850 100%);color: white;padding: 18px;text-align: center;font-size: 1.6rem;font-weight: 600;position: relative;display: flex;justify-content: space-between;align-items: center;}.chat-header-title{flex: 1;text-align: center;}/* 新增:模型选择下拉框样式 */.model-selector{background:rgba(255, 255, 255, 0.2);border: 1px solid rgba(255, 255, 255, 0.3);color: white;padding: 8px 16px;border-radius: 20px;font-size: 0.9rem;cursor: pointer;transition: all 0.3s ease;margin-left: 10px;outline: none;}.model-selector:hover{background:rgba(255, 255, 255, 0.3);}.model-selector option{background: #d98850;color: white;border: none;}.new-chat-btn{background:rgba(255, 255, 255, 0.2);border: 1px solid rgba(255, 255, 255, 0.3);color: white;padding: 8px 16px;border-radius: 20px;cursor: pointer;font-size: 0.9rem;transition: all 0.3s ease;margin-right: 10px;}.new-chat-btn:hover{background:rgba(255, 255, 255, 0.3);transform:translateY(-2px);}.header-placeholder{width: 80px;}.chat-messages{flex: 1;padding: 20px;overflow-y: auto;background:radial-gradient(circle at 20% 30%,rgba(249, 231, 216, 0.1) 0%, transparent 50%),radial-gradient(circle at 80% 70%,rgba(232, 200, 160, 0.1) 0%, transparent 50%);}.message{margin-bottom: 20px;display: flex;animation: fadeIn 0.5s ease-out;}@keyframes fadeIn{from{opacity: 0;transform:translateY(20px)scale(0.95);}to{opacity: 1;transform:translateY(0)scale(1);}}.user-message{justify-content: flex-end;}.ai-message{justify-content: flex-start;}.message-content{max-width: 75%;padding: 15px 20px;border-radius: 20px;font-size: 1.1rem;line-height: 1.5;position: relative;box-shadow: 0 2px 10px rgba(222, 184, 135, 0.1);border: 2px solid transparent;}.user-message .message-content{background:linear-gradient(135deg, #f0b890 0%, #e69c68 100%);color: white;border-color: #d98850;border-bottom-right-radius: 8px;}.ai-message .message-content{background:linear-gradient(135deg, #faf6f0 0%, #f9e7d8 100%);color: #333;border-color: #e6b89c;border-bottom-left-radius: 8px;}.chat-input{padding: 20px;background: #faf6f0;border-top: 2px solid #e6b89c;display: flex;gap: 15px;position: relative;}.message-input{flex: 1;padding: 15px 20px;border: 2px solid #e6b89c;border-radius: 30px;font-size: 1.1rem;outline: none;background:rgba(255, 255, 255, 0.9);transition: all 0.3s ease;box-shadow: 0 2px 8px rgba(222, 184, 135, 0.1);}.message-input:focus{border-color: #d98850;box-shadow: 0 2px 15px rgba(217, 136, 80, 0.2);transform:scale(1.01);}.send-button{padding: 15px 30px;background:linear-gradient(135deg, #e69c68 0%, #d98850 100%);color: white;border: none;border-radius: 30px;font-size: 1.1rem;font-weight: 600;cursor: pointer;transition: all 0.3s ease;box-shadow: 0 2px 10px rgba(217, 136, 80, 0.2);}.send-button:hover{transform:translateY(-2px);box-shadow: 0 4px 15px rgba(217, 136, 80, 0.3);}.send-button:active{transform:translateY(0);}.send-button::after{content:"→";margin-left: 8px;display: inline-block;transition: transform 0.3s ease;}.send-button:hover::after{transform:translateX(5px);}.send-button:disabled{opacity: 0.6;cursor: not-allowed;transform: none;box-shadow: none;}.typing-indicator{display: none;padding: 15px 20px;background:linear-gradient(135deg, #faf6f0 0%, #f9e7d8 100%);border: 2px solid #e6b89c;border-radius: 20px;border-bottom-left-radius: 8px;margin-bottom: 20px;position: relative;}.typing-indicator.show{display: flex;align-items: center;}.typing-dots{display: flex;align-items: center;gap: 6px;}.typing-dot{width: 10px;height: 10px;background: #d98850;border-radius: 50%;animation: typing 1.4s infinite ease-in-out;}.typing-dot:nth-child(1){animation-delay: 0s;}.typing-dot:nth-child(2){animation-delay: 0.2s;}.typing-dot:nth-child(3){animation-delay: 0.4s;}@keyframes typing{0%, 60%, 100%{transform:translateY(0)scale(1);opacity: 0.7;}30%{transform:translateY(-8px);opacity: 1;}}.welcome-message{text-align: center;color: #555;font-size: 1.2rem;margin: 30px 0;padding: 30px;background:linear-gradient(135deg, #fff 0%, #faf6f0 100%);border-radius: 20px;border: 2px solid #e6b89c;box-shadow: 0 4px 15px rgba(222, 184, 135, 0.1);}.welcome-message h3{color: #d98850;margin-bottom: 15px;font-size: 1.5rem;font-weight: 600;}.welcome-message p{line-height: 1.6;}.chat-messages::-webkit-scrollbar{width: 8px;}.chat-messages::-webkit-scrollbar-track{background:rgba(249, 231, 216, 0.2);border-radius: 4px;}.chat-messages::-webkit-scrollbar-thumb{background:linear-gradient(135deg, #e69c68 0%, #d98850 100%);border-radius: 4px;}.chat-messages::-webkit-scrollbar-thumb:hover{background:linear-gradient(135deg, #d98850 0%, #c87840 100%);}</style></head><body><divclass="chat-container"><divclass="chat-header"><!-- 新增:模型选择下拉框 --><selectclass="model-selector"id="modelSelector"><optionvalue="deepseek">DeepSeek</option><optionvalue="zhipu">智谱GLM</option></select><spanclass="chat-header-title">AI 聊天助手</span><buttonclass="new-chat-btn"id="newChatBtn">新对话</button></div><divclass="chat-messages"id="chatMessages"><divclass="welcome-message"id="welcomeMessage"><h3>欢迎使用AI 聊天助手</h3><p>你可以随时提出问题,我会尽力解答<br> 期待与你愉快交流!</p></div></div><divclass="typing-indicator"id="typingIndicator"><divclass="typing-dots"><divclass="typing-dot"></div><divclass="typing-dot"></div><divclass="typing-dot"></div></div><spanstyle="margin-left: 15px;color: #d98850;font-weight: 600;">正在思考中...</span></div><divclass="chat-input"><inputtype="text"class="message-input"id="messageInput"placeholder="请输入你想说的话..."autocomplete="off"><buttonclass="send-button"id="sendButton">发送</button></div></div><script>const chatMessages = document.getElementById('chatMessages');const messageInput = document.getElementById('messageInput');const sendButton = document.getElementById('sendButton');const typingIndicator = document.getElementById('typingIndicator');const welcomeMessage = document.getElementById('welcomeMessage');const newChatBtn = document.getElementById('newChatBtn');// 新增:获取模型选择器DOMconst modelSelector = document.getElementById('modelSelector');// 当前会话ID(用于记忆功能)let conversationId ='default';// 创建新会话asyncfunctioncreateNewConversation(){try{const response =awaitfetch('/api/conversation/new');const data =await response.json(); conversationId = data.conversationId;// 清空聊天消息 chatMessages.innerHTML ='';// 显示欢迎消息const newWelcome = document.createElement('div'); newWelcome.className ='welcome-message'; newWelcome.id ='welcomeMessage'; newWelcome.innerHTML =` <h3>欢迎使用AI 聊天助手</h3> <p>你可以随时提出问题,我会尽力解答<br> 期待与你愉快交流!</p> `; chatMessages.appendChild(newWelcome); console.log('新会话已创建:', conversationId);}catch(error){ console.error('创建新会话失败:', error);// 即使失败也生成一个本地ID conversationId ='local-'+ Date.now();}}// 发送消息函数(使用流式接口)asyncfunctionsendMessage(){const message = messageInput.value.trim();if(!message)return;// 隐藏欢迎消息const welcomeMsg = document.getElementById('welcomeMessage');if(welcomeMsg){ welcomeMsg.style.display ='none';}// 添加用户消息到聊天界面addMessage(message,'user');// 清空输入框 messageInput.value ='';// 显示正在输入指示器showTypingIndicator();// 禁用发送按钮 sendButton.disabled =true;// 预先创建AI消息容器(用于流式显示)const aiMessageDiv =createAIMessageContainer();try{// 新增:获取选中的模型值const selectedModel = modelSelector.value;// 修改:请求URL中添加model参数const response =awaitfetch(`/ai/generate/stream?message=${encodeURIComponent(message)}&conversationId=${encodeURIComponent(conversationId)}&model=${encodeURIComponent(selectedModel)}`,{method:'GET',headers:{'Accept':'text/event-stream',}});if(!response.ok){thrownewError(`HTTP error! status: ${response.status}`);}// 隐藏正在输入指示器(开始接收数据时隐藏)hideTypingIndicator();// 读取流式响应const reader = response.body.getReader();const decoder =newTextDecoder();let fullResponse ='';while(true){const{ done, value }=await reader.read();if(done)break;// 解码数据块const chunk = decoder.decode(value,{stream:true});// SSE 格式:每行以 "data:" 开头const lines = chunk.split('\n');for(const line of lines){if(line.startsWith('data:')){const data = line.substring(5).trim();if(data){ fullResponse += data;// 更新AI消息内容updateAIMessageContent(aiMessageDiv, fullResponse);}}elseif(line.trim()&&!line.startsWith(':')){// 处理非标准SSE格式(直接返回文本) fullResponse += line;updateAIMessageContent(aiMessageDiv, fullResponse);}}}// 如果没有收到任何内容if(!fullResponse){updateAIMessageContent(aiMessageDiv,'抱歉,没有收到回复。');}}catch(error){// 隐藏正在输入指示器hideTypingIndicator();// 显示错误消息updateAIMessageContent(aiMessageDiv,'抱歉,处理你的请求时出现了错误,请稍后再试。'); console.error('Error:', error);}finally{// 重新启用发送按钮 sendButton.disabled =false; messageInput.focus();}}// 创建AI消息容器(用于流式显示)functioncreateAIMessageContainer(){const messageDiv = document.createElement('div'); messageDiv.className ='message ai-message';const contentDiv = document.createElement('div'); contentDiv.className ='message-content'; contentDiv.textContent =''; messageDiv.appendChild(contentDiv); chatMessages.appendChild(messageDiv);// 滚动到最新消息 chatMessages.scrollTop = chatMessages.scrollHeight;return contentDiv;}// 更新AI消息内容(流式更新)functionupdateAIMessageContent(contentDiv, content){ contentDiv.textContent = content;// 滚动到最新消息 chatMessages.scrollTop = chatMessages.scrollHeight;}// 添加消息到聊天界面(保留用于用户消息)functionaddMessage(content, sender){const messageDiv = document.createElement('div'); messageDiv.className =`message ${sender}-message`;const contentDiv = document.createElement('div'); contentDiv.className ='message-content'; contentDiv.textContent = content; messageDiv.appendChild(contentDiv); chatMessages.appendChild(messageDiv);// 滚动到最新消息 chatMessages.scrollTop = chatMessages.scrollHeight;}// 显示正在输入指示器functionshowTypingIndicator(){ typingIndicator.classList.add('show'); chatMessages.scrollTop = chatMessages.scrollHeight;}// 隐藏输入指示器functionhideTypingIndicator(){ typingIndicator.classList.remove('show');}// 事件监听器 sendButton.addEventListener('click', sendMessage); messageInput.addEventListener('keypress',(e)=>{if(e.key ==='Enter'&&!e.shiftKey){ e.preventDefault();sendMessage();}});// 新对话按钮 newChatBtn.addEventListener('click', createNewConversation);// 页面加载完成后聚焦输入框 document.addEventListener('DOMContentLoaded',()=>{ messageInput.focus();});</script></body></html>

Read more

零基础快速入门前端DOM 操作核心知识与实战解析(完整汇总版)(可用于备赛蓝桥杯Web应用开发)

零基础快速入门前端DOM 操作核心知识与实战解析(完整汇总版)(可用于备赛蓝桥杯Web应用开发)

DOM(Document Object Model,文档对象模型)是 JavaScript 操作 HTML 文档的桥梁,它将网页转换为一棵 “树”,每个 HTML 标签、属性、文本都是树上的节点。掌握 DOM 操作,就能动态改变网页内容、样式和交互。本文结合实战代码,从基础到进阶系统梳理 DOM 核心知识。 一、DOM 元素获取:找到要操作的 “节点” 操作 DOM 的第一步是 “找到元素”,常用方法如下: 方法 描述 示例 querySelector() 通过 CSS 选择器获取单个元素 document.querySelector(".div1") getElementById() 通过

在 Cursor 中打造你的专属前端“AI 助手”:Agent Skills 实战指南 什么是 Agent Skills?

在 Cursor 中打造你的专属前端“AI 助手”:Agent Skills 实战指南 什么是 Agent Skills?

文章目录 * 一、什么是 Agent Skills? * 二、使用步骤 * 1.下载官方提供的agent-skills文档 * 2.cursor中使用 * 三、如何设计自己的skills * 四、实战:打造一个“生成标准 React 组件”的 Skill * 第一步:创建目录 * 第二步:编写 SKILL.md * 总结:为什么你应该开始用 Skills? 一、什么是 Agent Skills? 简单来说,Agent Skills 是一种标准化的方式,用来封装特定任务的知识和工作流。 如果说 MCP (Model Context Protocol) 是给 AI 装上了“手”(让它能连接数据库、Github)

Spring Web MVC从入门到实战

Spring Web MVC从入门到实战

—JavaEE专栏— 1. Spring Web MVC核心概念 1.1 什么是Spring Web MVC Spring Web MVC是基于Servlet API构建的原始Web框架,从一开始就包含在Spring框架中,其正式名称来源于源模块名称(spring-webmvc),通常简称为Spring MVC。 官方定义:Spring Web MVC is the original web framework built on the Servlet API and has been included in the Spring Framework from the very beginning. Servlet是Java Web开发的规范,定义了动态页面开发的技术标准,而Tomcat、Weblogic等Servlet容器则是该规范的具体实现,

Spring 核心技术解析【纯干货版】- XV:Spring 网络模块 Spring-Web 模块精讲

Spring 核心技术解析【纯干货版】- XV:Spring 网络模块 Spring-Web 模块精讲

Spring Framework 作为 Java 生态中最流行的企业级开发框架,提供了丰富的模块化支持。其中,Spring Web 模块是支撑 Web 开发的基础组件,无论是传统的 MVC 应用,还是 REST API 及微服务架构,都离不开它的核心能力。 本篇文章将深入解析 Spring Web 模块的核心概念、依赖关系、作用及关键组件,并通过实际案例展示如何使用 Spring Web 进行 RESTful API 调用。本文力求内容精炼、干货满满,帮助你掌握 Spring Web 的核心技术点。 文章目录 * 1、Spring-Web 模块介绍 * 1.1、Spring-Web 模块概述 * 1.2、Spring-Web