Flutter+SpringBoot实现ChatGPT流实输出

news2025/1/13 15:33:05

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

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

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

服务端

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

工具类及使用

http依赖这里使用okHttp

    <dependency>
      <groupId>com.squareup.okhttp3</groupId>
      <artifactId>okhttp</artifactId>
      <version>4.9.3</version>
    </dependency>
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 = "<<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 + "}";
        }
    }


}

使用案例:

    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);
        });
    }

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;
    }

}

Flutter端

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

依赖

	dio: ^5.2.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);
  }
}



使用

  //构建文章、流式对话
  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");
      }
    });
  }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1058022.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

FOC程序cubemx配置-ADC配置

具体配置步骤大家参考&#xff1a;这篇文章 我配置后用keil5自带的仿真工具查看引脚波形&#xff0c;在这里写一下遇到的问题。 1、波形仿真的时候出现 Unknown Signal&#xff1a;参考 这篇文章 2、生成的波形并不完全互补。 PS&#xff1a;出现以上这种情况时&#xff0…

【斗罗大陆2】动画新增12集备案,冰碧帝皇蝎形象被吐槽遭狂喷!

Hello,小伙伴们&#xff0c;我是小郑继续为大家深度解析斗罗大陆2绝世唐门。 《斗罗大陆2》动画新增12集备案 《斗罗大陆2》动画正在如火如荼的上映着&#xff0c;《斗罗大陆2》动画也在同步新增了。 在2023年9月全国重点网络动画片规划备案通过剧目信息中&#xff0c;《斗罗大…

【计算机网络】高级IO之select

文章目录 1. 什么是IO&#xff1f;什么是高效 IO? 2. IO的五种模型五种IO模型的概念理解同步IO与异步IO整体理解 3. 阻塞IO4. 非阻塞IOsetnonblock函数为什么非阻塞IO会读取错误&#xff1f;对错误码的进一步判断检测数据没有就绪时&#xff0c;返回做一些其他事情完整代码myt…

【算法|动态规划No.8】leetcode面试题 17.16. 按摩师

个人主页&#xff1a;兜里有颗棉花糖 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 兜里有颗棉花糖 原创 收录于专栏【手撕算法系列专栏】【LeetCode】 &#x1f354;本专栏旨在提高自己算法能力的同时&#xff0c;记录一下自己的学习过程&#xff0c;希望…

Python使用词云图展示

网上看到一个txt文本信息&#xff0c;共2351条饭否记录&#xff0c;据说是微信之父每天发的饭否记录&#xff0c;其实我不知道什么是饭否。我读取这个文本内容&#xff0c;展示到词语图上。之前也使用过&#xff0c;但是好久没有玩Python了&#xff0c;称假期空闲&#xff0c;练…

【从入门到起飞】IO高级流(1)(缓冲流,转换流,序列化流,反序列化流)

&#x1f38a;专栏【JavaSE】 &#x1f354;喜欢的诗句&#xff1a;天行健&#xff0c;君子以自强不息。 &#x1f386;音乐分享【如愿】 &#x1f384;欢迎并且感谢大家指出小吉的问题&#x1f970; 文章目录 &#x1f384;缓冲流&#x1f354;字节缓冲流&#x1f6f8;一次读取…

vue ant 隐藏列

vue ant 隐藏列 重要代码 type: FormTypes.hidden{ title: 序号, key: barCode, width: 10%, type: FormTypes.hidden},

YTM32的电源管理与低功耗系统详解

YTM32的电源管理与低功耗系统详解 苏勇&#xff0c;2023年10月 文章目录 YTM32的电源管理与低功耗系统详解缘起原理与机制电源管理模型的功耗模式正常模式&#xff08;Normal&#xff09;休眠模式&#xff08;Sleep&#xff09;深度休眠模式&#xff08;DeepSleep&#xff09;…

树概念及结构

.1树的概念 树是一种非线性的数据结构&#xff0c;它是由n&#xff08;n>0&#xff09;个有限结点组成一个具有层次关系的集合。把它叫做树是因 为它看起来像一棵倒挂的树&#xff0c;也就是说它是根朝上&#xff0c;而叶朝下的。 有一个特殊的结点&#xff0c;称为根结点&a…

新手学习笔记-----编译和链接

