大家好我是Java1234_小锋老师分享一套锋哥原创的基于LangChain4j的全模态聊天机器人系统(SpringBoot4Vue3)项目介绍随着人工智能技术的快速发展大语言模型LLM已经从单一的文本处理能力演进到能够同时理解文本、图片、音频、视频等多种模态信息的全模态阶段。如何在 Java 技术栈中高效集成并落地全模态大模型能力构建一个交互友好、响应迅速的智能聊天机器人系统成为当前企业级应用开发的重要课题。本文设计并实现了一个基于 LangChain4j 的全模态聊天机器人系统。系统后端采用 SpringBoot 4.0.7 框架搭建结合 MyBatis-Plus 持久层框架与 MySQL 8 数据库进行数据管理通过 LangChain4j 1.15.1 框架以 OpenAI 兼容模式接入阿里云 DashScope 平台的 Qwen3.5-Omni-Plus 全模态大模型并基于 SSEServer-Sent Events实现了文本回复的流式打字机效果。前端采用 Vue3 Vite Element Plus ECharts 技术栈构建单页应用实现了用户注册登录、多模态聊天、会话管理以及管理员后台数据统计大屏等功能。系统使用 JWT 实现无状态鉴权使用 MD5 对用户密码进行加密存储并对普通用户和管理员进行角色权限控制。经过测试系统能够正确理解并响应文本、图片、音频、视频四种模态的输入流式回复体验流畅各项功能运行稳定达到了预期的设计目标。本系统验证了 LangChain4j 在 Java 生态中构建全模态 AI 应用的可行性为同类智能对话系统的开发提供了有价值的参考。源码下载链接https://pan.baidu.com/s/1lGWHVvO1DfohIoUBDj-4XA?pwd1234提取码1234系统展示核心代码package com.java1234.omnichat.controller; import com.java1234.omnichat.common.BusinessException; import com.java1234.omnichat.common.CurrentUser; import com.java1234.omnichat.common.RequireLogin; import com.java1234.omnichat.common.Result; import com.java1234.omnichat.entity.User; import com.java1234.omnichat.service.ChatService; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; /** * 全模态聊天控制器 - SSE流式接口 * * author Java1234 */ RestController RequestMapping(/chat) RequireLogin RequiredArgsConstructor public class ChatController { private final ChatService chatService; /** * 流式全模态聊天接口(SSE) * 支持文本、图片、音频、视频输入理解 * * param conversationId 会话ID(新建时可为空) * param content 文本内容 * param contentType 内容类型: text/image/audio/video * param fileUrl 附件URL * param currentUser 当前用户 * return SSE发射器 */ GetMapping(/stream) public SseEmitter streamChat( RequestParam(required false) Long conversationId, RequestParam(required false) String content, RequestParam(defaultValue text) String contentType, RequestParam(required false) String fileUrl, CurrentUser User currentUser) { if ((content null || content.isBlank()) (fileUrl null || fileUrl.isBlank())) { throw new BusinessException(消息内容不能为空); } SseEmitter emitter new SseEmitter(120000L); chatService.streamChat(currentUser.getId(), conversationId, content, contentType, fileUrl, emitter); return emitter; } }template div classchat-page !-- 左侧会话列表 -- aside classsidebar div classsidebar-header div classbrand el-icon :size24ChatDotRound //el-icon spanJava1234/span /div el-button typeprimary classgradient-btn sizesmall clickhandleNewChat el-iconPlus //el-icon 新对话 /el-button /div div classconversation-list div v-forconv in conversations :keyconv.id classconv-item :class{ active: currentConvId conv.id } clickselectConversation(conv) el-iconChatLineSquare //el-icon span classconv-title{{ conv.title }}/span el-icon classconv-delete click.stophandleDeleteConv(conv.id)Delete //el-icon /div /div div classsidebar-footer span{{ userStore.nickname || userStore.username }}/span el-button text typedanger clickhandleLogout退出/el-button /div /aside !-- 右侧聊天区域 -- main classchat-main div v-if!currentConvId !isNewChat classwelcome div classwelcome-icon el-icon :size60ChatDotRound //el-icon /div h2Java1234 全模态智能助手/h2 p支持文本、图片、音频、视频多模态理解与交互/p p classhint点击「新对话」开始聊天或直接输入消息/p /div div v-else classchat-area !-- 消息列表 -- div refmessageListRef classmessage-list div v-formsg in messages :keymsg.id classmessage-item :classmsg.role div classavatar el-avatar :size36 :style{ background: msg.role user ? #6366f1 : #10b981 } {{ msg.role user ? 我 : AI }} /el-avatar /div div classbubble div v-ifmsg.contentType text || msg.role assistant classtext-content {{ msg.content }} /div div v-ifmsg.contentType image msg.fileUrl classmedia-content p v-ifmsg.content{{ msg.content }}/p el-image :srcresolveMediaUrl(msg.fileUrl) fitcontain stylemax-width:300px;max-height:200px;border-radius:8px :preview-src-list[resolveMediaUrl(msg.fileUrl)] / /div div v-ifmsg.contentType audio msg.fileUrl classmedia-content p v-ifmsg.content{{ msg.content }}/p audio :srcresolveMediaUrl(msg.fileUrl) controls stylewidth:100%;max-width:300px/audio /div div v-ifmsg.contentType video msg.fileUrl classmedia-content p v-ifmsg.content{{ msg.content }}/p video :srcresolveMediaUrl(msg.fileUrl) controls stylemax-width:400px;max-height:250px;border-radius:8px/video /div div classmsg-time{{ formatDateTime(msg.createTime) }}/div /div /div !-- 流式输出中的消息 -- div v-ifstreaming classmessage-item assistant div classavatar el-avatar :size36 stylebackground:#10b981AI/el-avatar /div div classbubble div classtext-content{{ streamContent }}span classcursor-blink|/span/div /div /div /div !-- 输入区域 -- div classinput-area div v-ifpendingFile classpending-file el-tag closable closependingFile null {{ pendingFile.name }} ({{ pendingFile.contentType }}) /el-tag /div div classinput-row el-upload :show-file-listfalse :before-uploadhandleFileSelect acceptimage/*,audio/*,video/* el-button circleel-iconPaperclip //el-icon/el-button /el-upload el-input v-modelinputText placeholder输入消息支持文本/图片/音频/视频... sizelarge keyup.enterhandleSend :disabledstreaming / el-button typeprimary classgradient-btn sizelarge :loadingstreaming clickhandleSend :disabled!canSend el-iconPromotion //el-icon /el-button /div /div /div /main /div /template script setup import { ref, computed, nextTick } from vue import { useRouter } from vue-router import { ElMessage, ElMessageBox } from element-plus import { useUserStore } from /store/user import { formatDateTime, resolveMediaUrl } from /utils/format import { getConversationList, createConversation, deleteConversation, getMessageList, uploadFile } from /api/chat const router useRouter() const userStore useUserStore() const conversations ref([]) const currentConvId ref(null) const isNewChat ref(false) const messages ref([]) const inputText ref() const streaming ref(false) const streamContent ref() const pendingFile ref(null) const messageListRef ref() const canSend computed(() { return (inputText.value.trim() || pendingFile.value) !streaming.value }) /** 加载会话列表 */ async function loadConversations() { const res await getConversationList() conversations.value res.data || [] } /** 新建对话 */ function handleNewChat() { currentConvId.value null isNewChat.value true messages.value [] inputText.value pendingFile.value null } /** 选择会话 */ async function selectConversation(conv) { currentConvId.value conv.id isNewChat.value false const res await getMessageList(conv.id) messages.value res.data || [] scrollToBottom() } /** 删除会话 */ async function handleDeleteConv(id) { await ElMessageBox.confirm(确定删除该会话, 提示, { type: warning }) await deleteConversation(id) if (currentConvId.value id) { currentConvId.value null messages.value [] } loadConversations() } /** 文件选择 */ function handleFileSelect(file) { const isImage file.type.startsWith(image/) const isAudio file.type.startsWith(audio/) const isVideo file.type.startsWith(video/) if (!isImage !isAudio !isVideo) { ElMessage.warning(仅支持图片、音频、视频文件) return false } if (file.size 50 * 1024 * 1024) { ElMessage.warning(文件大小不能超过50MB) return false } pendingFile.value { file, name: file.name, contentType: isImage ? image : isAudio ? audio : video } return false } /** 发送消息(SSE流式) */ async function handleSend() { if (!canSend.value) return let fileUrl null let contentType text if (pendingFile.value) { const uploadRes await uploadFile(pendingFile.value.file) fileUrl uploadRes.data.url contentType uploadRes.data.contentType } const content inputText.value.trim() || (pendingFile.value ? 请分析这个${contentType image ? 图片 : contentType audio ? 音频 : 视频} : ) const convId currentConvId.value // 添加用户消息到界面 messages.value.push({ id: Date.now(), role: user, contentType, content, fileUrl, createTime: new Date().toISOString() }) inputText.value pendingFile.value null streaming.value true streamContent.value scrollToBottom() // 构建SSE请求参数 const params new URLSearchParams() if (convId) params.append(conversationId, convId) if (content) params.append(content, content) params.append(contentType, contentType) if (fileUrl) params.append(fileUrl, fileUrl) // 使用 fetch ReadableStream 接收 SSE 流式响应 try { const response await fetch(/api/chat/stream?${params.toString()}, { headers: { Authorization: Bearer ${userStore.token} } }) if (!response.ok) { throw new Error(请求失败) } const reader response.body.getReader() const decoder new TextDecoder() let buffer let currentEvent message while (true) { const { done, value } await reader.read() if (done) break buffer decoder.decode(value, { stream: true }) const parts buffer.split(\n) buffer parts.pop() || for (const line of parts) { if (line.startsWith(event:)) { currentEvent line.substring(6).trim() } else if (line.startsWith(data:)) { const data line.substring(5).trim() if (currentEvent conversation) { currentConvId.value Number(data) isNewChat.value false } else if (currentEvent message) { streamContent.value data scrollToBottom() } else if (currentEvent done) { streaming.value false messages.value.push({ id: Date.now() 1, role: assistant, contentType: text, content: streamContent.value, createTime: new Date().toISOString() }) streamContent.value loadConversations() scrollToBottom() } else if (currentEvent error) { ElMessage.error(data) streaming.value false } } else if (line ) { currentEvent message } } } if (streaming.value) { streaming.value false if (streamContent.value) { messages.value.push({ id: Date.now() 1, role: assistant, contentType: text, content: streamContent.value, createTime: new Date().toISOString() }) streamContent.value loadConversations() scrollToBottom() } } } catch (err) { ElMessage.error(聊天请求失败) streaming.value false } } /** 滚动到底部 */ function scrollToBottom() { nextTick(() { if (messageListRef.value) { messageListRef.value.scrollTop messageListRef.value.scrollHeight } }) } /** 退出登录 */ function handleLogout() { userStore.logout() router.push(/login) } loadConversations() /script style scoped .chat-page { display: flex; height: 100vh; overflow: hidden; background: #f5f7fa; } .sidebar { width: 280px; min-height: 0; background: #fff; border-right: 1px solid #e8e8e8; display: flex; flex-direction: column; overflow: hidden; } .sidebar-header { flex-shrink: 0; padding: 20px; border-bottom: 1px solid #f0f0f0; } .brand { display: flex; align-items: center; gap: 8px; font-size: 18px; font-weight: 600; color: var(--primary); margin-bottom: 15px; } .conversation-list { flex: 1; min-height: 0; overflow-y: auto; padding: 10px; } .conv-item { display: flex; align-items: center; gap: 8px; padding: 12px 15px; border-radius: 8px; cursor: pointer; transition: all 0.2s; margin-bottom: 4px; } .conv-item:hover { background: #f5f5f5; } .conv-item.active { background: #eef2ff; color: var(--primary); } .conv-title { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 14px; } .conv-delete { opacity: 0; color: #f56c6c; transition: opacity 0.2s; } .conv-item:hover .conv-delete { opacity: 1; } .sidebar-footer { flex-shrink: 0; padding: 15px 20px; border-top: 1px solid #f0f0f0; display: flex; justify-content: space-between; align-items: center; font-size: 14px; color: #666; } .chat-main { flex: 1; min-height: 0; display: flex; flex-direction: column; overflow: hidden; } .welcome { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; color: #666; } .welcome-icon { width: 100px; height: 100px; background: var(--gradient); border-radius: 30px; display: flex; align-items: center; justify-content: center; color: #fff; margin-bottom: 20px; } .welcome h2 { font-size: 24px; color: #333; margin-bottom: 10px; } .welcome .hint { color: #aaa; margin-top: 10px; font-size: 13px; } .chat-area { flex: 1; min-height: 0; display: flex; flex-direction: column; overflow: hidden; } .message-list { flex: 1; min-height: 0; overflow-y: auto; padding: 20px 30px; } .message-item { display: flex; gap: 12px; margin-bottom: 20px; } .message-item.user { flex-direction: row-reverse; } .bubble { max-width: 70%; padding: 12px 16px; border-radius: 12px; background: #fff; box-shadow: 0 1px 4px rgba(0,0,0,0.06); line-height: 1.6; } .message-item.user .bubble { background: #eef2ff; } .text-content { white-space: pre-wrap; word-break: break-word; } .msg-time { font-size: 11px; color: #bbb; margin-top: 6px; } .cursor-blink { animation: blink 1s infinite; color: var(--primary); } keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } } .input-area { flex-shrink: 0; padding: 15px 30px 20px; border-top: 1px solid #eee; background: #fff; } .pending-file { margin-bottom: 8px; } .input-row { display: flex; gap: 10px; align-items: center; } .input-row .el-input { flex: 1; } /style