Uniapp使用websocket进行ai回答的流式输出

之前要使用sse的时候,发现uniapp不支持sse......,然后想着直接使用双向通信,websocket来实现流式输出。

接入达模型我使用的是百炼平台的dashscope方式的sdk,调用的deepseek-v3.1模型。实现了流式输出、历史消息上下文、历史记录存取、动态提示词。

具体实现很简单,见代码:

前端uniapp部分代码:

javascript 复制代码
// 建立WebSocket连接
const connectWebSocket = () => {
    // 关闭现有的连接(如果有的话)
    if (socketTask) {
        socketTask.close()
}
    
    // 创建新的WebSocket连接
    socketTask = uni.connectSocket({
        url: AiWsUrl + 'chatws', // WebSocket连接地址
        success: () => {
            console.log('WebSocket连接成功')
        },
        fail: (err) => {
            console.error('WebSocket连接失败:', err)
            uni.showToast({
                icon: 'none',
                title: '连接服务器失败'
            })
        }
    })
    
    // 监听WebSocket连接打开事件
    socketTask.onOpen(() => {
        console.log('WebSocket连接已打开')
        uni.showToast({
            icon: 'none',
            title: '已连接到服务器'
        })
    })
    
    // 监听WebSocket错误事件
    socketTask.onError((err) => {
        console.error('WebSocket发生错误:', err)
        uni.showToast({
            icon: 'none',
            title: '连接出现错误'
        })
    })
    
    // 监听WebSocket关闭事件
    socketTask.onClose(() => {
        console.log('WebSocket连接已关闭')
        uni.showToast({
            icon: 'none',
            title: '与服务器断开连接'
        })
    })
    
    // 监听WebSocket消息接收事件
    socketTask.onMessage((res) => {
        handleWebSocketMessage(res.data as string)
    })
}

// 处理WebSocket接收到的消息
const handleWebSocketMessage = (data: string) => {
    try {
        // 解析接收到的数据
        const messageData = JSON.parse(data)
        
        // 根据消息类型处理
        switch (messageData.type) {
            case 'start':
                // 开始接收流式响应
                isReceivingStream.value = true
                fullText.value = ''
                displayedLength.value = 0
                
                // 清除之前的定时器
                if (typingTimer) {
                    clearInterval(typingTimer)
                    typingTimer = null
                }
                
                streamingMessage.value = {
                    role: 'assistant',
                    content: '',
                    time: new Date().toISOString(),
                    sendFrom: 0,
                    sessionId: currentSessionId.value,
                    assistantStyleId: currentAssistantModel.value
                }
                messages.value.push(streamingMessage.value)
                break
                
            case 'chunk':
                // 接收流式响应片段
                if (isReceivingStream.value && streamingMessage.value) {
                    // 将片段内容追加到完整文本中
                    fullText.value += messageData.content
                    
                    // 如果还没有启动打字机效果,则启动它
                    if (!typingTimer) {
                        startTypingEffect()
                    }
                }
                break
                
            case 'end':
                // 流式响应结束
                isReceivingStream.value = false
                // 确保所有文本都显示完毕
                if (typingTimer) {
                    clearInterval(typingTimer)
                    typingTimer = null
                }
                if (streamingMessage.value) {
                    streamingMessage.value.content = fullText.value
                }
                streamingMessage.value = null
                fullText.value = ''
                displayedLength.value = 0
                break
                
            case 'error':
                // 错误处理
                isReceivingStream.value = false
                if (typingTimer) {
                    clearInterval(typingTimer)
                    typingTimer = null
                }
                streamingMessage.value = null
                fullText.value = ''
                displayedLength.value = 0
                uni.showToast({
                    icon: 'none',
                    title: messageData.message || '服务器响应错误'
                })
                break
                
            default:
                console.warn('未知的WebSocket消息类型:', messageData.type)
        }
    } catch (err) {
        console.error('解析WebSocket消息失败:', err)
    }
}

