Spring Boot 2.7+JDK8+WebSocket对接阿里云百炼Qwen3.5-Plus 实现流式对话+思考过程实时展示
前言
在大模型应用开发中,流式对话 和思考过程展示 是提升用户体验的核心功能。本文将基于 Spring Boot 2.7 + JDK8 + WebSocket 技术栈,对接阿里云百炼Qwen3.5-Plus 大模型,实现:
✅ 前后端WebSocket全双工实时通信
✅ 大模型流式输出回答(打字机效果)
✅ 思考过程实时打印 到前端页面
✅ 历史对话上下文记忆
✅ 前端美观UI+Markdown格式化+自动重连+心跳保活
整套代码开箱即用,适合新手学习和生产环境二次开发!
一、环境准备
1. 基础环境
- JDK 1.8(强制要求,阿里云SDK兼容JDK8+)
- Spring Boot 2.7.x(推荐2.7.15)
- Maven 3.6+
- 浏览器(Chrome/Firefox)
2. 阿里云百炼准备
- 登录阿里云百炼控制台
- 生成API-Key:右上角头像 → API-Key管理 → 创建密钥
- 开通
qwen3.5-plus模型权限(免费额度可直接测试)
二、项目核心依赖(pom.xml)
引入Spring WebSocket、阿里云百炼SDK、工具类等核心依赖:
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="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>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.15</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>ali-qwen-websocket</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>ali-qwen-websocket</name>
<properties>
<java.version>1.8</java.version>
<hutool.version>5.8.23</hutool.version>
<dashscope.version>2.22.12</dashscope.version>
</properties>
<dependencies>
<!-- Spring Boot WebSocket 核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- Spring Boot Web核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Thymeleaf 模板引擎 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- 阿里云百炼 SDK -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dashscope-sdk-java</artifactId>
<version>${dashscope.version}</version>
</dependency>
<!-- JSON 处理 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.43</version>
</dependency>
<!-- Hutool 工具库 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<!-- Lombok 简化代码 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
三、配置文件(application.yml)
配置阿里云百炼的API-Key、模型、接口地址:
yaml
server:
port: 8080
spring:
thymeleaf:
cache: false # 开发环境关闭缓存
# 阿里云百炼配置
aliyun:
dashscope:
api-key: 你的阿里云百炼API-Key # 推荐从环境变量读取
model: qwen3.5-plus # 模型名称
base-url: https://dashscope.aliyuncs.com # 固定地址
四、核心代码实现
1. WebSocket消息实体(ChatMessage.java)
统一前后端消息格式,区分思考过程、正式内容、开始/结束/错误等类型:
java
package com.example.demo.request;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* WebSocket 消息对象
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ChatMessage {
// 消息类型:system/user/assistant/thinking/content/start/finish/error/pong
private String type;
// 角色:system/user/assistant
private String role;
// 消息内容
private String content;
// 思考过程内容
private String thinking;
// 历史对话记录
private List<ChatMessage> history;
// 时间戳
private Long timestamp;
// 快捷构造方法
public static ChatMessage system(String content) {
return ChatMessage.builder().type("system").role("system").content(content).timestamp(System.currentTimeMillis()).build();
}
public static ChatMessage user(String content) {
return ChatMessage.builder().type("user").role("user").content(content).timestamp(System.currentTimeMillis()).build();
}
public static ChatMessage assistant(String content) {
return ChatMessage.builder().type("assistant").role("assistant").content(content).timestamp(System.currentTimeMillis()).build();
}
public static ChatMessage thinking(String thinking) {
return ChatMessage.builder().type("thinking").thinking(thinking).timestamp(System.currentTimeMillis()).build();
}
public static ChatMessage content(String content) {
return ChatMessage.builder().type("content").content(content).timestamp(System.currentTimeMillis()).build();
}
public static ChatMessage start() {
return ChatMessage.builder().type("start").timestamp(System.currentTimeMillis()).build();
}
public static ChatMessage finish() {
return ChatMessage.builder().type("finish").timestamp(System.currentTimeMillis()).build();
}
public static ChatMessage error(String message) {
return ChatMessage.builder().type("error").content(message).timestamp(System.currentTimeMillis()).build();
}
public static ChatMessage pong() {
return ChatMessage.builder().type("pong").timestamp(System.currentTimeMillis()).build();
}
}
2. 阿里云百炼核心服务(AliQwenService.java)
核心亮点:开启思考过程、流式输出、上下文记忆
java
package com.example.demo.service;
import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversation;
import com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversationParam;
import com.alibaba.dashscope.common.MultiModalMessage;
import com.alibaba.dashscope.common.Role;
import com.alibaba.dashscope.exception.ApiException;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.example.demo.request.ChatMessage;
import io.reactivex.Flowable;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
/**
* 阿里云百炼 Qwen3.5-Plus 服务
*/
@Slf4j
@Service
public class AliQwenService {
@Value("${aliyun.dashscope.api-key}")
private String apiKey;
@Value("${aliyun.dashscope.model:qwen3.5-plus}")
private String model;
private MultiModalConversation multiModalConversation;
@PostConstruct
public void init() {
this.multiModalConversation = new MultiModalConversation();
log.info("阿里云百炼服务初始化完成,模型: {}", model);
}
/**
* 流式对话 + 思考过程
*/
public void streamChat(String userContent,
List<ChatMessage> history,
Consumer<String> onThinking,
Consumer<String> onContent,
Runnable onFinish,
Consumer<Throwable> onError)
throws NoApiKeyException, ApiException {
List<MultiModalMessage> messages = new ArrayList<>();
// 加载历史对话
if (history != null) {
for (ChatMessage msg : history) {
if ("user".equals(msg.getRole())) {
messages.add(MultiModalMessage.builder().role(Role.USER.getValue())
.content(Collections.singletonList(Collections.singletonMap("text", msg.getContent()))).build());
} else if ("assistant".equals(msg.getRole())) {
messages.add(MultiModalMessage.builder().role(Role.ASSISTANT.getValue())
.content(Collections.singletonList(Collections.singletonMap("text", msg.getContent()))).build());
}
}
}
// 添加当前用户消息
messages.add(MultiModalMessage.builder().role(Role.USER.getValue())
.content(Collections.singletonList(Collections.singletonMap("text", userContent))).build());
// 构建请求:开启思考过程 + 增量流式输出
MultiModalConversationParam param = MultiModalConversationParam.builder()
.apiKey(apiKey)
.model(model)
.messages(messages)
.enableThinking(true) // 🔥 必须开启:获取思考过程
.incrementalOutput(true) // 🔥 增量输出
.build();
// 流式调用
Flowable<com.alibaba.dashscope.aigc.multimodalconversation.MultiModalConversationResult> resultFlow = multiModalConversation.streamCall(param);
resultFlow.subscribe(
result -> {
try {
MultiModalMessage message = result.getOutput().getChoices().get(0).getMessage();
// 1. 推送思考过程
String reasoningContent = message.getReasoningContent();
if (reasoningContent != null && !reasoningContent.trim().isEmpty()) {
onThinking.accept(reasoningContent);
}
// 2. 推送正式回答
List<java.util.Map<String, Object>> contents = message.getContent();
if (contents == null || contents.isEmpty()) return;
String output = (String) contents.get(0).get("text");
if (output == null || output.trim().isEmpty()) return;
onContent.accept(output);
} catch (Exception e) {
log.error("处理流式响应异常", e);
}
},
error -> {
log.error("流式调用异常", error);
onError.accept(error);
},
onFinish // 调用完成
);
}
}
3. WebSocket配置(拦截器+处理器+配置类)
(1)连接拦截器(AliAuthInterceptor.java)
简单鉴权+用户ID绑定:
java
package com.example.demo.config.websocket;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import java.util.Map;
@Slf4j
@Component
public class AliAuthInterceptor implements HandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) {
if (request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
String userId = servletRequest.getServletRequest().getParameter("userId");
if (StrUtil.isBlank(userId)) {
log.warn("WebSocket 连接被拒绝:缺少 userId");
return false;
}
attributes.put("userId", userId);
return true;
}
return false;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {}
}
(2)消息处理器(AliWebSocketHandler.java)
处理前后端通信、实时推送流式数据:
java
package com.example.demo.config.websocket;
import com.alibaba.fastjson2.JSON;
import com.example.demo.request.ChatMessage;
import com.example.demo.service.AliQwenService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Component
public class AliWebSocketHandler extends TextWebSocketHandler {
private static final Map<String, WebSocketSession> SESSION_MAP = new ConcurrentHashMap<>();
@Autowired
private AliQwenService aliQwenService;
// 连接建立
@Override
public void afterConnectionEstablished(WebSocketSession session) {
String userId = getUserId(session);
SESSION_MAP.put(userId, session);
sendMessage(session, ChatMessage.system("连接成功,开始对话吧!"));
}
// 接收消息
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
ChatMessage chatMessage = JSON.parseObject(message.getPayload(), ChatMessage.class);
if ("chat".equals(chatMessage.getType())) {
handleStreamChat(session, chatMessage);
}
}
// 流式对话处理
private void handleStreamChat(WebSocketSession session, ChatMessage userMessage) {
sendMessage(session, ChatMessage.start());
try {
aliQwenService.streamChat(
userMessage.getContent(),
userMessage.getHistory(),
thinking -> sendMessage(session, ChatMessage.thinking(thinking)), // 思考过程
content -> sendMessage(session, ChatMessage.content(content)), // 正式内容
() -> sendMessage(session, ChatMessage.finish()), // 完成
error -> sendMessage(session, ChatMessage.error(error.getMessage())) // 错误
);
} catch (Exception e) {
sendMessage(session, ChatMessage.error("调用大模型失败: " + e.getMessage()));
}
}
// 发送消息
private void sendMessage(WebSocketSession session, ChatMessage message) {
try {
session.sendMessage(new TextMessage(JSON.toJSONString(message)));
} catch (IOException e) {
log.error("发送消息失败", e);
}
}
private String getUserId(WebSocketSession session) {
return session.getAttributes().get("userId").toString();
}
}
(3)WebSocket核心配置(WebSocketConfig.java)
开启WebSocket、注册路由:
java
package com.example.demo.config.websocket;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
@Configuration
@EnableWebSocket
public class WebSocketConfig {
@Bean
public WebSocketConfigurer webSocketConfigurer(AliAuthInterceptor aliAuthInterceptor, AliWebSocketHandler aliWebSocketHandler) {
return registry -> {
registry.addHandler(aliWebSocketHandler, "/ws/qwen")
.addInterceptors(aliAuthInterceptor)
.setAllowedOrigins("*");
};
}
}
4. 页面跳转Controller(ChatController.java)
java
package com.example.demo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class ChatController {
@GetMapping("/")
public String index() {
return "chat";
}
}
5. 前端页面(chat.html)
路径:resources/templates/chat.html
功能:美观UI、实时渲染思考过程、流式回答、Markdown格式化、自动重连、心跳保活
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Qwen3.5-plus 实时对话</title>
<style>
* {margin: 0;padding: 0;box-sizing: border-box;}
body {font-family: Arial;background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);height: 100vh;display: flex;justify-content: center;align-items: center;}
.chat-container {width: 90%;max-width: 1000px;height: 90vh;background: white;border-radius: 20px;display: flex;flex-direction: column;overflow: hidden;}
.chat-header {background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);color: white;padding: 20px;text-align: center;}
.connection-status {width: 10px;height: 10px;border-radius: 50%;display: inline-block;background: #ff4444;}
.connection-status.connected {background: #00ff88;}
.chat-messages {flex: 1;overflow-y: auto;padding: 20px;background: #f5f7fa;}
.thinking-box {background: #fff8e1;border-left: 4px solid #ffc107;padding: 12px;margin: 10px 0;border-radius: 8px;}
.answer-box {background: white;padding: 12px;border-radius: 8px;}
.answer-box.streaming::after {content: "▋";animation: blink 1s infinite;color: #667eea;}
@keyframes blink {0%,50%{opacity:1;}51%,100%{opacity:0;}}
.chat-input-area {padding: 20px;background: white;border-top: 1px solid #e0e0e0;}
#messageInput {flex:1;padding:12px 20px;border:2px solid #e0e0e0;border-radius:25px;outline:none;}
#sendBtn {padding:12px 30px;background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);color:white;border:none;border-radius:25px;cursor: pointer;}
</style>
</head>
<body>
<div class="chat-container">
<div class="chat-header">
<h1>🤖 Qwen3.5-plus 智能助手</h1>
<p><span class="connection-status" id="statusDot"></span><span id="statusText">连接中...</span></p>
</div>
<div class="chat-messages" id="chatMessages"></div>
<div class="chat-input-area">
<div class="input-wrapper">
<input type="text" id="messageInput" placeholder="输入消息,按Enter发送">
<button id="sendBtn" onclick="sendMessage()">发送</button>
</div>
</div>
</div>
<script>
const userId = 'user_' + Math.random().toString(36).substr(2,9);
let ws = null;
let isStreaming = false;
let chatHistory = [];
let currentThinkingBox = null, currentAnswerBox = null;
let thinkingContent = '', answerContent = '';
// 连接WebSocket
function connect() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
ws = new WebSocket(`${protocol}//${window.location.host}/ws/qwen?userId=${userId}`);
ws.onopen = () => {document.getElementById('statusDot').classList.add('connected');document.getElementById('statusText').textContent='已连接';}
ws.onmessage = (e) => handleMessage(JSON.parse(e.data));
ws.onclose = () => setTimeout(connect,5000);
}
// 处理消息
function handleMessage(msg) {
switch(msg.type){
case 'start': startNewResponse();break;
case 'thinking': appendThinking(msg.thinking);break;
case 'content': appendContent(msg.content);break;
case 'finish': finishResponse();break;
}
}
// 开始响应
function startNewResponse() {
isStreaming = true;
const div = document.getElementById('chatMessages');
currentThinkingBox = document.createElement('div');
currentThinkingBox.className='thinking-box';
currentThinkingBox.innerHTML=`<div class="thinking-label">思考过程</div><div class="thinking-content"></div>`;
currentAnswerBox = document.createElement('div');
currentAnswerBox.className='answer-box streaming';
div.appendChild(currentThinkingBox);
div.appendChild(currentAnswerBox);
}
// 追加思考内容
function appendThinking(text) {
thinkingContent += text;
currentThinkingBox.querySelector('.thinking-content').textContent=thinkingContent;
}
// 追加回答内容
function appendContent(text) {
answerContent += text;
currentAnswerBox.innerHTML=answerContent;
}
// 完成响应
function finishResponse() {
isStreaming=false;
currentAnswerBox.classList.remove('streaming');
}
// 发送消息
function sendMessage() {
const content = document.getElementById('messageInput').value.trim();
if(!content||isStreaming)return;
ws.send(JSON.stringify({type:'chat',content:content,history:chatHistory}));
document.getElementById('messageInput').value='';
}
window.onload=connect;
</script>
</body>
</html>
五、运行测试
- 替换
application.yml中的阿里云API-Key - 启动Spring Boot项目
- 浏览器访问:
http://localhost:8080 - 发送消息,即可看到:
✅ 实时打印思考过程
✅ 流式输出回答内容 (打字机效果)
✅ 上下文记忆历史对话
六、核心功能解析
- 思考过程开启 :
enableThinking(true)必须开启,Qwen3.5-Plus才会返回推理内容 - 流式输出 :阿里云SDK
streamCall+ WebSocket全双工通信,实现实时推送 - 上下文记忆:前端传递历史对话,后端拼接消息参数,实现多轮对话
- 前端体验:动态追加文本、自动滚动、Markdown格式化、断线重连
七、常见问题&注意事项
1. 常见问题
- WebSocket连接失败 :检查端口、路由
/ws/qwen、跨域配置 - 无思考过程 :确保
enableThinking=true,且模型支持思考(qwen3.5-plus支持) - API-Key报错:检查密钥正确性,是否开通模型权限
- 流式输出卡顿 :网络问题,已开启
incrementalOutput=true增量输出
2. 生产环境优化
- API-Key不要硬编码,使用环境变量/配置中心
- WebSocket鉴权替换为JWT
- 限制历史对话长度,避免请求参数过大
- 增加限流、异常熔断机制
八、总结
本文完整实现了 Spring Boot + WebSocket + 阿里云百炼 的智能对话系统,核心亮点是流式输出+思考过程实时展示,代码结构清晰、开箱即用,非常适合大模型应用开发入门学习和二次开发!
如果对你有帮助,欢迎点赞+收藏+关注~