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 = nullapp.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 = 0setTask = 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:EventSourceconst 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]);