具体发送ws消息给后端就直接调用socketTask的send方法就可以。

后端:

配置类:

条件注解和Profile注解是防止打包的时候,说缺少相应的容器,测试环境中没有可用的 ServerContainer(WebSocket 服务器容器)。但是正式的环境是没问题的。

java 复制代码
/**
 * 开启WebSocket支持
 **/
@Configuration
public class WebSocketConfig {
    @Bean
    @Profile("!test")
    @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
 
}

此外还有一个问题,如果想使用service之类的bean,会出现说为null的情况,之后想了想,还是得用srpingcontext来手动注入bean。

java 复制代码
@Component
public class SpringContextUtil implements ApplicationContextAware {
    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringContextUtil.applicationContext = applicationContext;
    }

    public static <T> T getBean(Class<T> clazz) {
        return applicationContext.getBean(clazz);
    }

    public static Object getBean(String name) {
        return applicationContext.getBean(name);
    }
}

具体使用见onMessage处。

对于websocket,把这个下面这个ws类当成类似于controller的存在就行。controller是负责http请求响应的,而下面这个ws类是处理websocket连接的。

发送消息给前端只需要调用session来实现就可以,防止高耦合,可以和我这种一样,直接将session传入service中,交给具体的部分去调用,记住session是jakarta.websocket包下的,不要导错了。

java 复制代码
import com.alibaba.fastjson2.JSON;
import com.mauro.serverai.service.ChatMessageService;
import com.mauro.serverai.utils.SpringContextUtil;
import com.mauro.servercommon.model.ai.chat.ChatMessageVo;
import jakarta.websocket.OnClose;
import jakarta.websocket.OnMessage;
import jakarta.websocket.OnOpen;
import jakarta.websocket.Session;
import jakarta.websocket.server.ServerEndpoint;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.util.concurrent.CopyOnWriteArraySet;

@ServerEndpoint("/ws/chatws")
@Component
@Slf4j
public class ChatWebSocket {

    //与某个客户端的连接会话,需要通过它来给客户端发送数据
    private Session session;

    //concurrent包的线程安全Set,用来存放每个客户端对应的WebSocket对象。
    private static CopyOnWriteArraySet<ChatWebSocket> chatWebSockets =new CopyOnWriteArraySet<>();


    /**
     *  建立连接成功
     * @param session
     */
    @OnOpen
    public void onOpen(Session session){
        this.session=session;
        chatWebSockets.add(this);
        log.info("【websocket消息】 有新的连接,总数{}", chatWebSockets.size());
    }

    /**
     * 连接关闭
     */
    @OnClose
    public void onClose(){
        this.session=session;
        chatWebSockets.remove(this);
        log.info("【websocket消息】 连接断开,总数{}", chatWebSockets.size());
    }

    /**
     * 接收客户端消息
     * @param message
     */
    @OnMessage
    public void onMessage(String message){
        log.info("【websocket消息】 收到客户端发来的消息:{}",message);
        ChatMessageVo chatMessageVo = JSON.parseObject(message, ChatMessageVo.class);
        if (chatMessageVo == null){
            return;
        }
        ChatMessageService chatMessageService = SpringContextUtil.getBean(ChatMessageService.class);
        chatMessageService.sendMessage(chatMessageVo,session);
    }
}

service这里通过不同助手的类型传入了不同的提示词(background)(我放数据库了),同时根据会话id获取某次会话的历史记录,当然每个模型都有最大上下文,要控制好内容总长度。

java 复制代码
@Service
public class ChatMessageServiceImpl extends ServiceImpl<ChatMessageMapper, ChatMessagePo> implements ChatMessageService {
    @Resource
    private ChatMessageMapper chatMessageMapper;
    @Resource
    private AssistantStyleService assistantStyleService;
    @Resource
    private AIService aiService;

