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?小编这里写一个详细图…...

IDEA运行Tomcat出现乱码问题解决汇总
最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…...
Vue记事本应用实现教程
文章目录 1. 项目介绍2. 开发环境准备3. 设计应用界面4. 创建Vue实例和数据模型5. 实现记事本功能5.1 添加新记事项5.2 删除记事项5.3 清空所有记事 6. 添加样式7. 功能扩展:显示创建时间8. 功能扩展:记事项搜索9. 完整代码10. Vue知识点解析10.1 数据绑…...

微软PowerBI考试 PL300-选择 Power BI 模型框架【附练习数据】
微软PowerBI考试 PL300-选择 Power BI 模型框架 20 多年来,Microsoft 持续对企业商业智能 (BI) 进行大量投资。 Azure Analysis Services (AAS) 和 SQL Server Analysis Services (SSAS) 基于无数企业使用的成熟的 BI 数据建模技术。 同样的技术也是 Power BI 数据…...

对WWDC 2025 Keynote 内容的预测
借助我们以往对苹果公司发展路径的深入研究经验,以及大语言模型的分析能力,我们系统梳理了多年来苹果 WWDC 主题演讲的规律。在 WWDC 2025 即将揭幕之际,我们让 ChatGPT 对今年的 Keynote 内容进行了一个初步预测,聊作存档。等到明…...

ServerTrust 并非唯一
NSURLAuthenticationMethodServerTrust 只是 authenticationMethod 的冰山一角 要理解 NSURLAuthenticationMethodServerTrust, 首先要明白它只是 authenticationMethod 的选项之一, 并非唯一 1 先厘清概念 点说明authenticationMethodURLAuthenticationChallenge.protectionS…...
高防服务器能够抵御哪些网络攻击呢?
高防服务器作为一种有着高度防御能力的服务器,可以帮助网站应对分布式拒绝服务攻击,有效识别和清理一些恶意的网络流量,为用户提供安全且稳定的网络环境,那么,高防服务器一般都可以抵御哪些网络攻击呢?下面…...

【论文阅读28】-CNN-BiLSTM-Attention-(2024)
本文把滑坡位移序列拆开、筛优质因子,再用 CNN-BiLSTM-Attention 来动态预测每个子序列,最后重构出总位移,预测效果超越传统模型。 文章目录 1 引言2 方法2.1 位移时间序列加性模型2.2 变分模态分解 (VMD) 具体步骤2.3.1 样本熵(S…...

短视频矩阵系统文案创作功能开发实践,定制化开发
在短视频行业迅猛发展的当下,企业和个人创作者为了扩大影响力、提升传播效果,纷纷采用短视频矩阵运营策略,同时管理多个平台、多个账号的内容发布。然而,频繁的文案创作需求让运营者疲于应对,如何高效产出高质量文案成…...
LangChain知识库管理后端接口:数据库操作详解—— 构建本地知识库系统的基础《二》
这段 Python 代码是一个完整的 知识库数据库操作模块,用于对本地知识库系统中的知识库进行增删改查(CRUD)操作。它基于 SQLAlchemy ORM 框架 和一个自定义的装饰器 with_session 实现数据库会话管理。 📘 一、整体功能概述 该模块…...

RSS 2025|从说明书学习复杂机器人操作任务:NUS邵林团队提出全新机器人装配技能学习框架Manual2Skill
视觉语言模型(Vision-Language Models, VLMs),为真实环境中的机器人操作任务提供了极具潜力的解决方案。 尽管 VLMs 取得了显著进展,机器人仍难以胜任复杂的长时程任务(如家具装配),主要受限于人…...