从零实现一个GPT 【React + Express】--- 【2】实现对话流和停止生成

摘要

这是本系列文章的第二篇,开始之前我们先回顾一下上一篇文章的内容:

从零实现一个GPT 【React + Express】— 【1】初始化前后端项目,实现模型接入+SSE

在这一篇中,我们主要创建了前端工程和后端工程,这里贴一下我的github地址:

https://github.com/TeacherXin/gpt-xin
https://github.com/TeacherXin/gpt-xin-server

最后我们实现了前端和后端部分的SSE内容,可以通过前端发送query,后端调用gpt模型通过流试返回内容。

而在这一篇中,我们主要把对话部分给实现出来,就是通过后端返回的内容来渲染对话流。

对话流的数据结构

首先我们来到前端项目,肯定是在components下创建一个DialogCardList组件,用来展示对话。

读者可以先在豆包上发送个对话试一下,可以看到对话区域主要是通过问答对的结构展示的。就是一问一答。

所以我们很容易就能设计出来,这个对话列表的数据结构应该是一个List,List下的每一个对象包含着,id,answer,question三个属性。

所以我们可以设计一下DialogCardList组件的store:

import { create } from 'zustand';interface DialogCard {question: string;answer: string;cardId: string;
}interface DialogCardListStore {sessionId: string;setSessionId: (id: string) => void;dialogCardList: DialogCard[];addDialogCard: (card: DialogCard) => void;changeLastAnswer: (question: string) => void;changeLastId: (id: string) => void;
}export const useDialogCardListStore = create<DialogCardListStore>((set) => ({sessionId: '',setSessionId: (id: string) => set(() => ({ sessionId: id })),dialogCardList: [],addDialogCard: (card: DialogCard) => set((state) => ({dialogCardList: [...state.dialogCardList, card],})),changeLastAnswer: (answer: string) => set((state) => {const dialogCard = state.dialogCardList[state.dialogCardList.length - 1];if (dialogCard) {dialogCard.answer += answer;}return { dialogCardList: [...state.dialogCardList] };}),changeLastId: (id: string) => set((state) => {const dialogCard = state.dialogCardList[state.dialogCardList.length - 1];if (dialogCard) {dialogCard.cardId = id;}return { dialogCardList: [...state.dialogCardList] };}),}));

dialogCardList就是代表每个问答对组成的列表;

changeLastAnswer方法主要是用来修改最后一个card的answer,这里是因为sse返回内容是流试的。所以我们要不停的更新最后一个节点的回答。

后端添加major事件

刚才我们说到,每个对话的card都有三个属性,id,question,answer,那id是从哪里来的呢,肯定是后端返回的。

后端可以在每次返回模型输出内容之前,先返回一个id。但是这个id肯定不能是message类型的,所以,我们可以在major事件里返回对应的id。

在getChat方法中,在for循环之前先发送一个major消息:

const eventName = 'major';
res.write(`event: ${eventName}\n`);
res.write(`data: ${JSON.stringify({id: Date.now()})}\n\n`);

这样我们再看一下接口的返回:

在这里插入图片描述

可以看到在SSE中会先返回一个major类型的消息。

本篇章里server端的内容就三行代码的修改,
具体的提交可以查看:

https://github.com/TeacherXin/gpt-xin-server/commit/1ecc36ceb29acec888df48102ec64edf0c3c676f

实现前端对话流

现在我们已经有了对话流的数据结构,现在我们来想一下流程应该是什么样子的。

最开始肯定是在输入框里面输入内容然后发送调用chat接口了,然后服务端通过SSE返回消息内容。

我们现在有三个回调,major,message,close。这三个函数调用的时机是什么,函数需要做什么呢。我们就来模拟整个流程来讲解。

【第一步】发送消息

给dialogCardList添加一个问答对,不过这个时候只有一个question,接口还没有返回。所以answer和cardId应该为空

const data = {message: inputStore.inputValue,
};dialogCardListStore.addDialogCard({question: inputStore.inputValue,answer: '',cardId: '',
});inputStore.setInputValue('');
inputStore.setInputLoading(true);

【第二步】设置三种事件类型的回调

const url = 'http://localhost:3002/chat';const messageCallback = (message: Message) => {dialogCardListStore.changeLastAnswer(message.content);
};const closeCallback = () => {inputStore.setInputLoading(false);
};const majorCallback = (major: Major) => {dialogCardListStore.changeLastId(major.id);
};connectSSE(url, data, {message: messageCallback,major: majorCallback,close: closeCallback,
});

我们需要在messageCallback,不停的更新dialogCardList中,最后一个card的answer。
在majorCallback中,更新最后一个card的id
在closeCallback中,更新一下输入框的loading状态。
然后传给connectSSE方法即可。

【第三步】实现DialogCardList组件

有了数据结构以及更新流程之后,我们就可以实现DialogCardList组件了:

const DialogCardList: React.FunctionComponent = () => {const dialogCardListStore = useDialogCardListStore();return (<div className={styles.scrollContainer}><div className={styles.dialogCardList}>{dialogCardListStore.dialogCardList.map((item) => {return (<div className={styles.dialogCard} key={item.cardId}><div className={styles.question}><p>{item.question}</p></div><div className={styles.answer}>{item.answer}</div></div>);})}</div></div>);
};

只需要遍历dialogCardList把对应的问答对展示出来即可,CSS的样式这里我就不写了,可以直接看我的提交记录(贴在后面了)。

最终我们就可以通过发送query实现对话功能了,这里展示一下效果:

请添加图片描述

停止生成

现在我们发送完对话,如何停止生成,让这个对话结束呢。

其实我们只需要把SSE的请求取消即可,回到我们的sse.ts中,在最外层定义个abortController

let abortController = new AbortController();

然后修改connectSSE方法,把abortController传给fetch请求:

const res = await fetch(url, {headers: {'Content-Type': 'application/json', // 必须设置Accept: 'text/event-stream','Cache-Control': 'no-cache',},method: 'POST',body: JSON.stringify(params),signal: abortController.signal, // 用于取消请求});

最后再实现一个stopSSE方法,这里注意一下,每次停止生成都要生成一个新的AbortController,因为下次发送fetch请求不能用之前的AbortController,不然所有的请求都发不出去了:

const stopSSE = () => {abortController.abort(); // 取消 fetch 请求abortController = new AbortController();
}

当inputLoading为true的时候,点击按钮就停止生成。

前端部分在这一篇的内容也就实现完了,具体的代码变更可以看下面的提交记录:
https://github.com/TeacherXin/gpt-xin/commit/6cb2c719cce51ae9cd6af92cad1283de41c485c9

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

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

相关文章

SEQUENCE在RAC多实例开启CACHE的NEXTVAL数值乱序问题

问题说明 在多实例环境中可能会出现从Sequence所取出来的nextval是乱序的&#xff0c;比如第二次比第一次所取的数要小但这并不是我们所希望的。当程序逻辑Base on sequence.nextval数值所谓填充字段的大小来排序时&#xff0c;就会产生问题。 实际上就是由于多实例这一特性造成…

后台管理系统-权限管理

在后台管理系统当中&#xff0c;权限管理占着非常重要的位置&#xff0c;权限管理&#xff0c;顾名思义&#xff0c;就是用来管理用户登录后台的权限。 在权限管理中有三个重要的名词&#xff1a;账号&#xff0c;角色&#xff0c;权限 账号&#xff1a;通过账号进入平台&…

MySQL表的约束(5)

文章目录前言一、空属性二、默认值三、列描述四、zerofill五、主键六、自增长七、唯一键八、外键总结前言 真正约束字段的是数据类型&#xff0c;但是数据类型约束很单一&#xff0c;需要有一些额外的约束&#xff0c;更好的保证数据的合法性&#xff0c;从业务逻辑角度保证数据…

MyBatis:SQL与Java的智能桥梁

MyBatis&#xff1a;SQL 与 Java 的「智能翻译官」 —— 用 极简的方式 连接数据库和 Java 对象&#xff0c;告别 JDBC 的繁琐操作&#xff01;核心定位&#xff1a;半自动化 ORM 框架对比项JDBC 原生操作MyBatis 解决方案SQL 编写拼字符串&#xff08;易出错、难维护&#xff…

自动驾驶控制系统

目录 控制系统概述 无人车控制架构设计 自动驾驶控制核心技术 车辆纵向控制 车辆横向控制 自动驾驶控制方法 自动驾驶控制技术方案 人机交互系统 控制系统概述 控制技术是智能驾驶的关键,旨在环境感知技术的基础之上,根据决策规划出目标轨迹,通过纵向和横向控制系统…

网络安全基石:从弱口令治理到动态防御体系的构建

引言&#xff1a;数字时代的防御困局 在5G与物联网技术全面落地的数字新基建时代&#xff0c;企业网络资产规模呈现指数级增长。Verizon《2023年数据泄露调查报告》显示&#xff0c;61%的安全事件直接源于凭证失窃&#xff0c;而其中81%的攻击成功案例可溯源至初始口令强度的不…

Error: fatal: detected dubious ownership in repository at

这个错误是 Git 在新版中引入的一种 安全检查机制&#xff0c;目的是防止不同用户访问同一个 Git 仓库目录&#xff0c;避免潜在的权限或安全问题。你的情况是&#xff1a;仓库目录是属于另一个用户。当前以管理员用户 OVERSPREAD/Administrator 运行 Git。Git 为了安全起见&am…

嵌入式 数据结构学习 (六) 树、哈希表与内核链表

一、树(Tree)结构详解1. 树的基本概念树的核心特性非线性结构&#xff1a;数据元素之间存在一对多的层次关系递归定义&#xff1a;树的子树仍然是树专业术语&#xff1a;度(Degree)&#xff1a;结点拥有的子树数叶子结点&#xff1a;度为0的结点层次(Level)&#xff1a;根为第1…

封装WebSocket

一个基于原生 WebSocket 的封装库&#xff0c;实现了自动重连、心跳检测等功能&#xff0c;用于在前端应用中稳定地与后端 WebSocket 服务通信。下面从设计思路、关键功能等方面进行详细分析&#xff1a;设计思路 这个封装库采用单例模式设计&#xff0c;全局维护一个 WebSocke…

SLICEGPT: COMPRESS LARGE LANGUAGE MODELSBY DELETING ROWS AND COLUMNS

发表&#xff1a;ICLR24 机构&#xff1a;ETH Zurich 连接&#xff1a;https://arxiv.org/pdf/2401.15024 ABSTRACT 大型语言模型&#xff08;Large Language Models, LLMs&#xff09;已成为自然语言处理的基石&#xff0c;但其使用伴随着在计算和内存资源方面的高昂代价。…

Python 【技术面试题和HR面试题】➕ 循环结构、控制语句及综合应用问答

1.技术面试题 &#xff08;1&#xff09;详细描述单调栈的工作原理和应用场景 答&#xff1a; 原理 维护栈内元素单调递增 / 递减&#xff0c;新元素入栈前&#xff0c;弹出破坏单调性的栈顶&#xff0c;保持单调。 应用场景 排队比身高&#xff0c;搭积木找最大的空地 &#x…

100G系列光模块产品与应用场景介绍

在当今数字化时代&#xff0c;网络流量呈爆炸式增长&#xff0c;对数据传输速度和带宽的要求也越来越高。100G 光模块作为高速数据传输的关键组件&#xff0c;因其卓越的高速传输能力&#xff0c;已成为数据中心、云计算、企业网络以及 5G 通信网络等领域的重要组成部分。接下来…

运筹说 第140期 | 从直觉到算法:这些奠基人如何塑造了启发式方法的科学根基?

运筹说建构知识体系&#xff0c;解析学习要点运 筹 优 化 领 域 教 学 媒 体视频课程已上线&#xff01;&#xff01;!欢迎大家关注同名抖音和哔哩哔哩账号&#xff01;在人工智能与优化科学的浩瀚星空中&#xff0c;启发式算法如同一把钥匙&#xff0c;为人类打开了处…

Flutter编译安卓应用时遇到的compileDebugJavaWithJavac和compileDebugKotlin版本不匹配的问题

记一次flutter应用&#xff0c;编译安卓时&#xff0c;报的一个compileDebugJavaWithJavac和compileDebugKotlin版本本匹配的问题。 最终定位的原因是项目一来了audioplayers组件。 audioplayers组件有依赖了audioplayers_android&#xff0c; 它使用1.8编译的。 版本过低。后来…

linux-权限管理

linux-权限管理一、权限的基本类型二、权限的表示方式1. 字符形式&#xff08;rwx&#xff09;2. 数字形式三、权限管理常用命令1. chmod2. chown3. chgrp四、隐藏权限1. lsattr2. chattr五、权限掩码六、特别权限位1. suid2. sgid3. Sticky Bit七、权限委托1. 授权用户2. 授权…

从FCOS3D到PGD:看深度估计如何快速搭建你的3D检测项目

【导读】 还记得那个曾经在单目3D目标检测领域掀起热潮的 FCOS3D 吗&#xff1f;在后续更新中他们又推出了全新升级版——PGD&#xff08;Probabilistic and Geometric Depth&#xff09;最有意思的是&#xff0c;这次他们彻底换了路线&#xff1a;从原先的“直接回归深度”&a…

Apache Cloudberry 向量化实践(三)重塑表达式构建路径:Gandiva 优化实战

在向量化执行系统中&#xff0c;表达式构建是不可或缺的基础环节。无论是 SQL 中的投影、筛选&#xff0c;还是分区、聚合、排序&#xff0c;最终都需转化为底层执行引擎能识别和执行的表达式树。而在 Apache Cloudberry 向量化执行框架中&#xff0c;这一过程由 Gandiva 表达式…

Windows删除文件或者拔出U盘显示正在使用/占用解决办法

1、复制文件地址2、打开任务管理器&#xff0c;选择左侧【性能】3、打开资源监视器4、选择资源监视器中的CPU5、粘贴你复制的占用文件地址6、除了explore.exe以外&#xff0c;其他的关联的句柄都选中&#xff0c;然后右键结束

自由学习记录(68)

&#x1f9e0; blender为什么不用 M 或 T&#xff1f; 键位含义为什么没选MMove&#xff1f;其实被用作「Move to Collection」等功能不符合历史定义&#xff0c;而且功能太多了TTransform&#xff1f; 但 transform 是一个总称&#xff08;含移动、旋转、缩放&#xff09;T 被…

ReactNative【实战系列教程】我的小红书 8 -- 我(含左侧弹窗菜单,右下角图标等)

最终效果点左上角菜单按钮&#xff0c;弹出左侧菜单后代码实现app/(tabs)/mine.tsx import icon_add from "/assets/icons/icon_add.png"; import mine_bg from "/assets/images/mine_bg.png"; import Heart from "/components/Heart"; import a…