    @Override
    public Integer insert(ChatMessageVo chatMessage) {
        if (chatMessage == null) {
            return 0;
        }
        ChatMessagePo chatMessagePo = new ChatMessagePo();
        BeanUtils.copyProperties(chatMessage, chatMessagePo);
        chatMessagePo.setCreateTime(LocalDateTime.now());
        chatMessagePo.setUpdateTime(LocalDateTime.now());
        return chatMessageMapper.insert(chatMessagePo);
    }

    @Override
    public PageData<ChatMessageVo> getHistoryMessages(Integer pageNum, Integer pageSize, String sessionId) {
        // 分页查询
        Page<ChatMessagePo> page = new Page<>(pageNum, pageSize);
        IPage<ChatMessagePo> chatMessagePoPage = chatMessageMapper.selectPage(page, new LambdaQueryWrapper<ChatMessagePo>()
                .eq(ChatMessagePo::getSessionId, sessionId)
                .orderByDesc(ChatMessagePo::getCreateTime));
        // 转换为 Vo 列表
        List<ChatMessageVo> chatMessageVoList = chatMessagePoPage.getRecords().stream()
                .map(chatMessagePo -> {
                    ChatMessageVo chatMessageVo = new ChatMessageVo();
                    BeanUtils.copyProperties(chatMessagePo, chatMessageVo);
                    LocalDateTime createTime = chatMessagePo.getCreateTime();
                    Date time = Date.from(createTime.atZone(ZoneId.systemDefault()).toInstant());
                    chatMessageVo.setTime(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(time));
                    return chatMessageVo;
                }).collect(Collectors.toList());
        // 构建分页结果
        return new PageData<>((int) chatMessagePoPage.getTotal(), (int) chatMessagePoPage.getPages(), pageSize, pageNum, chatMessageVoList);
    }

    @Override
    public void sendMessage(ChatMessageVo chatMessage, Session session) {
        insert(chatMessage);
        String sessionId = chatMessage.getSessionId();
        int pageNum = 1;
        int pageSize = 5;
        PageData<ChatMessageVo> historyMessages = getHistoryMessages(pageNum, pageSize, sessionId);
        List<ChatMessageVo> memories = historyMessages.getList();
        AtomicInteger totalLength = new AtomicInteger();
        while (pageNum <= historyMessages.getTotalPage() && totalLength.get() < 90000) {
            PageData<ChatMessageVo> historyMessages1 = getHistoryMessages(pageNum, pageSize, sessionId);
            List<ChatMessageVo> list = historyMessages1.getList();
            list.stream().forEach(chatMessageVo -> {
                totalLength.addAndGet(chatMessageVo.getContent().length());
            });
            pageNum++;
            memories.addAll(historyMessages.getList());
        }
        Integer assistantStyleId = chatMessage.getAssistantStyleId();
        AssistantStylePo assistantStyle = assistantStyleService.getById(assistantStyleId);
        String background = assistantStyle.getDescription();
        String s = aiService.sendByMemories(chatMessage.getContent(), background, session, memories);

        ChatMessageVo chatMessageVo = new ChatMessageVo();
        chatMessageVo.setRole("assistant");
        chatMessageVo.setSessionId(sessionId);
        chatMessageVo.setAssistantStyleId(assistantStyleId);
        chatMessageVo.setContent(s);
        chatMessageVo.setSendFrom(-1 * assistantStyle.getId());
        insert(chatMessageVo);
    }
}

我的是根据type、content两个字段组成的JSON对象来返回给前端,可以让前端知道流式输出的进度。可以根据自己的想法改动。

java 复制代码
import com.alibaba.dashscope.aigc.generation.Generation;
import com.alibaba.dashscope.aigc.generation.GenerationParam;
import com.alibaba.dashscope.aigc.generation.GenerationResult;
import com.alibaba.dashscope.common.Message;
import com.alibaba.dashscope.common.Role;
import com.alibaba.dashscope.exception.ApiException;
import com.alibaba.dashscope.exception.InputRequiredException;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.alibaba.fastjson.JSONObject;
import com.mauro.serverai.service.AIService;
import com.mauro.servercommon.model.ai.chat.ChatMessageVo;
import io.reactivex.Flowable;
import jakarta.websocket.Session;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

