Web开发全栈AI功能集成:从前端Vue到后端SpringBoot调用SmallThinker-3B

张开发
2026/4/20 10:43:33 15 分钟阅读

分享文章

Web开发全栈AI功能集成:从前端Vue到后端SpringBoot调用SmallThinker-3B
Web开发全栈AI功能集成从前端Vue到后端SpringBoot调用SmallThinker-3B最近在做一个内部知识库问答系统需要集成一个轻量级的本地大模型来处理一些简单的文本生成和问答任务。SmallThinker-3B-Preview这个模型进入了我的视野它体积小、推理速度快非常适合集成到Web应用中。但怎么把它从一个独立的模型变成一个用户能通过浏览器直接使用的功能呢这需要一套完整的前后端协作方案。今天我就以一个“智能对话助手”项目为例带你走一遍从零开始将AI模型能力融入现代Web应用的全过程。我们会用Vue3搭建一个清爽的交互界面用SpringBoot构建稳健的后端API用MySQL来记住每一次对话最终实现一个支持用户登录、多轮对话、流式响应和记录查询的完整应用。如果你也在考虑为你的Web项目添加AI能力这篇文章或许能给你一些直接的参考。1. 项目蓝图与核心思路在动手写代码之前我们先理清整个项目的架构和核心流程。这就像盖房子先画图纸能避免后面走很多弯路。我们的目标是构建一个B/S架构的应用。用户在浏览器里使用Vue3开发的单页面应用所有与AI模型交互的请求都发送到后端的SpringBoot服务。SpringBoot服务扮演着“中间人”和“调度者”的角色它一方面处理用户认证、会话管理这些业务逻辑另一方面负责调用SmallThinker-3B模型来生成回复。为了不让用户干等回复会以“流”的形式一个字一个字地推送到前端形成类似ChatGPT的打字机效果。所有的对话记录都会被妥善地保存在MySQL数据库里方便用户随时回顾。整个系统的核心工作流程可以概括为以下几步用户在前端界面输入问题点击发送。Vue应用将问题、当前会话ID等信息通过HTTP请求发送给SpringBoot后端。SpringBoot后端验证用户身份和请求有效性。后端调用SmallThinker-3B模型的推理接口并开始接收模型生成的文本流。后端同时将用户的问题存入数据库。后端将接收到的文本流实时转发给前端并逐步将生成的回复也存入数据库。前端动态渲染接收到的文本流实现“打字机”效果。一次交互完成对话记录被持久化。这个流程中流式响应和状态管理是两个关键点。流式响应决定了用户体验是否流畅而前后端的状态同步比如当前回复是否生成完毕则是保证功能正确的基石。2. 后端基石SpringBoot服务搭建与模型集成后端是整个系统的大脑它要处理业务、连接模型、操作数据。我们首先从这里开始。2.1 项目初始化与核心依赖我使用Spring Initializr快速创建了一个项目引入以下核心依赖!-- Spring Boot Web -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- 数据库相关 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency !-- 用于处理JSON -- dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId /dependency !-- 用于HTTP客户端调用模型API -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-webflux/artifactId /dependencyWebFlux的引入是为了使用响应式的WebClient它能很好地处理服务器发送事件SSE这种流式响应是我们实现流式返回的关键。2.2 数据模型设计根据需求我们至少需要用户和对话消息两张表。// User.java 用户实体 Entity Table(name users) Data NoArgsConstructor AllArgsConstructor public class User { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; private String username; private String password; // 实际存储应为加密后的哈希值 private String email; private LocalDateTime createdAt; } // ChatMessage.java 对话消息实体 Entity Table(name chat_messages) Data NoArgsConstructor AllArgsConstructor public class ChatMessage { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; private Long sessionId; // 会话ID用于区分不同对话线程 private String role; // user 或 assistant Column(columnDefinition TEXT) private String content; // 消息内容 private LocalDateTime timestamp; ManyToOne JoinColumn(name user_id) private User user; // 关联用户 }2.3 核心服务集成SmallThinker-3B假设SmallThinker-3B模型已经通过类似Ollama、vLLM等框架部署好并提供了一个HTTP API端点例如http://localhost:11434/api/generate。我们的服务需要去调用它。首先创建一个配置类来定义WebClientConfiguration public class WebClientConfig { Value(${ai.model.base-url}) private String modelBaseUrl; Bean public WebClient modelApiWebClient() { return WebClient.builder() .baseUrl(modelBaseUrl) .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .build(); } }然后创建AI服务类来处理与模型的交互。这里的关键是处理流式响应。Service Slf4j public class AIService { private final WebClient webClient; public AIService(WebClient modelApiWebClient) { this.webClient modelApiWebClient; } public FluxString streamChatCompletion(ChatRequest request) { // 构建请求体具体格式需参照SmallThinker-3B的API文档 MapString, Object requestBody new HashMap(); requestBody.put(model, smallthinker-3b-preview); requestBody.put(prompt, request.getPrompt()); requestBody.put(stream, true); // 关键开启流式输出 requestBody.put(max_tokens, 512); return this.webClient.post() .uri(/api/generate) .bodyValue(requestBody) .accept(MediaType.TEXT_EVENT_STREAM) // 接受服务器发送事件 .retrieve() .bodyToFlux(String.class) // 以Flux流的形式接收响应 .doOnNext(chunk - log.debug(收到模型响应块: {}, chunk)) .map(this::extractContentFromChunk) // 解析每个chunk中的文本内容 .filter(content - content ! null !content.isEmpty()); } // 解析模型返回的JSON chunk提取“response”字段 private String extractContentFromChunk(String jsonChunk) { try { ObjectMapper mapper new ObjectMapper(); JsonNode node mapper.readTree(jsonChunk); return node.path(response).asText(); } catch (Exception e) { log.warn(解析响应块失败: {}, jsonChunk, e); return ; } } }ChatRequest是一个简单的请求封装类。FluxString是Spring Reactor中的类型代表一个异步的字符串数据流。这样当模型生成一个词我们就能立刻收到一个包含该词的Flux元素并可以立即转发给前端。2.4 控制器提供RESTful API控制器负责接收前端请求协调AI服务和数据服务并返回响应。RestController RequestMapping(/api/chat) RequiredArgsConstructor public class ChatController { private final AIService aiService; private final ChatMessageService messageService; private final UserService userService; // 假设有用户服务 PostMapping(/stream) public FluxServerSentEventString streamChat(RequestBody StreamChatRequest request, RequestHeader(Authorization) String authHeader) { // 1. 用户认证简化示例 User currentUser userService.authenticate(authHeader); // 2. 保存用户消息 ChatMessage userMessage messageService.saveMessage(currentUser, request.getSessionId(), user, request.getMessage()); // 3. 构建给模型的Prompt可以包含历史记录 String prompt buildPromptWithHistory(request.getMessage(), request.getSessionId(), currentUser); ChatRequest aiRequest new ChatRequest(prompt); // 4. 调用AI服务获取流式响应并转换为SSE格式 return aiService.streamChatCompletion(aiRequest) .map(chunk - { // 可以在这里累积或处理每个chunk return ServerSentEvent.Stringbuilder() .data(chunk) .build(); }) .concatWithValues( // 流结束时发送一个特殊事件并保存完整的助手消息 ServerSentEvent.Stringbuilder() .event(end) .data() .build() ) .doOnComplete(() - { // 当流完成时保存完整的助手回复到数据库 // 注意这里需要将之前收到的所有chunk拼接起来 // 实际项目中需要在过程中累积内容 }); } GetMapping(/history) public ResponseEntityListChatMessage getChatHistory(RequestParam Long sessionId, RequestHeader(Authorization) String authHeader) { User currentUser userService.authenticate(authHeader); ListChatMessage history messageService.getMessagesBySessionAndUser(sessionId, currentUser); return ResponseEntity.ok(history); } }这里我们创建了一个/api/chat/stream的POST端点它返回FluxServerSentEventString这正是SSEServer-Sent Events协议所需的格式。前端可以通过EventSource API轻松订阅这个流。3. 前端界面Vue3构建动态交互体验后端准备好了接下来我们构建一个用户看得见、摸得着的前端界面。3.1 项目初始化与状态管理使用Vite创建一个新的Vue3项目并安装必要的依赖npm create vuelatest ai-chat-frontend cd ai-chat-frontend npm install axios pinia # axios用于HTTP请求Pinia用于状态管理我们使用Pinia来管理全局状态比如用户登录信息、当前会话、消息列表等。// stores/chat.js import { defineStore } from pinia import { ref } from vue import axios from axios export const useChatStore defineStore(chat, () { const messages ref([]) // 当前会话的消息列表 const currentSessionId ref(Date.now()) // 简单用时间戳生成会话ID const isLoading ref(false) const apiBaseUrl import.meta.env.VITE_API_BASE_URL || http://localhost:8080/api // 发送消息并处理流式响应 async function sendMessage(content) { const userMessage { role: user, content, timestamp: new Date() } messages.value.push(userMessage) const assistantMessage { role: assistant, content: , timestamp: null } messages.value.push(assistantMessage) const assistantIndex messages.value.length - 1 isLoading.value true try { const eventSource new EventSourcePolyfill(${apiBaseUrl}/chat/stream, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${localStorage.getItem(token)} // 假设使用JWT }, body: JSON.stringify({ message: content, sessionId: currentSessionId.value }) }) eventSource.onmessage (event) { if (event.data) { // 累积流式返回的内容 messages.value[assistantIndex].content event.data } } eventSource.addEventListener(end, () { eventSource.close() messages.value[assistantIndex].timestamp new Date() isLoading.value false }) eventSource.onerror (error) { console.error(EventSource failed:, error) eventSource.close() messages.value[assistantIndex].content \n\n响应生成中断 isLoading.value false } } catch (error) { console.error(发送请求失败:, error) messages.value[assistantIndex].content 抱歉请求发送失败。 isLoading.value false } } // 获取历史记录 async function fetchHistory(sessionId) { try { const response await axios.get(${apiBaseUrl}/chat/history, { params: { sessionId }, headers: { Authorization: Bearer ${localStorage.getItem(token)} } }) messages.value response.data } catch (error) { console.error(获取历史记录失败:, error) } } return { messages, currentSessionId, isLoading, sendMessage, fetchHistory } })注意浏览器原生的EventSource不支持POST请求和自定义Header所以我们使用了event-source-polyfill库你需要先安装它npm install event-source-polyfill。3.2 构建聊天主界面现在我们来创建核心的聊天组件。!-- components/ChatWindow.vue -- template div classchat-container div classmessages-area div v-for(msg, index) in chatStore.messages :keyindex :class[message-bubble, msg.role] div classavatar{{ msg.role user ? 你 : AI }}/div div classcontent !-- 使用v-html或自定义组件渲染Markdown注意安全 -- div v-ifmsg.role assistant index chatStore.messages.length - 1 chatStore.isLoading {{ msg.content }}span classcursor▌/span /div div v-else {{ msg.content }} /div /div /div /div div classinput-area textarea v-modelinputText keydown.enter.exact.preventhandleSend placeholder输入你的问题... :disabledchatStore.isLoading / button clickhandleSend :disabled!inputText.trim() || chatStore.isLoading {{ chatStore.isLoading ? 思考中... : 发送 }} /button /div /div /template script setup import { ref } from vue import { useChatStore } from /stores/chat const chatStore useChatStore() const inputText ref() function handleSend() { if (!inputText.value.trim()) return const textToSend inputText.value.trim() inputText.value chatStore.sendMessage(textToSend) } /script style scoped .chat-container { display: flex; flex-direction: column; height: 600px; border: 1px solid #e0e0e0; border-radius: 8px; } .messages-area { flex: 1; overflow-y: auto; padding: 16px; } .message-bubble { display: flex; margin-bottom: 16px; } .message-bubble.user { flex-direction: row-reverse; } .avatar { width: 32px; height: 32px; border-radius: 50%; background-color: #007bff; color: white; display: flex; align-items: center; justify-content: center; margin: 0 8px; flex-shrink: 0; } .message-bubble.assistant .avatar { background-color: #28a745; } .content { max-width: 70%; padding: 10px 14px; border-radius: 18px; background-color: #f1f3f4; } .message-bubble.user .content { background-color: #007bff; color: white; } .input-area { display: flex; border-top: 1px solid #e0e0e0; padding: 12px; } textarea { flex: 1; border: 1px solid #ccc; border-radius: 4px; padding: 10px; resize: none; font-family: inherit; } button { margin-left: 12px; padding: 10px 20px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; } button:disabled { background-color: #ccc; cursor: not-allowed; } .cursor { animation: blink 1s infinite; } keyframes blink { 50% { opacity: 0; } } /style这个组件实现了基本的聊天界面显示消息列表、一个文本输入框和一个发送按钮。最关键的部分是当收到AI的流式响应时它会动态更新最后一条助手消息的内容并显示一个闪烁的光标营造出“正在输入”的效果。3.3 集成与运行最后在App.vue中集成这个聊天组件并配置好路由如果需要的话和全局状态。!-- App.vue -- template div idapp header h1智能对话助手 (SmallThinker-3B)/h1 p基于Vue3 SpringBoot MySQL的全栈AI集成示例/p /header main !-- 这里可以添加登录组件 -- ChatWindow / /main /div /template script setup import ChatWindow from ./components/ChatWindow.vue /script现在分别启动你的SpringBoot后端和Vue前端开发服务器打开浏览器你应该就能看到一个可以交互的聊天界面了。输入问题就能看到来自SmallThinker-3B模型的流式回复。4. 关键问题与优化实践把基础功能跑通只是第一步。在实际开发中我遇到了几个典型问题这里分享下我的解决思路。1. 流式响应中断或延迟大这可能是模型服务本身推理速度慢或者网络问题。前端需要增加健壮性处理比如设置超时、提供重试按钮。在后端可以考虑将模型调用异步化先立即返回一个“已接收”的响应再通过WebSocket或另一个SSE连接推送结果避免HTTP连接长时间占用。2. 对话上下文管理我们的简单示例只是将当前问题发给模型。要让模型有“记忆”需要将历史对话也作为Prompt的一部分。可以在后端buildPromptWithHistory方法中从数据库查询出最近的N条历史记录拼接成一个有格式的上下文例如“用户: 之前的问题\n助手: 之前的回答\n用户: 当前问题”再发给模型。注意上下文长度不能超过模型的最大限制。3. 用户认证与数据隔离示例中使用了简单的Bearer Token。在生产环境务必使用成熟的方案如JWT或OAuth2。确保每个/history查询和/stream请求都严格校验用户身份只返回该用户自己的数据防止数据越权访问。4. 前端体验优化自动滚动当新消息到来或内容更新时自动将消息区域滚动到底部。Markdown渲染AI回复常包含Markdown格式。可以集成如marked和highlight.js库来美化渲染。停止生成按钮在流式响应过程中提供一个按钮让用户可以主动中断生成。本地存储可以将当前会话的临时消息保存在localStorage防止页面意外刷新导致输入内容丢失。5. 部署考量跨域问题在SpringBoot后端通过CrossOrigin注解或配置WebMvcConfigurer解决。模型服务部署SmallThinker-3B可以与其他服务一起部署也可以独立部署。独立部署时注意后端服务与模型服务之间的网络连通性和延迟。数据库连接池生产环境务必配置合适的数据库连接池如HikariCP。5. 总结走完这一整套流程你会发现将一个AI模型集成到Web应用中不仅仅是调用一个API那么简单。它涉及到前后端架构设计、异步通信机制流式响应、状态同步、数据持久化和用户体验打磨等多个环节。我们这个项目采用的技术栈Vue3 SpringBoot MySQL是一个经典且稳健的组合清晰地分离了关注点。SpringBoot负责复杂的业务逻辑和模型集成Vue3负责构建动态、响应式的用户界面MySQL则可靠地存储所有数据。流式响应SSE的引入极大地提升了交互的实时感和友好度。当然这只是一个起点。在此基础上你可以继续扩展很多功能比如支持文件上传让模型处理图片、实现多模态交互、增加对更多模型的支持、加入管理后台查看使用统计等等。希望这个完整的案例能为你自己的全栈AI应用开发提供一个扎实的起点。动手试试把想法变成现实吧。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。

更多文章