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

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

FPGA project : TFT_LCD

实验目标&#xff1a; 驱动TFT_LCD显示十色彩条。 重点掌握的知识&#xff1a; 1&#xff0c;液晶显示器&#xff0c;简称LCD(Liquid Crystal Display)&#xff0c;相对于上一代CRT显示器(阴极射线管显示器)&#xff0c;LCD显示器具有功耗低、体积小、承载的信息量大及不伤眼…

vtk 动画入门 1 代码

实现效果如图&#xff1a; #include <vtkAutoInit.h> //VTK_MODULE_INIT(vtkRenderingOpenGL2); //VTK_MODULE_INIT(vtkInteractionStyle); VTK_MODULE_INIT(vtkRenderingOpenGL2); VTK_MODULE_INIT(vtkInteractionStyle); //VTK_MODULE_INIT(vtkRenderingFreeType); #in…

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

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

postgresql-备份与恢复

postgresql-备份与恢复 基本概念备份类型物理备份与逻辑备份在线备份与离线备份全量备份与增量备份 备份恢复工具备份与恢复逻辑备份与还原备份单个数据库psqlpg_dumppg_store 备份整个集群 基本概念 服务器系统错误、硬件故障或者人为失误都可能导致数据的丢失或损坏。因此&am…

小谈设计模式(15)—观察者模式

小谈设计模式&#xff08;15&#xff09;—观察者模式 专栏介绍专栏地址专栏介绍 观察者模式核心思想主要角色Subject&#xff08;被观察者&#xff09;ConcreteSubject&#xff08;具体被观察者&#xff09;Observer&#xff08;观察者&#xff09;ConcreteObserver&#xff0…

创建型设计模式 原型模式 建造者模式 创建者模式对比

创建型设计模式 单例 工厂模式 看这一篇就够了_软工菜鸡的博客-CSDN博客 4.3 原型模式 4.3.1 概述 用一个已经创建的实例作为原型&#xff0c;通过复制该原型对象来创建一个和原型对象相同的新对象。 4.3.2 结构 原型模式包含如下角色&#xff1a; 抽象原型类&#xff1a;规定了…

趋势列表上又多了两个漏洞!

CVE-2023-24955 和 CVE-2023-29360 来自微软产品 5 月和 6 月的安全补丁报告。它们之所以特别危险&#xff0c;是因为出现了公开漏洞利用。 以下是详细信息。 第一个漏洞 CVE-2023-24955存在于 Microsoft SharePoint Server 中。它可导致远程代码执行。 它与覆盖随后由服务器执…

MeterSphere v2.10.X-lts 双节点HA部署方案

一、MeterSphere高可用部署架构及服务器配置 1.1 服务器信息 序号应用名称操作系统要求配置要求描述1负载均衡器CentOS 7.X /RedHat 7.X2C,4G&#xff0c;200GB部署Nginx&#xff0c;实现负载路由。 部署NFS服务器。2MeterSphere应用节点1CentOS 7.X /RedHat 7.X8C,16GB,200G…

Scala第十一章节

Scala第十一章节 1.模式匹配 2. Option 类型 3.偏函数 4.正则表达式 5.异常处理 6.提取器 7.案例&#xff1a;随机职业 scala总目录 文档资料下载

《计算机视觉中的多视图几何》笔记(12)

12 Structure Computation 本章讲述如何在已知基本矩阵 F F F和两幅图像中若干对对应点 x ↔ x ′ x \leftrightarrow x x↔x′的情况下计算三维空间点 X X X的位置。 文章目录 12 Structure Computation12.1 Problem statement12.2 Linear triangulation methods12.3 Geomet…

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

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

匿名上位机V7波形显示教程-简单能用

匿名上位机V7波形显示教程-简单能用 匿名上位机V7下位机数据格式根据匿名上位机V7的手册说明文档&#xff0c;编写对应的指令在主函数中初始化ANDmessage驱动连接匿名上位机V7 匿名上位机V7下位机数据格式 DATA区域内容&#xff1a; 举例说明DATA区域格式&#xff0c;例如上文&…