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?小编这里写一个详细图…...
vibe-to-ui:让AI助手将你的“感觉”翻译成专业设计系统
1. 项目概述:当“感觉”成为设计语言如果你和我一样,是一个能写出复杂业务逻辑,但一碰到UI设计就头疼的开发者,那今天聊的这个工具,可能会彻底改变你的工作流。我们常常陷入一个困境:心里有一个模糊的“感觉…...
别再微调模型了!Claude 3.5 Sonnet新增3类零样本指令模板:Prompt工程师的最后护城河正在崩塌?
更多请点击: https://intelliparadigm.com 第一章:Claude 3.5 Sonnet零样本指令能力的范式跃迁 Claude 3.5 Sonnet 在零样本(zero-shot)场景下展现出前所未有的指令理解与泛化能力,标志着大模型从“模式复现”向“意图…...
疫情如何重塑GPU市场:从游戏硬件到数字基础设施的演变
1. 市场预期的“扭曲”:疫情如何重塑GPU行业逻辑如果你在2020年初问任何一位半导体行业的分析师,他们对当年第二季度GPU(图形处理器)市场的预测,大概率会得到一个基于历史季节性规律的保守或平稳的答案。然而ÿ…...
期刊论文发表难破局:虎贲等考 AI 以真文献 + 强实证,大幅提升录用率
在职称评审、毕业要求、科研考核的多重压力下,期刊论文早已成为硬指标。可现实是:投稿容易录用难,初审因选题、文献、实证、格式任意一点不合格就被拒稿,返修反复消耗数月。通用 AI 只能堆砌文字、编造来源,普通工具仅…...
企业级AI应用如何通过Taotoken统一管理多个大模型API调用
🚀 告别海外账号与网络限制!稳定直连全球优质大模型,限时半价接入中。 👉 点击领取海量免费额度 企业级AI应用如何通过Taotoken统一管理多个大模型API调用 在构建企业级AI应用时,技术团队常常面临一个现实挑战&#x…...
Go语言构建高效命令行工具集:从设计到工程化实践
1. 项目概述:一个“好用的”开源工具集最近在GitHub上闲逛,发现了一个挺有意思的仓库,叫ImGoodBai/goodable。光看这个名字,就透着一股子“实用主义”的气息——“好用的”。作为一名常年混迹于开源社区,喜欢折腾各种工…...
用100道题拿下你的算法面试(链表篇-7):复制带随机指针的链表
一、面试问题 给定一个链表的头节点,链表中每个节点都包含两个指针:一个指向下一个节点的 next 指针,以及一个指向链表中任意节点的 random 指针。请复制该链表,并返回新链表的头节点。 二、【朴素解法】使用哈希表 —— 时间复杂…...
从零搭建VGG16:深入解析网络架构与PyTorch实战
1. VGG16网络架构解析 VGG16作为卷积神经网络发展史上的里程碑,其核心设计理念至今仍影响着现代深度学习模型。我第一次接触这个网络时,被它简洁优雅的结构深深吸引——全部使用33小卷积核堆叠,配合22最大池化,这种设计就像用乐高…...
LayerDivider终极指南:5分钟掌握智能插画分层技术
LayerDivider终极指南:5分钟掌握智能插画分层技术 【免费下载链接】layerdivider A tool to divide a single illustration into a layered structure. 项目地址: https://gitcode.com/gh_mirrors/la/layerdivider 你是否曾经面对一张复杂的插画作品…...
【开盘预测】2026年5月13日(周三)
生成时间:2026-05-12 20:30 | 数据来源:金融市场数据 核心预测:市场震荡整理,关注4200-4250区间,量能变化是关键一、今日收盘总结指数收盘点涨跌幅关键技术位上证指数4214.49-0.25%失守4220,守在4200上方深…...
