node对接ChatGpt的流式输出的配置
首先看一下效果
将数据用流的方式返回给客户端,这种技术需求在传统的管理项目中不多见,但是在媒体或者有实时消息等功能上就会用到,这个知识点对于前端还是很重要的。
即时你不写服务端,但是服务端如果给你这样的接口,你也得知道怎么去使用联调。
1. nodejs实现简单的SSE服务,使用write返回流式
SSE服务(Server-Sent Events),是一种服务器向客户端推送实时更新的机制模式。
const express = require('express');
const app = express();
const port = 8002;
let strArr = [
'你好',
'吃饭了吗',
'What are you doing?',
'My name is yy',
'8888',
'hello'
]
let setTask = null
app.get('/events', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream;charset=utf-8');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
let num = 0
setTask = setInterval(()=>{
res.write(`data:${strArr[num]}\n\n`)
num++
if(num > 5){
res.write(`data:end\n\n`)
res.end()
// res.closed()
clearInterval(setTask)
setTask = null
}
},1000)
});
app.listen(port, () => {
console.log(`${port}端口已启动`);
});
2. 前端实现接收数据流
这里使用一个叫做EventSource的api去实现流式接口的调用和数据获取**。**
配置代理(重要)
如果我们用vue,react等等框架开发时,需要在代理处做一些配置,确保数据会以流式的返回。如果不做这层代理的配置,那么你获取的数据就会是执行完所有的res.write,一次性的全部返回给前端,就不是我们想要的效果。
效果如下,在配置代理中将compress设置为false
devServer:{
client:{
overlay:false
},
port:8080,
open:true,
compress:false, //流式数据返回的关键配置
proxy:{
'/server1':{
target:'http://localhost:3001',
ws:false,
changeOrigin:true,
pathRewrite:{
'^/server1':''
}
},
'/server2':{
target:'http://localhost:3002',
ws:false,
changeOrigin:true,
pathRewrite:{
'^/server2':''
}
},
'/sse':{
target:'http://localhost:8002',
ws:false,
changeOrigin:true,
pathRewrite:{
'^/sse':''
}
}
}
}
前端实现接口调用
<template>
<div>
<el-button @click="sendMsg">发送消息</el-button>
<p v-for="(item,index) in msgList" :key="index">{{ item }}</p>
</div>
</template>
<script>
export default{
name:'admin',
data(){
return{
msgList:[]
}
},
methods:{
sendMsg(){
let vm = this
//方案1:EventSource
const eventSource = new EventSource('/sse/events');
//消息监听
eventSource.onmessage = function(event) {
console.log(eventSource,vm,'状态')
console.log(event.data); // 输出SSE发送的数据
if(event.data === 'end'){
eventSource.close()
}else{
vm.msgList.push(event.data)
}
};
//连接成功
eventSource.onopen = function(event){
}
//连接出错
eventSource.onerror = function(error) {
if (eventSource.readyState === EventSource.CLOSED) {
// 连接已关闭,可能需要重新连接
console.error('SSE连接已关闭:', error);
}
}
//方案2:xhr(不推荐)
// const xhr = new XMLHttpRequest();
// const url = '/sse/events';
// xhr.open('GET', url,true);
// xhr.setRequestHeader('Accept', 'text/event-stream');
// xhr.onload = (event)=>{
// if(xhr.status === 200){
// console.log(xhr.responseText,'onload',event)
// }
// }
// xhr.onreadystatechange = (event)=>{
// // if(xhr.status === 200){
// // console.log(xhr.responseText,'onreadystatechange',event)
// // }
// }
// xhr.onprogress = (event)=>{
// if(xhr.status === 200){
// console.log(xhr.responseText,'onreadystatechange',event)
// }
// }
// xhr.send()
}
}
}
</script>
<style lang="less">
</style>
这样就大功告成了,如果以后要是做类似于chatgpt这种效果,就可以用到的。
3. 对接ai接口,使用write返回数据流格式
服务器端(node+express)与openai接口对接部分代码:
// const md5 = require('md5')
import express from 'express';
import mysql from 'mysql';
import cors from 'cors';
import jwt from 'jsonwebtoken';
import bodyParser from 'body-parser';
import OpenAI from "openai";
const app = express()
app.use(bodyParser.json()) //解析json
app.use(bodyParser.urlencoded({ extended: true })); //解析客户端传递过来的参数
function query(sql, callback) {
// 从连接池中获取一个连接
pool.getConnection((err, connection) => {
if (err) {
callback(err, null);
} else {
// 执行查询
connection.query(sql, (err, results) => {
// 释放连接
connection.release();
callback(err, results);
});
}
});
}
app.get('/open', (req, res) => {
const openai = new OpenAI(
{
// 若没有配置环境变量,请用百炼API Key将下行替换为:apiKey: "sk-xxx",
apiKey: "",
baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1"
}
);
async function main() {
const completion = await openai.chat.completions.create({
model: "qwen-plus",
messages: [
{ "role": "system", "content": "You are a helpful assistant." },
{ "role": "user", "content": "你是谁?" }
],
stream: true,
});
for await (const chunk of completion) {
// console.log(JSON.stringify(chunk));
// res.write(chunk);
res.write(JSON.stringify(chunk));
// res.end();
}
res.end();
}
main();
})
app.listen(3001, () => {
console.log('serve is running at http://127.0.0.1:3001')
})
上面的apikey值需要你申请密钥
然后在浏览器访问地址http://192.168.3.105:3001/open,就会输出流式数据,在前端中调用就行
4. res.write、res.end及res.send 使用以及区别?
首先,Express响应中常用的四种API:res.write() | res.end() | res.send() | res.json(),这4个API方法,都可以发送HTTP响应,返回浏览器的请求数据
一. res.write()方法
//引入express包
const express = require('express');
//创建路由器对象
const r = express.Router();
//往路由器中添加路由
//商品列表路由:get /list
r.get('/list', (req, res) => {
// 在响应头信息里设置响应返回的内容类型为html,编码为utf-8(在浏览器页面正常显示中文)
// 设置内容解析的编码为utf-8,正确地告诉浏览器,服务器响应的内容是什么编码的,你浏览器应该按照我服务器设定的编码格式来解析给你的内容
// res.writeHead(200, {
// 'Content-Type': 'text/html;charset=utf-8'
// });
let show = "<h2>888</h2>";
res.write(show);
res.write('商品列表');
res.end();
//res.end('<div>该方法用于结束响应的浏览器请求</div>');
});
//导出路由器对象
module.exports = r;
打开接口地址
1. res.write()响应的数据“所见即所得”
res.write()的返回数据是没有经过处理的,原封不动的返回原数据,所见即所得
2. res.write()与res.end()总是且必须成对出现
如果要使用res.write()最后必须要有res.end,两者是成对出现的,缺一不可,也就是说使用res.write方法向前端返回数据,必须调用res.end方法结束请求。否则浏览器会一直处于处于请求状态
3. res.write()方法在结束浏览器响应请求之前,允许多次调用
如果想要输出多条语句,使用的是res.write(),也就是说在res.end() 之前,res.write() 可以被执行多次),且返回的数据会被拼接到一起。
4.res.write()是可以结合HTML标签显示的
res.write()输出内容可以结合HTML标签进行使用。
5. res.write()只支持输出字符串类型或是Buffer对象两种内容类型的数据
如果此时我们输出一个数字就会报错,查看报错信息,提醒我们不能输出number类型
res.write(123);
二. res.end方法
//引入express包
const express = require('express');
//创建路由器对象
const r = express.Router();
//往路由器中添加路由
//商品列表路由:get /list
r.get('/list', (req, res) => {
// 在响应头信息里设置响应返回的内容类型为html,编码为utf-8(在浏览器页面正常显示中文)
// 设置内容解析的编码为utf-8,正确地告诉浏览器,服务器响应的内容是什么编码的,你浏览器应该按照我服务器设定的编码格式来解析给你的内容
res.writeHead(200, {
'Content-Type': 'text/html;charset=utf-8'
});
res.end('<div>该方法用于结束响应的浏览器请求</div>');
//下面语句将不会输出
res.end("Hello world");
});
//导出路由器对象
module.exports = r;
res.end()函数用于结束响应过程。该方法用于快速结束响应,而无需任何数据。也就是说用于在没有任何数据的情况下快速结束响应。如果有响应数据,就不能用 res.end,会报错,请使用res.send()和res.json()等方法。
1. res.end()响应的数据“所见即所得”
res.end()的返回数据同res.write()一样,也是没有经过处理的,原封不动的返回原数据,所见即所得
2. res.end()是不允许输出多行的
不同于res.write()方法,res.end()作为结束浏览器请求的方法,仅能调用一次
3. res.end()是可以结合HTML标签显示的
res.end()同res.write()一样,输出的内容可以是带HTML标签的内容
res.end(‘’
4. res.end()只支持输出字符串类型或是Buffer对象两种内容类型的数据
res.end()同res.write一样,不能输入除字符串类型或是Buffer对象类型外的其他内容类型的数据
三. res.send()方法
1. res.send()响应的数据是经过处理的
打开浏览器控制台,在响应头中被自动添加了context-type,也就是说,res.send()方法响应返回给页面数据时,在响应头信息里会被自动添加设置返回数据类型的context-type属性
2. res.send()只能被调用一次,因为它等同于res.write+res.end()
多个send输出只执行第一个send语句,后续send语句将不被执行
3.res.send()同res.write()、res.end()一样,可以结合HTML标签数据显示
*4. res.send**()支*持多种内容格式的输出
res.send()方法可以支持多种参数,比如可以传String、Array、Buffer对象、对象、json对象
当参数是Array或Object、json对象,Express以JSON表示响应:
res.send({ user: ‘tobi’ });
res.send()只能被调用一次,因为它等同于res.write+res.end()**
多个send输出只执行第一个send语句,后续send语句将不被执行
3.res.send()同res.write()、res.end()一样,可以结合HTML标签数据显示
*4. res.send**()支*持多种内容格式的输出
res.send()方法可以支持多种参数,比如可以传String、Array、Buffer对象、对象、json对象
当参数是Array或Object、json对象,Express以JSON表示响应:
res.send({ user: ‘tobi’ });
res.send([1,2,3]);