从零实现一个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:

js 复制代码
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消息:

js 复制代码
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应该为空

js 复制代码
const data = {
    message: inputStore.inputValue,
};

dialogCardListStore.addDialogCard({
    question: inputStore.inputValue,
    answer: '',
    cardId: '',
});

inputStore.setInputValue('');
inputStore.setInputLoading(true);

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

js 复制代码
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组件了:

js 复制代码
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

js 复制代码
let abortController = new AbortController();

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

js 复制代码
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,不然所有的请求都发不出去了:

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

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

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

相关推荐
前端_学习之路1 小时前
React--Fiber 架构
前端·react.js·架构
coderlin_1 小时前
BI布局拖拽 (1) 深入react-gird-layout源码
android·javascript·react.js
甜瓜看代码1 小时前
1.
react.js·node.js·angular.js
伍哥的传说1 小时前
React 实现五子棋人机对战小游戏
前端·javascript·react.js·前端框架·node.js·ecmascript·js
Misha韩3 小时前
React Native 一些API详解
react native·react.js
小李飞飞砖3 小时前
React Native 组件间通信方式详解
javascript·react native·react.js
小李飞飞砖3 小时前
React Native 状态管理方案全面对比
javascript·react native·react.js
前端小盆友8 小时前
从零实现一个GPT 【React + Express】--- 【5】实现网页生成能力
gpt·react.js·express
Lazy_zheng9 小时前
React 核心 API 全景实战:从状态管理到性能优化,一网打尽
前端·javascript·react.js