Flutter+SpringBoot实现ChatGPT流实输出
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>>";@PostConstructpublic 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@Builderpublic static class GptChatResultDTO implements Serializable {/*** gpt请求返回的全部内容*/private String resContent;/*** 上下文消耗的字数*/private int wordSize;/*** 耗时*/private long time;}/*** 连续对话DTO*/@Data@Builder@NoArgsConstructor@AllArgsConstructorpublic static class ChatGptDTO implements Serializable {/*** 对话内容*/private String content;/*** 角色 {@link GptRoleEnum}*/private String role;}/*** gpt连续对话角色*/@Getterpublic 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*/@Datapublic 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;}@Overridepublic 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<>();//设定gpttalkList.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 {@Autowiredprivate ChatGptStreamUtil chatGptStreamUtil;@PostMapping(value = "/chatStream")@ApiOperation("流式对话")public SseEmitter chatStream() {SseEmitter emitter = new SseEmitter(80000L);//构建一个上下文对话情景List<ChatGptDTO> talkList = new ArrayList<>();//设定gpttalkList.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) -> {//这里服务端接收到消息就发送给FlutterchatGptStreamUtil.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 {//统一添加tokenvar 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");}});}
相关文章:
Flutter+SpringBoot实现ChatGPT流实输出
FlutterSpringBoot实现ChatGPT流式输出、上下文了连续对话 最终实现Flutter的流式输出上下文连续对话。 这里就是提供一个简单版的工具类和使用案例,此处页面仅参考。 服务端 这里直接封装提供工具类,修改自己的apiKey即可使用,支持连续…...
淘宝天猫粉丝福利购店铺优惠券去哪里找到领取网站?
淘宝天猫优惠券去哪里找到领取网站? 领取淘宝天猫粉丝福利购优惠券可通过百度搜索:草柴,进入草柴官方网站 或 手机应用商店搜索:草柴,下载安装草柴APP,就可以领取淘宝天猫优惠券; 草柴APP如何领…...
【考研复习】union有关的输出问题
文章目录 遇到的问题正确解答拓展参考文章 遇到的问题 首次遇到下面的代码时,感觉应该输出65,323。深入理解union的存储之后发现正确答案是:67,323. union {char c;int i; } u; int main(){u.c A;u.i 0x143;printf("%d,%d\n", u.c, u.i); …...
Android学习之路(16) Android 数据库Litepal
一.LitePal的介绍 Litepal是Android郭霖大神的一个开源Android数据库的开源框架,它采用了对象关系映射(ORM)的模式,这是让我们非常好的理解的数据库,一个实体类对应我们数据库中的一个表。该库中还封装了许多的方法&a…...
Redis持久化(RDB/AOF)
"在哪里走散,你都会 找 到 我。" 认识持久化 我们在接触Mysql事务的时候,一定了解过Mysql事务的四个特性: "原子性(A)一致性(C)隔离性(I)持久性(D)" 而其中持久性其实与持久化是一回事,所谓持久与不持久&#x…...
小谈设计模式(15)—观察者模式
小谈设计模式(15)—观察者模式 专栏介绍专栏地址专栏介绍 观察者模式核心思想主要角色Subject(被观察者)ConcreteSubject(具体被观察者)Observer(观察者)ConcreteObserver࿰…...
简单工厂模式 创建型模式(非GoF经典设计模式)
简单工厂模式是属于创建型模式,也因为工厂中的方法一般设置为静态,又叫做静态工厂方法(Static Factory Method)模式,但不属于23种GOF设计模式之一。简单工厂模式是由一个工厂对象决定创建出哪一种产品类的实例。简单工…...
PE文件之导入表
1. 导入表 2. 显示导入表信息的例子 ; 作用: 将RVA地址转成FOA即文件偏移 ; 参数: _pFileHdr 指向读到内存中文件的基址指针 ; _dwRVA 目标RVA地址 ; 返回: 目标RVA转成文件偏移的值 RVA2FOA PROC USES esi edi edx, _pFileHdr:PTR BYTE, _dwRVA:DWORDmov esi, _pFil…...
二、码制及其转换
原码 根据我们所学可知,数字电路的逻辑电路是通过输出0和1来表示二进制数的,那么这个二进制数的正负又该怎么表示呢? 答案是在这个二进制数的最高位作为符号位来表示正负性,用0正数,用1表示负数,在这种表达…...
在pycharm中出现下载软件包失败的解决方法
一. 一般情况下我们会选择在设置中下载软件包,过程如下. 1. 直接点击左上角的文件, 再点击设置, 再点击项目, 在右边选择python解释器,点击号,输入要下载的软件包, 在下面的一系列的包中选择相对应的包,点击安装就可以了,有的时候我们下载的是最新的版本,如果要下载固定的版本…...
10.0 探索API调试事件原理
本章笔者将通过Windows平台下自带的调试API接口实现对特定进程的动态转存功能,首先简单介绍一下关于调试事件的相关信息,调试事件的建立需要依赖于DEBUG_EVENT这个特有的数据结构,该结构用于向调试器报告调试事件。当一个程序发生异常事件或者…...
文心一言 VS 讯飞星火 VS chatgpt (104)-- 算法导论10.1 2题
二、用go语言,说明如何在一个数组 A[1…n]中实现两个栈,使得当两个栈的元素个数之和不为 n 时,两者都不会发生上溢。要求 PUSH 和 POP 操作的运行时间为 O(1)。 文心一言: 在这个问题中,我们要在一个数组 A[1…n] 中…...
检测防火墙是否开启、判断程序是否加入防火墙白名单(附源码)
VC常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/124272585C软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...&a…...
vtk 动画入门 1 代码
实现效果如图: #include <vtkAutoInit.h> //VTK_MODULE_INIT(vtkRenderingOpenGL2); //VTK_MODULE_INIT(vtkInteractionStyle); VTK_MODULE_INIT(vtkRenderingOpenGL2); VTK_MODULE_INIT(vtkInteractionStyle); //VTK_MODULE_INIT(vtkRenderingFreeType); #in…...
【VR】【unity】如何在VR中实现远程投屏功能?
【背景】 目前主流的VD应用,用于娱乐很棒,但是用于工作还是无法效率地操作键鼠。用虚拟键盘工作则显然是不现实的。为了让自己的头显能够起到小面积代替多显示屏的作用,自己动手开发投屏VR应用。 【思路】 先实现C#的投屏应用。研究如何将C#投屏应用用Unity 3D项目转写。…...
OpenGl材质
在现实世界里,每个物体会对光产生不同的反应。比如,钢制物体看起来通常会比陶土花瓶更闪闪发光,一个木头箱子也不会与一个钢制箱子反射同样程度的光。有些物体反射光的时候不会有太多的散射(Scatter),因而产生较小的高光点,而有些物体则会散射很多,产生一个有着更大半径的…...
背包问题
目录 开端 01背包问题 AcWing 01背包问题 Luogu P2925干草出售 Luogu P1048采药 完全背包问题 AcWing 完全背包问题 Luogu P1853投资的最大效益 多重背包问题 AcWing 多重背包问题 I AcWing 多重背包问题 II Luogu P1776宝物筛选 混合背包问题 AcWing 混合背包问题…...
JavaSE | 初始Java(十一) | 抽象类和抽象接口
抽象类概念 在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的, 如果 一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类 在 Java 中,一个…...
产品经理如何科学的进行需求调研?
导语:作为产品经理,需求调研是开展工作的重要环节之一。科学、有效地进行需求调研不仅可以帮助产品经理更好地了解用户需求,还能指导产品设计和功能开发,提升产品的竞争力。本文将介绍几种科学的方法和技巧,帮助产品经…...
AI智能问答系统源码/AI绘画商业系统/支持GPT联网提问/支持Midjourney绘画
一、AI创作系统 SparkAi创作系统是基于国外很火的ChatGPT进行开发的AI智能问答系统和AI绘画系统。本期针对源码系统整体测试下来非常完美,可以说SparkAi是目前国内一款的ChatGPT对接OpenAI软件系统。那么如何搭建部署AI创作ChatGPT?小编这里写一个详细图…...
玩具玩偶配送经营商城小程序的作用是什么?
玩具玩偶是小孩子们喜欢的产品,其市场需求度很高,以前玩具店里总是不缺乏客户,但现在随着人们生活品牌提升及消费形式改变,无论玩具厂商还是门店经销商都面对着不少痛点: 如拓客引流难、线上销售经营难、营销难、分销…...
latex表格内容换行
问题描述: 在用latex表格中编写公式时,可能出现公式太长,表格中后面的内容不能在文档中呈现,如下图1,故要进行行内内容的换行,使内容呈现完全而传统的\换行后,换行内容会顶格,如图2。 解决方…...
2023 牛客国庆day4 【10.2训练补题】
目录 B-Basic Gcd Problem(素数筛快速幂) H-Harder Gcd Problem(素数) B-Basic Gcd Problem(素数筛快速幂) 打表找规律发现答案为 (n质因子数目)^c #include<bits/stdc.h> using namespace std;…...
android的USB开发时 mUsbManager.getDeviceList()获取都为空
类提供的主要方法有: getDeviceList() 获得设备列表,返回的是一个HashMap.;hasPermission(UsbDevice device) 判断你的应用程序是否有接入此USB设备的权限,如果有则返回真,否则返回false.openDevice(UsbDevice device) 打开USB设…...
SpringCloud Alibaba - Seata 部署 TC 服务,并集成微服务
目录 一、Seata 架构 1.1、Seata 架构重要角色 1.2、部署 TC 服务 1.2.1、前言 1.2.2、下载 seata-server 包,解压 1.2.3、修改配置 1.2.4、在 nacos 中添加配置 1.2.5、创建数据库表 1.2.6、启动 TC 服务 1.3、微服务集成 Seata 1.3.1、引入依赖 1.3.2、…...
Java基础面试,接口和抽象类的区别?
接口和抽象类的区别? 抽象类可以存在普通成员函数,而接口中只能存在public abstract 方法。抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的.抽象类只能继承一个,接口可以实现多个。 接…...
《视觉 SLAM 十四讲》V2 第 4 讲 李群与李代数 【什么样的相机位姿 最符合 当前观测数据】
P71 文章目录 4.1 李群与李代数基础4.1.3 李代数的定义4.1.4 李代数 so(3)4.1.5 李代数 se(3) 4.2 指数与对数映射4.2.1 SO(3)上的指数映射罗德里格斯公式推导 4.2.2 SE(3) 上的指数映射SO(3),SE(3),so(3),se(3)的对应关系 4.3 李代数求导与扰动模型4.3.2 SO(3)上的李代数求导…...
【深蓝学院】手写VIO第4章--基于滑动窗口算法的 VIO 系统:可观性和 一致性--笔记
0. 内容 T1. 参考SLAM14讲P247直接可写,注意 ξ 1 , ξ 2 \xi_1,\xi_2 ξ1,ξ2之间有约束(关系)。 套用舒尔补公式: marg掉 ξ 1 \xi_1 ξ1之后,信息被传递到 L 1 和 L 2 L_1和L_2 L1和L2之间了。 T2....
mfc 动态加载dll库,Mat转CImage,读ini配置文件,鼠标操作,在edit控件上画框,调试信息打印
动态加载dll库 h文件中添加 #include "mydll.h" #ifdef UNICODE //区分字符集 #define LoadLibrary LoadLibraryW #else #define LoadLibrary LoadLibraryA #endif // !UNICODEtypedef double(*mydllPtr)(int, int);类内添加: mydllPtr m_mydll; cpp…...
索尼 toio™应用创意开发征文|检测工业平台震动
虽然索尼toio Q宝机器人主要是为儿童教育娱乐开发的,但我认为它在工业等领域也有一定应用潜力。例如,工业领域经常会有某些平面在实际作业中持续震动,导致零件过疲劳、平台失去稳定等问题。而这样的平台往往位于机器内部,从外部很…...
东莞建站模板代理/怎么建立网站
1.安装MySQL (免费) 官网现下载地址 http://dev.mysql.com/downloads/mysql/ (我选的mysql-5.7.17-macos10.12-x86_64.dmg) 点击download 会跳转到另外一个界面,这个界面是提示你需不需要注册的,直接选…...
wordpress开启raid/广告联盟有哪些平台
载着工匠精神和服务意识上路,企业才能驶向更光明的未来自行车为什么不会倒?这是一个隔一段时间就会有人问起的有趣问题。德国明斯特大学的一位教授,最近在一期德国广播节目中给出了解释。他将学骑自行车和幼年学步的平衡性进行对比࿰…...
我想学做网站/重庆排名优化整站优化
通过对全球2944位首席信息官的年度调查,全球领先的信息技术研究和顾问公司Gartner发现人们现已深处数字商业时代,许多企业正根据数字化能力重构业务与运营模式。本文探讨了2016年中国首席信息官议程的三项主要调查结果,此次调查包含来自中国8…...
网站在百度找不到了/免费seo教程分享
1.AppMon工作原理 AppMon使用了多平台动态框架环境Frida,Frida是一款基于Python JavasSript 的hook框架,适应android\ios\linux\win\osx等平台的脚本交互环境。AppMon还包括了一系列app事件监控和行为修改脚本,并能通过web接口显示和操作。 …...
销售网站开发与设计现状/前端seo是什么意思
全局变量什么是全局变量?全局变量就好比一个容器或者一个公用的东西一样,就类似外面公共场所的凳子一样,大家都可以使用这个凳子。和他相反的局部变量是啥子东东呢?局部变量就是局部的东西,如果全局变量是桌子…...
网站列表页是啥/搜索引擎大全全搜网
目录:导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结(尾部小惊喜)前言 对程序猿来说改bug可…...