当前位置:   article > 正文

实现AI问答的流式输出_如何利用星火ai实现流输出

如何利用星火ai实现流输出

概要

        最近写的一个小项目,涉及到AI问答的功能,所以接入了科大讯飞的Api,刚开始有个问题是:使用http请求,一次性完整的获得一个答案,可能得需要30多秒,这样用户体验十分不友好,所以后面决定使用流式数据实现这一部分功能。

        用到的技术:SpringBoot、WebSocket等。

SpringBoot中如何使用WebSocket?

1)引入依赖

  1.         <dependency>
  2.             <groupId>org.springframework.boot</groupId>
  3.             <artifactId>spring-boot-starter-websocket</artifactId>
  4.             <version>3.1.0</version>
  5.         </dependency>

2) 配置类

把serverEndpointExporter 交给Spring管理

  1. @Configuration
  2. public class WebSocketConfig {
  3. @Bean
  4. public ServerEndpointExporter serverEndpointExporter()
  5. {
  6. return new ServerEndpointExporter();
  7. }
  8. }

3) 创建一个WebSocker服务类

下面是一个基本的服务类

  1. @Component
  2. @Slf4j
  3. @ServerEndpoint("/ws/{userid}")
  4. //这里类似于Controller的接口,不同于Controller接口,这里是ws://....进行访问的。
  5. public class WebSocketServer {
  6. /**
  7. * 连接建立成功调用的方法
  8. */
  9. @OnOpen
  10. public void onOpen(Session session, @PathParam("userid") Integer userid) {
  11. log.info("{} 与服务器进行连接.",userid);
  12. }
  13. /**
  14. * 收到客户端消息后调用的方法
  15. *
  16. * @param message 客户端发送过来的消息
  17. */
  18. @OnMessage
  19. public void onMessage(String message, @PathParam("userid") String userid) {
  20. log.info("用户:{},发送信息:{}",userid,message);
  21. }
  22. /**
  23. * 连接关闭调用的方法
  24. *
  25. * @param userid
  26. */
  27. @OnClose
  28. public void onClose(@PathParam("userid") Integer userid) {
  29. log.info("{} :关闭连接" , userid);
  30. }
  31. /**
  32. * 给用户发送信息
  33. *
  34. * @param message
  35. */
  36. public void sendAIResultToUser(Integer userid,String message) {
  37. }
  38. }

4) 测试连接

WebSocket在线测试工具 (wstool.js.org)

 

实现AI问答的流式输出

       接入星火API,使用的是一个github上写好的SDK

briqt/xunfei-spark4j: 科大讯飞 星火认知大模型API Java SDK 流式调用、同步调用、FunctionCall、tokens统计、多版本切换;长期更新 (github.com)

  1. <!--讯飞SDK-->
  2. <dependency>
  3. <groupId>io.github.briqt</groupId>
  4. <artifactId>xunfei-spark4j</artifactId>
  5. <version>1.2.0</version>
  6. </dependency>

        这个SDK提供了两种模式:一种是基于http的,一种是socket的,如果实现流式输出必然得使用后者。那么我想到的思路大概是这样的:前端打开AI问答的页面,在此时就和服务器进行socket连接,后端通过唯一的userId作为key,session作为value,把连接信息放到Map中。

当用户写好问题时,进行一次http请求,也就是相当于把问题和用户信息给后端(其实这里可以直接通过socket实现对话的方式进行AI问答,由于我需要对用户使用AI情况进行统计、管理,所以加了层http请求), 通过请求拿到userId,进而拿到对应的session,这时用户一定是和服务器有socket连接的,后端服务器与星火的服务器进行socket连接,星火的服务器给后端服务器响应内容,后端服务器就把相应的内容响应给前端,前端进行展示即可;用户可以继续使用,也可以退出此功能,退出时,就会把对应的session删除。

WebSocketServer 代码

        前端首先回到这里与后端服务器的进行socket连接。

  1. @Component
  2. @Slf4j
  3. @ServerEndpoint("/ws/{userid}")
  4. public class WebSocketServer {
  5. //存放会话对象
  6. private static Map<Integer, Session> sessionMap = new HashMap();
  7. /**
  8. * 连接建立成功调用的方法
  9. */
  10. @OnOpen
  11. public void onOpen(Session session, @PathParam("userid") Integer userid) {
  12. log.info("{} 与服务器进行连接.",userid);
  13. sessionMap.put(userid , session);
  14. }
  15. /**
  16. * 收到客户端消息后调用的方法
  17. *
  18. * @param message 客户端发送过来的消息
  19. */
  20. @OnMessage
  21. public void onMessage(String message, @PathParam("userid") String userid) {
  22. log.info("用户:{},发送信息:{}",userid,message);
  23. }
  24. /**
  25. * 连接关闭调用的方法
  26. *
  27. * @param userid
  28. */
  29. @OnClose
  30. public void onClose(@PathParam("userid") Integer userid) {
  31. log.info("{} :关闭连接" , userid);
  32. sessionMap.remove(userid);
  33. }
  34. /**
  35. * 给用户发送信息
  36. *
  37. * @param message
  38. */
  39. public void sendAIResultToUser(Integer userid,String message) {
  40. //获得对应的session
  41. Session session = sessionMap.get(userid);
  42. try {
  43. //服务器向客户端发送消息
  44. session.getBasicRemote().sendText(message);
  45. } catch (Exception e) {
  46. e.printStackTrace();
  47. }
  48. }
  49. /**
  50. * @description: 通过userId获得一个session
  51. * @param: [java.lang.Integer]
  52. * @return: javax.websocket.Session
  53. */
  54. public Session getSessionByUserId(Integer userid){
  55. return sessionMap.get(userid);
  56. }
  57. }

 

