Spring Boot 2.7+JDK8+WebSocket对接阿里云百炼Qwen3.5-Plus 实现流式对话+思考过程实时展示

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. 阿里云百炼准备

  1. 登录阿里云百炼控制台
  2. 生成API-Key:右上角头像 → API-Key管理 → 创建密钥
  3. 开通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>

五、运行测试

  1. 替换application.yml中的阿里云API-Key
  2. 启动Spring Boot项目
  3. 浏览器访问:http://localhost:8080
  4. 发送消息,即可看到:
    ✅ 实时打印思考过程
    ✅ 流式输出回答内容 (打字机效果)
    ✅ 上下文记忆历史对话

六、核心功能解析

  1. 思考过程开启enableThinking(true) 必须开启,Qwen3.5-Plus才会返回推理内容
  2. 流式输出 :阿里云SDK streamCall + WebSocket全双工通信,实现实时推送
  3. 上下文记忆:前端传递历史对话,后端拼接消息参数,实现多轮对话
  4. 前端体验:动态追加文本、自动滚动、Markdown格式化、断线重连

七、常见问题&注意事项

1. 常见问题

  • WebSocket连接失败 :检查端口、路由/ws/qwen、跨域配置
  • 无思考过程 :确保enableThinking=true,且模型支持思考(qwen3.5-plus支持)
  • API-Key报错:检查密钥正确性,是否开通模型权限
  • 流式输出卡顿 :网络问题,已开启incrementalOutput=true增量输出

2. 生产环境优化

  • API-Key不要硬编码,使用环境变量/配置中心
  • WebSocket鉴权替换为JWT
  • 限制历史对话长度,避免请求参数过大
  • 增加限流、异常熔断机制

八、总结

本文完整实现了 Spring Boot + WebSocket + 阿里云百炼 的智能对话系统,核心亮点是流式输出+思考过程实时展示,代码结构清晰、开箱即用,非常适合大模型应用开发入门学习和二次开发!

如果对你有帮助,欢迎点赞+收藏+关注~

相关推荐
快乐柠檬不快乐2 小时前
IDEA报错内存溢出解决(java.lang.OutOfMemoryError)
java·ide·intellij-idea
程序0072 小时前
在线五子棋小游戏(.NET Core+FreeSql+WebSocket ) html+js
websocket·html·.netcore
RDCJM2 小时前
Spring Boot项目接收前端参数的11种方式
前端·spring boot·后端
.柒宇.2 小时前
力扣hot 100之和为 K 的子数组(Java版)
java·算法·leetcode
%Leo2 小时前
macos idea 插件搜索不到
java·intellij-idea
苏渡苇2 小时前
枚举的高级用法——用枚举实现策略模式和状态机
java·单例模式·策略模式·枚举·状态机·enum
鱼鳞_2 小时前
Java学习笔记_Day19
java·笔记·学习
candyTong2 小时前
Claude Code 是怎么跑起来的:从 Agent Loop 理解代理循环实现
前端·后端·ai编程
曹牧2 小时前
Java:驱动程序无法通过使用安全套接字层(SSL)加密与 SQL Server 建立连接
java·开发语言·ssl