当前位置: 首页 > news >正文

从零实现一个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.lryc.cn/news/584163.html

相关文章:

  • Flink Exactly Once 和 幂等
  • Spring for Apache Pulsar->Reactive Support->Message Consumption
  • wpf使用webview2显示网页内容(最低兼容.net framework4.5.2)
  • 以太网基础⑤UDP 协议原理与 FPGA 实现
  • 西电考研录取:哪些省份考研上岸西电更容易?
  • websocket的客户端(发送接收json的消息)
  • TCP-与-UDP-协议详解:原理、区别与应用场景全解析
  • 自动驾驶环境感知:天气数据采集与融合技术实战
  • 关键字前跟空格或首字母, 关键字后跟空格或标点符号; 标点符号后面不是必须跟空格;
  • Duplicate cleaner pro 的使用技巧
  • 汽车加气站操作工分类有几种
  • 制作一款打飞机游戏78:游戏选项
  • 城市脉搏中的隐形守护者
  • 羊肚菌自动采收车设计cad【7张】+三维图+设计说明书
  • U-Boot 2025.07 引入的 “uthreads” 优势介绍
  • 【三维重建】开源slam数据集介绍
  • ssh: connect to host github.com port 22: connection refused
  • 微服务架构下某汽车APP电商模块订单服务自动化测试方案(Python蹭个场)
  • 庄懂的水特效AP01/L16/Water
  • 【TCP/IP】10. 引导协议与动态主机配置协议
  • 汽车功能安全-软件集成和验证(Software Integration Verification)【目的、验证输入、集成验证要求】9
  • RHCE考试 ——笔记
  • 接口(上篇)
  • 编译OpenHarmony-4.0-Release RK3566 报错
  • 【保姆级教程】基于anji-plus-captcha实现行为验证码(滑动拼图+点选文字),前后端完整代码奉上!
  • 前端面试十一之TS
  • 电路研究9.4——合宙Air780EP的LuatOS、CSDK跟标准AT
  • 【Ubuntu】编译sentencepiece库
  • php中array($this, ‘loadClass‘)表示啥意思?
  • 【STM32 学习笔记】WDR看门狗