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 + 阿里云百炼 的智能对话系统,核心亮点是流式输出+思考过程实时展示,代码结构清晰、开箱即用,非常适合大模型应用开发入门学习和二次开发!

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

相关推荐
橙淮21 小时前
并发编程(六)
java·jvm
拽着尾巴的鱼儿21 小时前
springboot openfeign 自定义feign 接口重试机制
java·spring boot·后端
白露与泡影1 天前
2026大厂Java面试题大全!牛客网最新版
java·开发语言
西陵1 天前
Agent 为什么会陷入 Doom Loop?OpenClaw 的破解之道
前端·人工智能·ai编程
EntyIU1 天前
JVM内存与GC笔记
java·jvm·笔记
XS0301061 天前
并发编程 六
java·后端
yaoxin5211231 天前
419. 现代 Java IO 最佳实践 - 写入文本文件
java·windows·python
雪宫街道1 天前
synchronized 锁的范围:对象锁、类锁与代码块锁
java·jvm·后端·面试
向量引擎1 天前
向量引擎API中转站深度测评:如何实现低成本、高并发的向量检索
人工智能·gpt·aigc·api·ai编程
x***r1511 天前
linux安装 jdk-8u291-linux-x64.tar.gz 详细步骤(解压配置环境变量)
java