從零實現一個GPT 【React + Express】--- 【1】初始化前后端項目,實現模型接入+SSE

摘要

本系列文章主要是實現一個能夠對話以及具有文生圖等功能的模型應用。主要UI界面會參考chat-gpt,豆包等系列應用。模型使用的是gpt開源的大模型。

如果你是一個前端開發工程師需要一個自己的開源項目,可以學習這個系列的文章,不需要有很完整的后端知識也可以完成。前端部分主要是通過React來實現,后端部分通過express來實現。

本篇側重點

【1】如何通過node端連接openai的模型
【2】如何在node端如何實現SSE
【3】如何在前端通過Post請求實現SSE
【4】前端如何解析SSE返回的數據格式

獲取api key

在實現項目之前,首先第一步就是要獲取一個openai的 api key,因為要使用openai的模型,這個是必須的。但是由于openai在國內是訪問不了的,所以這里推薦一個可以國內訪問的。

https://github.com/chatanywhere/GPT_API_free

這個github上可以申請一個免費的api,但是需要有一個github賬號。申請完之后請記住自己的api。

這個開源項目也提供了文檔可以查看:

https://chatanywhere.apifox.cn/

初始化前端項目

這里我們使用React + vite 來構建前端項目,如果你不熟悉vite可以先看一下vite的官方文檔:
https://cn.vite.dev/guide/

你可以通過以下指令創建一個vite項目,然后根據提示一步一步的走完。

npm create vite

這里注意你的node版本要在20以上,我使用的是node22。

然后你可以根據自己的喜好去配置eslint等代碼規范工具,這里就不做詳細解釋了。

初始化后端項目

后端項目對我來講就是能用就行,所以我使用了express來實現。可以通過express生成器來生成一個express項目:

npm install -g express-generatornpx express-generator

生成好express項目后

在router下我們新建一個chat.js用來實現對話的接口。再在app.js中引入進來。

這里我們用了一些轉換數據格式的中間件。

// app.jsconst express = require('express');
const app = express();
const chatRouter = require('./routes/chat');
const port = 3002;app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use('/', chatRouter);app.listen(port, () => {console.log(`Example app listening on port ${port}`)
})

實現SSE對話接口

Ok,現在我們前期的準備工作已經做好。你可以在豆包或者gpt里發送一段query,你會發現回答的內容是通過打字機的效果實現的。

如果查看接口的話,就會發現后端的返回結果是通過流試返回的。那后端是如何實現流試返回的效果的呢,其實簡單來講,就是在一個http請求里,通過循環不停地給前端發送消息。從而實現流試輸出的效果。這個就是SSE,當然如果想了解更多內容,可以在網上查閱相關文章。

當然你需要設置正確的響應頭,在chat.js中,需要正確設置text/event-stream:

// routers/chat.jsrouter.post('/chat', function(req, res) {res.set('Content-Type', 'text/event-stream;charset=utf-8');res.set('Access-Control-Allow-Origin', '*');res.set('X-Accel-Buffering', 'no');res.set('Cache-Control', 'no-cache, no-transform');const { message } = req.body;getChat(message, res);
});

在router里可以通過使用cors來解決跨域問題。

getChat方法就是我們要使用模型返回結果的方法,接受兩個參數,一個是用戶發送的query,還有就是res用來給前端返回結果。

那如果我們的后端就需要把用戶發送的query傳遞給模型,并且要求模型流試返回。這個時候就需要用到剛剛的api key了。但是我們先裝一下openai的node包,輸入以下指令進行安裝:

npm i openai

之后在代碼頂部進行引入:

const OpenAi = require('openai');const client = new OpenAi({apiKey: process.env.OPENAI_API_FREE_KEY, // 使用環境變量加載 API 密鑰baseURL: 'https://api.chatanywhere.tech/v1',
})

這里我通過環境變量加載apiKey,讀者如果是自己使用可以直接寫死或者也通過環境變量。

有了openAi的實例后,我們就可以用官方的api實現我們的getChat方法了:

const getChat = async (message, res) => {try {const stream = await client.chat.completions.create({messages: [{ role: 'system', content: '你是一個風趣幽默的中文助手' },{ role: 'user', content: message },],model: 'gpt-3.5-turbo',stream: true,});for await (const part of stream) {const eventName = 'message';if (Object.keys(part.choices[0]?.delta || {}).length > 0) {console.log(part.choices[0].delta);res.write(`event: ${eventName}\n`);res.write(`data: ${JSON.stringify(part.choices[0].delta)}\n\n`);}}res.end(); // 結束連接} catch (error) {console.error('Error during OpenAI API call:', error);res.end(); // 結束連接}
};

可以看到,我們在getChat方法里就是不斷的從openai的流試結果里,拿到后通過res.write方式返回給前端。這樣,我們就實現了一個簡單的后端模型sse返回。

這里注意一下,我們通過 res.write(event: ${eventName}\n) 來定義了返回內容的類型,這個類型后面可能不止這一種。

在第一篇文章中,我們的后端部分就實現這些,就可以完成一個基本的對話了。

如果想看這部分的內容,可以通過以下的commit查看:

https://github.com/TeacherXin/gpt-xin-server/commit/2c9d3a311793bf97c2777b212887d637abe58c74

實現前端布局

前端的整體布局我就不通過代碼去講解了,可以直接查看我的提交記錄,這里放一下我的基本布局:

主要就是整體分為兩部分,左側側邊欄,右側主體部分。可以給外層容器設置display:flex,然后側邊欄定寬,主體部分設置flex:1。

然后在主體部分里加入一個輸入框即可。

在這里插入圖片描述

現在我們主要來實現一下輸入框這個組件,組件內部維護兩個屬性(暫定)。

  • inputValue
  • inputLoading

分別代表輸入框的內容,和發送query之后的loading狀態。

狀態我們使用zustand來進行管理,當然如果讀者比較熟悉mobx或者redux,可以使用自己的方式來進行狀態管理。
我這里使用zustand。

// DialogInput/store.tsimport { create } from 'zustand';interface DialogInputStore {inputValue: string;setInputValue: (value: string) => void;inputLoading: boolean;setInputLoading: (value: boolean) => void;
}export const useDialogInputStore = create<DialogInputStore>((set) => ({inputValue: '',inputLoading: false,setInputValue: (value: string) => set({ inputValue: value }),setInputLoading: (value: boolean) => set({ inputLoading: value }),
}));

實現前端SSE

現在我們只需要在點擊發送按鈕的時候,把輸入的query發送給后端即可。但是前端發送SSE請求,我們常見的是通過new EventSource來實現。但是這種實現方式只能通過get請求,而我們后面實現的過程可能需要傳遞的參數會很多。所以不推薦。

這里我們直接通過post來實現一個sse請求,我們新建一個utils文件夾然后新建一個sse.ts。

// utils/sse.tsinterface CallBackMap {major: (data: Major) => void;message: (data: Message) => void;close: () => void;
}interface Major {id: string;
}interface Message {content: string;
}interface SendData {model: string;
}const connectSSE = async (url: string, params: SendData, callbackMap:CallBackMap) => {}

我們需要實現一個connectSSE方法,接受一個url參數,還有發送的信息,以及callbackMap。

callbackMap是針對于不同類型的返回值做出不同的處理。簡單解釋一下:

在實現后端sse的時候我們通過 res.write(event: ${eventName}\n)來定義不同返回值的類型,所以這里我們要根據不同的類型處理不同的操作。這里我們暫定有三個事件類型:major,message,close。

message代表的是模型返回內容時觸發的事件;close代表模型結果全部返回完之后的事件。major可以先不必關注,后續會用得到。

現在我們就可以實現connectSSE方法了:

// utils/sse.tsconst connectSSE = async (url: string, params: SendData, callbackMap:CallBackMap) => {try {const res = await fetch(url, {headers: {'Content-Type': 'application/json', // 必須設置Accept: 'text/event-stream','Cache-Control': 'no-cache',},method: 'POST',body: JSON.stringify(params),});if (!res.ok) {throw new Error('Error connecting to SSE');}if (!res.body) {throw new Error('Error connecting to SSE');}const reader = res.body.getReader();const decoder = new TextDecoder();while(true) {const { value, done } = await reader.read();if (done) {console.log('Stream closed');callbackMap.close();break;}const chunk = decoder.decode(value);console.log(chunk);// 解析chunk的方法const data = parseChunk(chunk);console.log(data);if (data.major) {callbackMap.major(data.major);}if (data.message) {callbackMap.message(data.message);}}} catch (error) {console.log('SSE error', error);}
};const parseChunk = (chunk: string): ParseChunk => {let type = '';const lines = chunk.split('\n');const eventData: ParseChunk = {};for (let i = 0; i < lines.length; i++) {if (lines[i].startsWith('event: major')) {type = 'major';continue;}if (lines[i].startsWith('event: message')) {type = 'message';continue;}if (lines[i].startsWith('data: ')) {if (type === 'message' && eventData[type]) {eventData[type]!.content += JSON.parse(lines[i].split(': ')[1]).content;} else if (type === 'major' || type === 'message') {eventData[type] = JSON.parse(lines[i].split(': ')[1]);}}}return eventData;};

回到輸入框組件,只需要給按鈕的綁定事件,調用該方法即可看到效果:

// DialogInput/index.tsxconst sendData = () => {const url = 'http://localhost:3002/chat';const sendData = {message: inputStore.inputValue,};connectSSE(url, sendData);
};

