• Flutter+SpringBoot实现ChatGPT流实输出


    Flutter+SpringBoot实现ChatGPT流式输出、上下文了连续对话

    最终实现Flutter的流式输出+上下文连续对话。
    在这里插入图片描述

    这里就是提供一个简单版的工具类和使用案例,此处页面仅参考。

    服务端

    这里直接封装提供工具类,修改自己的apiKey即可使用,支持连续对话

    工具类及使用

    http依赖这里使用okHttp

        <dependency>
          <groupId>com.squareup.okhttp3groupId>
          <artifactId>okhttpartifactId>
          <version>4.9.3version>
        dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    import com.alibaba.fastjson2.JSON;
    import com.squareup.okhttp.Call;
    import com.squareup.okhttp.MediaType;
    import com.squareup.okhttp.OkHttpClient;
    import com.squareup.okhttp.Request;
    import com.squareup.okhttp.RequestBody;
    import com.squareup.okhttp.Response;
    import com.squareup.okhttp.ResponseBody;
    import lombok.AllArgsConstructor;
    import lombok.Builder;
    import lombok.Data;
    import lombok.Getter;
    import lombok.NoArgsConstructor;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.stereotype.Component;
    import org.springframework.util.StringUtils;
    import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
    import vip.ailtw.common.utils.StringUtil;
    
    
    import javax.annotation.PostConstruct;
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.io.Serializable;
    import java.util.List;
    import java.util.concurrent.TimeUnit;
    import java.util.function.Consumer;
    import java.util.regex.Matcher;
    import java.util.regex.Pattern;
    
    @Slf4j
    @Component
    public class ChatGptStreamUtil {
    
        /**
         * 修改为自己的密钥
         */
        private final String apiKey = "xxxxxxxxxxxxxx";
    
        public final String gptCompletionsUrl = "https://api.openai.com/v1/chat/completions";
    
    
        private static final OkHttpClient client = new OkHttpClient();
        private static MediaType mediaType;
        private static Request.Builder requestBuilder;
    
    
        public final static Pattern contentPattern = Pattern.compile("\"content\":\"(.*?)\"}");
        /**
         * 对话符号
         */
        public final static String EVENT_DATA = "d";
    
        /**
         * 错误结束符号
         */
        public final static String EVENT_ERROR = "e";
    
        /**
         * 响应结束符号
         */
        public final static String END = "<>";
    
    
        @PostConstruct
        public void init() {
            client.setConnectTimeout(60, TimeUnit.SECONDS);
            client.setReadTimeout(60, TimeUnit.SECONDS);
            mediaType = MediaType.parse("application/json; charset=utf-8");
            requestBuilder = new Request.Builder()
                    .url(gptCompletionsUrl)
                    .header("Content-Type", "application/json")
                    .header("Authorization", "Bearer " + apiKey);
        }
    
    
        /**
         * 流式对话
         *
         * @param talkList 上下文对话,最早的对话放在首位
         * @param callable 消费者,流式对话每次响应的内容
         */
        public GptChatResultDTO chatStream(List<ChatGptDTO> talkList, Consumer<String> callable) throws Exception {
            long start = System.currentTimeMillis();
            StringBuilder resp = new StringBuilder();
            Response response = chatStream(talkList);
            //解析对话内容
            try (ResponseBody responseBody = response.body();
                 InputStream inputStream = responseBody.byteStream();
                 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) {
                String line;
                while ((line = bufferedReader.readLine()) != null) {
                    if (!StringUtils.hasLength(line)) {
                        continue;
                    }
                    Matcher matcher = contentPattern.matcher(line);
                    if (matcher.find()) {
                        String content = matcher.group(1);
                        resp.append(content);
                        callable.accept(content);
                    }
    
                }
            }
            int wordSize = 0;
            for (ChatGptDTO dto : talkList) {
                String content = dto.getContent();
                wordSize += content.toCharArray().length;
            }
            wordSize += resp.toString().toCharArray().length;
            long end = System.currentTimeMillis();
            return GptChatResultDTO.builder().resContent(resp.toString()).time(end - start).wordSize(wordSize).build();
        }
    
        /**
         * 流式对话
         *
         * @param talkList 上下文对话
         * @return 接口请求响应
         */
        private Response chatStream(List<ChatGptDTO> talkList) throws Exception {
            ChatStreamDTO chatStreamDTO = new ChatStreamDTO(talkList);
            RequestBody bodyOk = RequestBody.create(mediaType, chatStreamDTO.toString());
            Request requestOk = requestBuilder.post(bodyOk).build();
            Call call = client.newCall(requestOk);
            Response response;
            try {
                response = call.execute();
            } catch (IOException e) {
                throw new IOException("请求时IO异常: " + e.getMessage());
            }
            if (response.isSuccessful()) {
                return response;
            }
            try (ResponseBody body = response.body()) {
                if (429 == response.code()) {
                    String msg = "Open Api key 已过期,msg: " + body.string();
                    log.error(msg);
                }
                throw new RuntimeException("chat api 请求异常, code: " + response.code() + "body: " + body.string());
            }
        }
    
    
        private boolean sendToClient(String event, String data, SseEmitter emitter) {
            try {
                emitter.send(SseEmitter.event().name(event).data("{" + data + "}"));
                return true;
            } catch (IOException e) {
                log.error("向客户端发送消息时出现异常", e);
            }
            return false;
        }
    
        /**
         * 发送事件给客户端
         */
        public boolean sendData(String data, SseEmitter emitter) {
            if (StringUtil.isBlank(data)) {
                return true;
            }
            return sendToClient(EVENT_DATA, data, emitter);
        }
    
        /**
         * 发送结束事件,会关闭emitter
         */
        public void sendEnd(SseEmitter emitter) {
            try {
                sendToClient(EVENT_DATA, END, emitter);
            } finally {
                emitter.complete();
            }
        }
    
    
        /**
         * 发送异常事件,会关闭emitter
         */
        public void sendError(SseEmitter emitter) {
            try {
                sendToClient(EVENT_ERROR, "我累垮了", emitter);
            } finally {
                emitter.complete();
            }
        }
    
    
        /**
         * gpt请求结果
         */
        @Data
        @NoArgsConstructor
        @AllArgsConstructor
        @Builder
        public static class GptChatResultDTO implements Serializable {
            /**
             * gpt请求返回的全部内容
             */
            private String resContent;
    
            /**
             * 上下文消耗的字数
             */
            private int wordSize;
    
            /**
             * 耗时
             */
            private long time;
        }
    
    
        /**
         * 连续对话DTO
         */
        @Data
        @Builder
        @NoArgsConstructor
        @AllArgsConstructor
        public static class ChatGptDTO implements Serializable {
            /**
             * 对话内容
             */
            private String content;
    
            /**
             * 角色 {@link GptRoleEnum}
             */
            private String role;
        }
    
    
        /**
         * gpt连续对话角色
         */
        @Getter
        public static enum GptRoleEnum {
            USER_ROLE("user", "用户"),
            GPT_ROLE("assistant", "ChatGPT本身"),
    
            /**
             * message里role为system,是为了让ChatGPT在对话过程中设定自己的行为
             * 可以理解为对话的设定,如你是谁,要什么语气、等级
             */
            SYSTEM_ROLE("system", "对话设定"),
    
            ;
    
            private final String value;
            private final String desc;
    
            GptRoleEnum(String value, String desc) {
                this.value = value;
                this.desc = desc;
            }
        }
    
    
        /**
         * gpt请求body
         */
        @Data
        public static class ChatStreamDTO {
            private static final String model = "gpt-3.5-turbo";
            private static final boolean stream = true;
            private List<ChatGptDTO> messages;
    
    
            public ChatStreamDTO(List<ChatGptDTO> messages) {
                this.messages = messages;
            }
    
            @Override
            public String toString() {
                return "{\"model\":\"" + model + "\"," +
                        "\"messages\":" + JSON.toJSONString(messages) + "," +
                        "\"stream\":" + stream + "}";
            }
        }
    
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
    • 247
    • 248
    • 249
    • 250
    • 251
    • 252
    • 253
    • 254
    • 255
    • 256
    • 257
    • 258
    • 259
    • 260
    • 261
    • 262
    • 263
    • 264
    • 265
    • 266
    • 267
    • 268
    • 269
    • 270
    • 271
    • 272
    • 273
    • 274
    • 275
    • 276
    • 277
    • 278
    • 279
    • 280
    • 281
    • 282
    • 283
    • 284
    • 285
    • 286

    使用案例:

        public static void main(String[] args) throws Exception {
            ChatGptStreamUtil chatGptStreamUtil = new ChatGptStreamUtil();
            chatGptStreamUtil.init();
    
            //构建一个上下文对话情景
            List<ChatGptDTO> talkList = new ArrayList<>();
            //设定gpt
            talkList.add(ChatGptDTO.builder().content("你是chatgpt助手,能过帮助我查阅资料,编写教学报告。").role(GptRoleEnum.GPT_ROLE.getValue()).build());
            //开始提问
            talkList.add(ChatGptDTO.builder().content("请帮我写一篇小学数学加法运算教案").role(GptRoleEnum.USER_ROLE.getValue()).build());
            chatGptStreamUtil.chatStream(talkList, (respContent) -> {
                //这里是gpt每次流式返回的内容
                System.out.println("gpt返回:" + respContent);
            });
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    SpringBoot接口

    基于SpringBoot工程,提供接口,供Flutter端使用。

    通过上面的工具类的使用,可以知道gpt返回给我们的内容是一段一段的,因此如果我们服务端也要提供类似的效果,提供两个思路和实现:

    • WebSocket,服务端接收gpt返回的内容时推送内容给flutter
    • 使用Http长链接,也就是 SseEmitter,这里也是采用这种方式。

    代码:

    @RestController
    @RequestMapping("/chat")
    @Slf4j
    public class ChatController {
        @Autowired
        private ChatGptStreamUtil chatGptStreamUtil;
      
        @PostMapping(value = "/chatStream")
        @ApiOperation("流式对话")
        public SseEmitter chatStream() {
            SseEmitter emitter = new SseEmitter(80000L);
          
            //构建一个上下文对话情景
            List<ChatGptDTO> talkList = new ArrayList<>();
            //设定gpt
            talkList.add(ChatGptDTO.builder().content("你是chatgpt助手,能过帮助我查阅资料,编写教学报告。").role(GptRoleEnum.GPT_ROLE.getValue()).build());
            //开始提问
            talkList.add(ChatGptDTO.builder().content("请帮我写一篇小学数学加法运算教案").role(GptRoleEnum.USER_ROLE.getValue()).build());
            GptChatResultDTO gptChatResultDTO = chatGptStreamUtil.chatStream(talkList, (content) -> {
              //这里服务端接收到消息就发送给Flutter
                   chatGptStreamUtil.sendData(content, emitter);
                });
            return emitter;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    Flutter端

    这里使用dio作为网络请求的工具

    依赖

    	dio: ^5.2.1+1
    
    • 1

    工具类

    import 'dart:async';
    import 'dart:convert';
    
    import 'package:dio/dio.dart';
    import 'package:flutter/cupertino.dart';
    import 'package:flutter/foundation.dart';
    import 'package:get/get.dart' hide Response;
    
    ///http工具类
    class HttpUtil {
      Dio? client;
    
      static HttpUtil of() {
        return HttpUtil.init();
      }
    
      //初始化http工具
      HttpUtil.init() {
        if (client == null) {
          var options = BaseOptions(
              baseUrl: Config.baseUrl,
              connectTimeout: const Duration(seconds: 100),
              receiveTimeout: const Duration(seconds: 100));
          client = Dio(options);
          // 请求与响应拦截器/异常拦截器
          client?.interceptors.add(OnReqResInterceptors());
        }
      }
    
      Future<Stream<String>?> postStream(String path,
          [Map<String, dynamic>? params]) async {
        Response<ResponseBody> rs =
        await Dio().post<ResponseBody>(Config.baseUrl + path,
            options: Options(headers: {
              "Accept": "text/event-stream",
              "Cache-Control": "no-cache"
            }, responseType: ResponseType.stream),
            data: params 
        );
        StreamTransformer<Uint8List, List<int>> unit8Transformer =
        StreamTransformer.fromHandlers(
          handleData: (data, sink) {
            sink.add(List<int>.from(data));
          },
        );
        var resp = rs.data?.stream
            .transform(unit8Transformer)
            .transform(const Utf8Decoder())
            .transform(const LineSplitter());
        return resp;
      }
    
    
    
    /// Dio 请求与响应拦截器
    class OnReqResInterceptors extends InterceptorsWrapper {
      
      Future<void> onRequest(
          RequestOptions options, RequestInterceptorHandler handler) async {
        //统一添加token
        var headers = options.headers;
        headers['Authorization'] = '请求头token';
        return super.onRequest(options, handler);
      }
    
      
      void onError(DioError err, ErrorInterceptorHandler handler) {
        if (err.type == DioErrorType.unknown) {
          // 网络不可用,请稍后再试
        }
        return super.onError(err, handler);
      }
    
      
      void onResponse(
          Response<dynamic> response, ResponseInterceptorHandler handler) {
        Response res = response;
        return super.onResponse(res, handler);
      }
    }
    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83

    使用

      //构建文章、流式对话
      chatStream() async {
        final stream = await HttpUtil.of().postStream("/api/chat/chatStream");
        String respContent = "";
        stream?.listen((content) {
          debugPrint(content);
          if (content != '' && content.contains("data:")) {
            //解析数据
            var start = content.indexOf("{") + 1;
            var end = content.indexOf("}");
            var substring = content.substring(start, end);
            content = substring;
            respContent += content;
            print("返回的内容:$content");
          }
        });
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
  • 相关阅读:
    【项目】数据库事务与MQ发送一致性
    【915程序设计】16西交大软件专硕915程序设计真题讲解
    jvm08
    stable-diffusion-webui安装与使用过程中的遇到的error合集
    宁波财经学院程序设计补修单链表
    [附源码]SSM计算机毕业设计疫情状态下病房管理平台JAVA
    【力扣周赛】第 361 场周赛(⭐前缀和+哈希表 & 树上倍增、LCA⭐)
    Kylin Cube设计革新:维度自动合并的智能策略
    高级深入--day45
    2022.11.2 英语背诵
  • 原文地址:https://blog.csdn.net/wq2323/article/details/133523990