@Service
public class AIServiceImpl implements AIService {
    private static void handleGenerationResult(GenerationResult message,StringBuilder resultString, Session session) throws  IOException {
        String content = message.getOutput().getChoices().get(0).getMessage().getContent();
        if (content != null && !content.isEmpty()) {
            resultString.append(content);
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("content",content);
            jsonObject.put("type","chunk");
            session.getBasicRemote().sendText(jsonObject.toJSONString());
            System.out.println("AI回复:");
            System.out.print(content);
        }
    }
    private static GenerationParam buildGenerationParam(Message userMsg,Message systemMsg) {
        return GenerationParam.builder()
                // 若没有配置环境变量,请用阿里云百炼API Key将下行替换为:.apiKey("sk-xxx")
                .apiKey("sk-xxxxxxxxxxxxxxxxxxxxxx")
                // 此处以 deepseek-v3.2-exp 为例,可按需更换模型名称为 deepseek-v3.1、deepseek-v3 或 deepseek-r1
                .model("deepseek-v3.1")
                // 开启思考模式,该参数仅对 deepseek-v3.2-exp 和 deepseek-v3.1 有效。deepseek-v3 和 deepseek-r1 设定不会报错
                .enableThinking(false)
                .incrementalOutput(true)
                .resultFormat("message")
                .messages(Arrays.asList(systemMsg,userMsg))
                .build();
    }