該方法的主要原理就是設置好請求頭,通過fetch發送請求。不停的從reader中讀取數據chunk,然后再通過parseChunk方法解析chunk。解析的過程也就是簡單的字符串處理。

在實現connectSSE的過程,可以看到我打了很多的console.log,讀者在實現的過程也可以根據這個log來觀察數據的轉換狀態。而callbackMap我們沒有傳進去,后續再繼續補充,這個時候可以通過NetWork來看一下接口的返回結果。

在這里插入圖片描述

可以看到接口的返回是EventStream,而且都是message類型的消息。

具體部分可以看代碼的提交記錄:
https://github.com/TeacherXin/gpt-xin/commit/8485c8bbf0b5017fedfbbfe83ea265e83412f6f9

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/88408.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/88408.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/88408.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【PTA數據結構 | C語言版】在順序表 list 的第 i 個位置上插入元素 x

本專欄持續輸出數據結構題目集&#xff0c;歡迎訂閱。 文章目錄題目代碼題目 請編寫程序&#xff0c;將 n 個整數存入順序表&#xff0c;對任一給定整數 x&#xff0c;將其插入順序表中指定的第 i 個位置。注意&#xff1a;i 代表位序&#xff0c;從 1 開始&#xff0c;不是數…

汽車智能化2.0引爆「萬億蛋糕」,誰在改寫游戲規則?

進入2025年&#xff0c;長安、奇瑞、比亞迪等各大主機廠紛紛將智能化推進至全新高度&#xff0c;中國汽車智能化競爭進入了“技術市場生態”綜合較量階段。一方面&#xff0c;各大主機廠全力推進輔助駕駛的規模化普及&#xff0c;掀起了一場關于高階輔助駕駛的“技術平權”革命…

QT 第八講 --- 控件篇 Widget(三)界面系列

前言&#xff1a; 在上一講《QT 第七講 --- 控件篇 &#xff08;二&#xff09;window系列與qrc機制》中&#xff0c;我們探討了應用程序窗口&#xff08;QMainWindow, QWidget&#xff09;的基礎結構、窗口標志、狀態以及Qt強大的資源管理機制&#xff08;.qrc文件&#xff0…

廣州華銳互動:AR 領域的創新與服務先鋒?

&#xff08;一&#xff09;定制化服務? 廣州華銳互動秉持 “以客戶為中心” 理念&#xff0c;為客戶提供高度定制化 AR 解決方案。項目初期&#xff0c;通過多種方式深入了解客戶需求&#xff0c;挖掘痛點。基于需求分析&#xff0c;技術團隊運用自主研發技術和先進算法&…

暑假算法日記第一天

目標?&#xff1a;刷完靈神專題訓練算法題單 階段目標&#x1f4cc;&#xff1a;【算法題單】滑動窗口與雙指針 LeetCode題目:1456. 定長子串中元音的最大數目643. 子數組最大平均數 I1343. 大小為 K 且平均值大于等于閾值的子數組數目2090. 半徑為 k 的子數組平均值2379. 得…

【軟考高項】信息系統項目管理師-第1章 信息化發展(1.5 數字化轉型與元宇宙、1.6 標題類知識點、1.7 十四五規劃內容匯總)

文章大綱 第1章 信息化發展1.5 數字化轉型與元宇宙1.5.1 數字化轉型1.5.2 元宇宙1.6 標題類知識點1.7 十四五規劃內容匯總1.8 10道試題第1章 信息化發展 學習建議: 此章內容大部分為新增內容,基本是全新的章節2023年5月考試2分選擇,5分案例2023年下半年各批次選擇題2分左右1.…

STM32F103C8T6單片機內部執行原理及啟動流程詳解

引言&#xff1a;為什么深入理解STM32啟動流程很重要&#xff1f;STM32F103C8T6作為嵌入式開發中最常用的單片機之一&#xff0c;其內部執行原理和啟動流程是理解嵌入式系統底層運行機制的核心。無論是開發Bootloader、調試HardFault異常&#xff0c;還是優化系統啟動速度&…

【python 常用的數學科學/計算機視覺等工具】

當然有&#xff01;在科學計算、機器學習、圖像處理等領域&#xff0c;scikit-learn、scikit-image&#xff08;skimage&#xff09;、SciPy、OpenCV 是非常重要的庫&#xff0c;但它們不是唯一的。以下是一些與它們類似或互補的項目&#xff0c;按照用途分類列出&#xff1a; …

LUMP+NFS架構的Discuz論壇部署

一、配置準備 每臺主機都安裝mysql、nfs、php、mysql 對每臺主機都進行關閉防火墻、上下文等&#xff0c;減少阻礙[rooteveryone ~]# systemctl stop firewalld [rooteveryone ~]# setenforce 0安裝插件等[rootlocalhost mysql]# yum install -y nfs-utils nginx [rootlocalho…

C++STL-deque

