之前要使用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来流式输出。