目录 1. 翻译环境和运⾏环境 2. 翻译环境&#xff1a;预编译编译汇编链接 2.1 预处理 2.2 编译 2.2.1 词法分析 2.2.2 语法分析 2.2.3 语义分析 2.3 汇编 2.4 链接 3. 运⾏环境 1. 翻译环境和运⾏环境 在ANSI C的任何⼀种实现中&#xff0c;存在两个不同的环境。 第…

【Leetcode】 131. 分割回文串

给你一个字符串 s&#xff0c;请你将 s 分割成一些子串&#xff0c;使每个子串都是 回文串 。返回 s 所有可能的分割方案。 回文串 是正着读和反着读都一样的字符串。 示例 1&#xff1a; 输入&#xff1a;s "aab" 输出&#xff1a;[["a","a"…

华为云云耀云服务器L实例评测|Huawei Cloud EulerOS 自动化环境部署

[toc] Huawei Cloud EulerOS 自动化环境部署 云耀云服务器L实例【Huawei Cloud EulerOS 2.0 64bit】 Python Git Google Chrome Chromedriver Selenium More… 1. Python 镜像创建后自带。 2.Git 拉取项目。 sudo yum install git3. Google Chrome 使用root权限或sudo权…

WinFroms基于三层构造设计模式的框架所设计的代码生成器1.0

软件开发模式——三层架构 此文章需要在读懂 以上这篇架构模式的基础上再继续往下深入学习简化 目录 1.前言 2.框架准备 3 .coboBox的数据绑定 4.创建文件夹 5.工具方法 6.生成一个数据库访问助手类SqlHelper 7.生成模型层Model 8.生成数据访问层DAL层 9.生成业务…

VD6283TX环境光传感器驱动开发(3)----测试闪烁频率代码

VD6283TX环境光传感器驱动开发----3.测试闪烁频率代码 概述视频教学样品申请源码下载参考代码开发板设置测试结果 概述 ST提供了6283A1_AnalogFlicker代码在X-NUCLEO-6283A1获取闪烁频率&#xff0c;同时移植到VD6283TX-SATEL。 闪烁频率提取主要用于检测光源的闪烁频率&#…

在pycharm中出现下载软件包失败的解决方法

一. 一般情况下我们会选择在设置中下载软件包,过程如下. 1. 直接点击左上角的文件, 再点击设置, 再点击项目, 在右边选择python解释器,点击号,输入要下载的软件包, 在下面的一系列的包中选择相对应的包,点击安装就可以了,有的时候我们下载的是最新的版本,如果要下载固定的版本…

数据结构与算法——19.红黑树

这篇文章我们来讲一下红黑树。 目录 1.概述 1.1红黑树的性质 2.红黑树的实现 3.总结 1.概述 首先&#xff0c;我们来大致了解一下什么是红黑树 红黑树是一种自平衡的二叉查找树&#xff0c;是一种高效的查找树。红黑树具有良好的效率&#xff0c;它可在 O(logN) 时间内完…

爬虫HTTP代理:获取多种类型数据的神器

爬虫HTTP代理是一种常用的数据采集工具&#xff0c;它可以帮助用户获取各种类型的数据。以下是爬虫HTTP代理能获取的一些常见数据&#xff1a; 网页数据 爬虫HTTP代理最常用的功能就是获取网页数据。通过代理服务器&#xff0c;用户可以获取到被封锁或限制访问的网站数据&…

pytorch之nn.Conv1d详解

自然语言处理中一个句子序列&#xff0c;一维的&#xff0c;所以使用Conv1d

Linux--socket编程

socket套接字编程 一、服务器和客户端的开发步骤&#xff1a; 1、创建套接字 2、为套接字添加信息&#xff08;ip地址和端口号&#xff09; 3、监听网络连接 4、监听到有客户端接入&#xff0c;接受连接&#xff08;如没有接入&#xff0c;会发生阻塞到&#xff09; 5、数据…

【题库】咸鱼之王答题挑战题库大全

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhang.cn] [{name: , value: 未匹配到内容},# 第一期{name: 《三国演义》中&#xff0c;「大意失街亭」的是马谩&#xff1f;, value: 对},{name: 《三国演义》中&#xff0c;「挥泪斩马谩」的是孙权&#xff1f;, value: 错…