一.基礎概念deque和vector一樣都是對元素的操作&#xff0c;不同點&#xff1a;vector對元素增刪后元素會往前或往后移&#xff0c;如果數據不大沒有太多影響&#xff0c;如果數據很大效率會變低&#xff1b;deque對元素增刪不會使元素位置改變&#xff0c;所有效率會變高。二.…

字節跳動高質量聲音克龍文字轉語音合成軟件MegaTTS3整合包

MegaTTS3是抖音團隊聯合國內其他大學研發的一款語音合成及聲音克龍應用&#xff0c;可實現零樣本語音克龍及富有情感的自然語音合成。我基于當前最新版制作了免安裝一鍵啟動整合包。 MegaTTS3介紹 MegaTTS 3 是字節跳動&#xff08;ByteDance&#xff09;與浙江大學聯合開發的…

RPC:遠程過程調用機制

目錄 1、概念 2、RPC架構 2.1 RPC的四個核心組件 2.2 訪問流程 3、關鍵概念 3.1 接口定義語言 (IDL - Interface Definition Language) 3.2 序列化與反序列化 (Serialization & Deserialization - Marshalling/Unmarshalling) 3.3 網絡傳輸 (Transport) 3.4 服務發…

EPLAN 電氣制圖(六):電機正反轉副勾主電路繪制

一、項目背景&#xff1a;為什么繪制電機正反轉主電路&#xff1f; 在多功能天車系統中&#xff0c;電機正反轉控制是核心功能之一。通過 EPLAN 繪制主電路&#xff0c;不僅能清晰展示電源分配、換相邏輯和線纜連接&#xff0c;還能為后續 PLC 控制設計奠定基礎。本次以西門子設…

JAVA JVM對象的實現

jvm分配內存給對象的方式1. 內存分配的總體流程對象內存分配的主要步驟&#xff1a;類加載檢查&#xff1a;確認類已加載、解析和初始化。內存分配&#xff1a;根據對象大小&#xff0c;從堆中劃分內存空間。內存初始化&#xff1a;將分配的內存空間初始化為零值&#xff08;不…

CVE-2023-41990/CVE-2023-32434/CVE-2023-38606/CVE-2023-32435

CVE-2023-41990&#xff08;GitLab 命令注入漏洞&#xff09;漏洞原理CVE-2023-41990是GitLab CE/EE&#xff08;社區版/企業版&#xff09;中項目導出功能的一個命令注入漏洞。具體原理如下&#xff1a;①GitLab在導出項目時&#xff0c;會調用git命令生成項目存檔&#xff08…

RAG實戰指南 Day 8:PDF、Word和HTML文檔解析實戰

【RAG實戰指南 Day 8】PDF、Word和HTML文檔解析實戰 開篇 歡迎來到"RAG實戰指南"系列的第8天&#xff01;今天我們將深入探討PDF、Word和HTML文檔解析技術&#xff0c;這是構建企業級RAG系統的關鍵基礎。在實際業務場景中&#xff0c;80%以上的知識都以這些文檔格式…

【AXI】讀重排序深度

我們以DDR4存儲控制器為例&#xff0c;設計一個讀重排序深度為3的具體場景&#xff0c;展示從設備如何利用3級隊列優化訪問效率&#xff1a;基礎設定從設備類型&#xff1a;DDR4存儲控制器&#xff08;支持4個存儲體Bank0-Bank3&#xff09;讀重排序深度&#xff1a;3&#xff…

牛馬逃離北京(回歸草原計劃)

豐寧壩上草原自駕游攻略&#xff08;半虎線深度版&#xff09; &#x1f697; 路線&#xff1a;北京/承德 → 豐寧縣城 → 半虎線 → 大灘鎮&#xff08;2天1夜&#xff09; &#x1f3af; 核心玩法&#xff1a;免費草原、高山牧場、日落晚霞、牧群互動、星空煙花&#x1f33f;…

【前端】【Echarts】ECharts 詞云圖(WordCloud)教學詳解

效果ECharts 詞云圖&#xff08;WordCloud&#xff09;教學詳解 詞云圖是一種通過關鍵詞的大小、顏色等視覺差異來展示文本數據中詞頻或權重的圖表。它直觀、形象&#xff0c;是數據分析和內容展示中的利器。 本文將帶你從零開始&#xff0c;學習如何用 ECharts 的 WordCloud 插…

【arXiv 2025】新穎方法:基于快速傅里葉變換的高效自注意力,即插即用!

一、整體介紹 The FFT Strikes Again: An Efficient Alternative to Self-AttentionFFT再次出擊&#xff1a;一種高效的自注意力替代方案圖1&#xff1a;FFTNet整體流程&#xff0c;包括局部窗口處理&#xff08;STFT或小波變換&#xff0c;可選&#xff09;和全局FFT&#xff…