    private static GenerationParam buildGenerationParamByMemories(Message userMsg,Message systemMsg,List<Message> memoriesMsg) {
        // 合并systemMsg、memoriesMsg、userMsg
        List<Message> allMessages = new ArrayList<>();
        allMessages.add(systemMsg);
        allMessages.addAll(memoriesMsg); // memoriesMsg应该是List<Message>类型
        allMessages.add(userMsg);
        return GenerationParam.builder()
                // 若没有配置环境变量,请用阿里云百炼API Key将下行替换为:.apiKey("sk-xxx")
                .apiKey("sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
                // 此处以 deepseek-v3.2-exp 为例,可按需更换模型名称为 deepseek-v3.1、deepseek-v3 或 deepseek-r1
                .model("deepseek-v3.1")
                // 开启思考模式,该参数仅对 deepseek-v3.2-exp 和 deepseek-v3.1 有效。deepseek-v3 和 deepseek-r1 设定不会报错
                .enableThinking(false)
                .incrementalOutput(true)
                .resultFormat("message")
                .messages(allMessages)
                .build();
    }
    public static void streamCallWithMessage(Generation gen, Message userMsg,Message systemMsg, StringBuilder resultString, Session session)
            throws NoApiKeyException, ApiException, InputRequiredException {
        GenerationParam param = buildGenerationParam(userMsg,systemMsg);
        Flowable<GenerationResult> result = gen.streamCall(param);
        result.blockingForEach(message -> handleGenerationResult(message, resultString,session));
    }

    public static void streamCallWithMessageByMemories(Generation gen, Message userMsg, Message systemMsg, List<Message> memoriesMsg, StringBuilder resultString, Session session)
            throws NoApiKeyException, ApiException, InputRequiredException {
        GenerationParam param = buildGenerationParam(userMsg,systemMsg);
        Flowable<GenerationResult> result = gen.streamCall(param);
        result.blockingForEach(message -> handleGenerationResult(message, resultString,session));
    }

    @Override
    public String send(String content, String systemPrompt, Session session) {
        StringBuilder result = new StringBuilder();
        try {
            Generation gen = new Generation();
            Message userMsg = Message.builder().role(Role.USER.getValue()).content(content).build();
            Message systemMsg = Message.builder().role(Role.SYSTEM.getValue()).content(systemPrompt).build();
            streamCallWithMessage(gen, userMsg,systemMsg, result,session);
        } catch (ApiException | NoApiKeyException | InputRequiredException e) {
            System.err.println("An exception occurred: " + e.getMessage());
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("content","");
            jsonObject.put("type","error");
            try {
                session.getBasicRemote().sendText(jsonObject.toJSONString());
            } catch (IOException i) {
                throw new RuntimeException(i);
            }
        }
        return result.toString();
    }

    @Override
    public String sendByMemories(String content, String systemPrompt, Session session, List<ChatMessageVo> memories) {
        StringBuilder result = new StringBuilder();
        try {
            Generation gen = new Generation();
            Message userMsg = Message.builder().role(Role.USER.getValue()).content(content).build();
            Message systemMsg = Message.builder().role(Role.SYSTEM.getValue()).content(systemPrompt).build();
            // 转换memories为Message列表
            List<Message> memoriesMsg = memories.stream()
                    .map(mem -> Message.builder().role(mem.getRole().equals("user") ? Role.USER.getValue() : Role.ASSISTANT.getValue()).content(mem.getContent()).build())
                    .collect(Collectors.toList());
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("content","");
            jsonObject.put("type","start");
            session.getBasicRemote().sendText(jsonObject.toJSONString());
            streamCallWithMessageByMemories(gen, userMsg,systemMsg,memoriesMsg, result,session);
        } catch (ApiException | NoApiKeyException | InputRequiredException e) {
            System.err.println("An exception occurred: " + e.getMessage());
        } catch (IOException e) {
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("content","");
            jsonObject.put("type","error");
            try {
                session.getBasicRemote().sendText(jsonObject.toJSONString());
            } catch (IOException i) {
                throw new RuntimeException(i);
            }
        }
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("content","");
        jsonObject.put("type","end");
        try {
            session.getBasicRemote().sendText(jsonObject.toJSONString());
        } catch (IOException e) {
            JSONObject jsonObjectError = new JSONObject();
            jsonObjectError.put("content","");
            jsonObjectError.put("type","error");
            try {
                session.getBasicRemote().sendText(jsonObjectError.toJSONString());
            } catch (IOException i) {
                throw new RuntimeException(i);
            }
        }
        return result.toString();
    }
}

至此就完成了通过websocket来流式输出。

相关推荐
156082072196 小时前
基于7VX690T FPGA实现万兆TCP/IP资源和性能测试
网络协议·tcp/ip·fpga开发
踏浪无痕8 小时前
线上偶发 502 排查:用 Netty 成功复现 KeepAlive 时间窗口案例实战(附完整源码)
运维·网络协议
javaの历练之路9 小时前
基于 SpringBoot+Vue2 的前后端分离博客管理系统(含 WebSocket+ECharts)
spring boot·websocket·echarts
北京耐用通信9 小时前
告别“牵一发而动全身”:耐达讯自动化Profibus PA分线器为石化流量计网络构筑安全屏障
人工智能·网络协议·安全·自动化·信息与通信
Sinowintop9 小时前
易连EDI-EasyLink无缝集成之消息队列Kafka
分布式·网络协议·kafka·集成·国产化·as2·国产edi
良逍Ai出海10 小时前
Build in Public|为什么我开始做一款相册清理 App(听说有竞品年收益40W)
ios·uni-app·ai编程·coding
阿巴~阿巴~17 小时前
自定义协议设计与实践:从协议必要性到JSON流式处理
服务器·网络·网络协议·json·操作系统·自定义协议
jinxinyuuuus1 天前
GTA 风格 AI 生成器:跨IP融合中的“视觉语义冲突”与风格适配损失
人工智能·网络协议
嵌入式-小王1 天前
每天掌握一个网络协议----ICMP
网络·网络协议·ping