Controller层 

        调用/ai接口

  1. @Resource
  2. private WebSocketServer webSocketServer;
  3. @PostMapping("/ai")
  4. @ApiOperation("使用ai问答")
  5. public ResultJson<String> useAI(@RequestBody AskContent askContent){
  6. log.info("使用AI问答");
  7. try{
  8. // TODO:进行AI相关的统计、管理操作
  9. //调用service层的方法
  10. userService.sendMessageToXingHuo(askContent.getQuestion(),webSocketServer.getSessionByUserId(askContent.getUserid()));
  11. return ResultJson.success(null);
  12. }catch (Exception e){
  13. redisUtil.incrby("ai:error",1);
  14. System.err.println(e.getLocalizedMessage() + e.getMessage());
  15. return ResultJson.error("AI机器人出了点错误,请稍后再试 ~");
  16. }
  17. }

Service层 

        

  1. @Override
  2. public void sendMessageToXingHuo(String question, Session session) {
  3. /*下面都是使用的SDK*/
  4. List<SparkMessage> messages = new ArrayList<>(); //消息列表
  5. //MessageConstant.PRECONDITION 是我定义的一个常量.
  6. messages.add(SparkMessage.systemContent(MessageConstant.PRECONDITION)); //预设问题
  7. messages.add(SparkMessage.userContent(question)); //设置问题
  8. //发送信息
  9. SparkRequest sparkRequest = SparkRequest.builder()
  10. .messages(messages)
  11. .maxTokens(1024) //回答的最大token
  12. .temperature(0.5) //结果随机性
  13. .apiVersion(SparkApiVersion.V3_5) //版本情况
  14. .build(); //构建
  15. //重新设置一个session(返回客户端)
  16. sparkConsoleListener.setSession(session);
  17. //封装聊天信息
  18. sparkClient.chatStream(sparkRequest,sparkConsoleListener);
  19. }

  SparkConsoleListener 

        如果想要实现AI的流式输出,必须自己写一个类,实现SparkBaseListener,用于监听星火服务器给后端的响应,同时在这里后端把响应内容返回给前端。

  1. public class SparkConsoleListener extends SparkBaseListener {
  2. private Session session = null; //请求ai的会话
  3. public void setSession(Session session){
  4. this.session =session; //设置session
  5. }
  6. //固定的代码
  7. public ObjectMapper objectMapper = new ObjectMapper();
  8. {
  9. objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
  10. }
  11. @Override
  12. public void onMessage(String content, SparkResponseUsage usage, Integer status, SparkRequest sparkRequest, SparkResponse sparkResponse, WebSocket webSocket) {
  13. if (0 == status) {
  14. List<SparkMessage> messages = sparkRequest.getPayload().getMessage().getText();
  15. try {
  16. System.out.println("提问:" + objectMapper.writeValueAsString(messages));
  17. } catch (JsonProcessingException e) {
  18. throw new RuntimeException(e);
  19. }
  20. }
  21. try {
  22. session.getBasicRemote().sendText(content); //将socket的信息返回给前端
  23. } catch(IOException e){
  24. System.err.println(e.getMessage());
  25. }
  26. if (2 == status) {
  27. SparkTextUsage textUsage = usage.getText();
  28. System.out.println("\n回答结束;提问tokens:" + textUsage.getPromptTokens()
  29. + ",回答tokens:" + textUsage.getCompletionTokens()
  30. + ",总消耗tokens:" + textUsage.getTotalTokens());
  31. try {
  32. //结束的时候发个 |
  33. session.getBasicRemote().sendText("|");
  34. } catch (IOException e) {
  35. throw new RuntimeException(e);
  36. }
  37. }
  38. }
  39. }

小结

        网上有许多简单、高效的实现方法,这里只是我个人对该问题,十分局限的见解,如有问题欢迎指正。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/菜鸟追梦旅行/article/detail/550968
推荐阅读
相关标签
  

闽ICP备14008679号