首页 前端知识 使用原生nodejs来对接ChatGPT API流式响应

使用原生nodejs来对接ChatGPT API流式响应

2024-05-09 10:05:12 前端知识 前端哥 172 462 我要收藏

我是跟着B站一个老师写的:B站老师视频

文章末尾附全部代码,如有那个地方不对的或者说是不准确的还请各位大佬指出来哇(っ˘зʕ•̫͡•ʔ

目录

1.首先得创建一个空的文件夹,在vscode中打开

2.安装可以直接运行TS的开发工具

3.新建TS文件

4.在vscode中可以有node一些内置包的提示

5.在创建的TS文件中输入代码

6.在终端运行

6.如果以上都正确的话,就说明我们基本的效果已经出来了

7.接下来就可以对接ChatGPT的接口了

8.新建一个窗口(终端),安装axios

9.接着就是在TS文件中导入axios和api实例

10.创建一个env文件

11.因为nodejs中无法直接使用env的文件,所以这里需要安装一个包

12.接着就导入dotenv

13.接下来就是createServer方法中的了

14.因为请求的速度会很慢,所以需要安装proxy

15.导入socks-proxy-agent

16.接着就是在env和TS中写入代理地址

17.接着就试着一下输出stream格式

18.也可以试着更改message中的内容

19.接下来就开始前端的业务了

19.1创建一个html文件

19.2再来看一下req

19.3请求的是跟地址

19.4请求的是chat

19.5如果请求的是其他地址

19.6注意一点

19.7html中的代码

19.8返回字符串

19.9在页面输出

19.10用文本框的形式输出

20.全部代码

TS代码

env代码

html代码


1.首先得创建一个空的文件夹,在vscode中打开

2.安装可以直接运行TS的开发工具

npm i -g ts-node-dev

3.新建TS文件

4.在vscode中可以有node一些内置包的提示

npm i -D @types/node

5.在创建的TS文件中输入代码

// 从http中导入createServer方法,目的是创建一个web服务
import {createServer} from 'http'

createServer((req, res) => {
    res.end('ok')
}).listen(9001)// 这个9001是监听的端口号
console.log('http://localhost:9001');

6.在终端运行

tsnd server.ts => 我这里的server是我起的名字,你们根据自己起的名字写

最后效果如图所示:

因为我代码里面写的端口号是9001,所以我输出的是http://localhost:9001,最后运行结束后,终端里面会出现log出来的接口,Ctrl+单击终端中的链接,浏览器就会出现我代码中的OK

6.如果以上都正确的话,就说明我们基本的效果已经出来了

7.接下来就可以对接ChatGPT的接口了

https://platform.openai.com/docs/api-reference/making-requests

打开上面的链接(ChatGPT的官方文档),找到下图这个地方,这个图片中就是ChatGPT的接口和其他一些东西了

8.新建一个窗口(终端),安装axios

npm i axios

9.接着就是在TS文件中导入axios和api实例

// 导入axios
import axios from 'axios'
// 创建axios的一个api实例
const api = axios.create({
    baseURL:'https://api.openai.com/v1',//这里写的就是ChatGPT的接口
    timeout: 5000,// 可以不写
    headers: {
        'Content-Type': 'application/json', // 这个可以不写,重要的是下面的Authorization
        Authorization: `Bearer ${}`// 这里是咱们自己的token,看env文件,这里我把key放到了环境变量中
    }
})

10.创建一个env文件

这里的key就是之前群里发的那个Excel表格里面的东西

OPENAI_API_KEY = 你自己的key

11.因为nodejs中无法直接使用env的文件,所以这里需要安装一个包

npm i dotenv

12.接着就导入dotenv

从一下代码中可以看到Authorization后面添加了东西,其实这是引入dotenv/config后,会自动加载dotenv文件,并且会输出到process.env中去,然后后面再加上OPENAI_API_KEY就可以了。这就设置好了最基本的axios实例

import 'dotenv/config'
​
const api = axios.create({
    baseURL:'https://api.openai.com/v1',//这里写的就是ChatGPT的接口
    timeout: 5000,// 可以不写
    headers: {
        'Content-Type': 'application/json', // 这个可以不写,重要的是下面的Authorization
        Authorization: `Bearer ${process.env.OPENAI_API_KEY}`// 这里是咱们自己的token
    }
})

13.接下来就是createServer方法中的了

createServer(async (req, res) => {
  // 这里是ChatGPT的接口根地址,返回的数据是JSON格式
  const {data} = await api.post("chat/completions", {
    // 这里是ChatGPT的JSON数据
    model: "gpt-3.5-turbo",
    messages: [{ role: "user", content: "Say this is a test!" }],
    max_tokens: 30,// 测试的时候加一个max_tokens,目的是控制ChatGPT的输出长度,可以减少请求的时间
    // temperature: 0.7,
  });
//   res.end无法直接返回JSON格式,需要利用JSON.stringify()转成字符串
  res.end(JSON.stringify(data));
}).listen(9001); // 这个9001是监听的端口号

上面代码中的这个部分是ChatGPT官方文档中的-d这个部分的东西。content: "Say this is a test!" 中引号里面的东西就是咱们平时向小柴提问的问题

{
    // 这里是ChatGPT的JSON数据
    model: "gpt-3.5-turbo",
    messages: [{ role: "user", content: "Say this is a test!" }],
    max_tokens: 30,// 测试的时候加一个max_tokens,目的是控制ChatGPT的输出长度,可以减少请求的时间
    // temperature: 0.7,
}

14.因为请求的速度会很慢,所以需要安装proxy

npm i socks-proxy-agent

15.导入socks-proxy-agent

import {SocksProxyAgent} from 'socks-proxy-agent'

16.接着就是在env和TS中写入代理地址

从上述图片中可以看到,红框框中的地址是7890,所以我这里写的是7890,你们需要根据自己的来写

SOCKS_PROXY = socks5://127.0.0.1:7890
httpsAgent: new SocksProxyAgent(process.env.SOCKS_PROXY)

如图所示:这就是运行出来的结果,ChatGPT直接返回的是一个JSON对象,最重要的是choices中的。可以看到message下的content是直接返回的一个内容

17.接着就试着一下输出stream格式

// 导入stream
import { Stream } from 'stream';

createServer(async (req, res) => {
  // 这里是ChatGPT的接口根地址,返回的数据是JSON格式。post默认的是任何格式,但是这里是stream格式
  const {data} = await api.post<Stream>("chat/completions", {
    // 这里是ChatGPT的JSON数据
    "model": "gpt-3.5-turbo",
    "messages": [{ "role": "user", "content": "Say this is a test!" }],
    max_tokens: 30,// 测试的时候加一个max_tokens,目的是控制ChatGPT的输出长度,可以减少请求的时间
    // temperature: 0.7,
    stream:true,
  },{
    // 设置返回的数据类型为stream
    responseType:'stream'
  });
  data.pipe(res);// 可以直接流向res响应的对象
//   res.end无法直接返回JSON格式,需要利用JSON.stringify()转成字符串。但是因为ChatGPT的返回是stream格式,所以需要用pipe方法将stream转成字符串,这里就把JSON给注销了
//   res.end(JSON.stringify(data));
}).listen(9001); // 这个9001是监听的端口号

如下图所示,这就是运行出来的结果:

18.也可以试着更改message中的内容

"messages": [{ "role": "user", "content": "你可以讲一个爱情故事吗?" }],
max_tokens: 100,// 并且,我把这里的最大限度设成100,在运行过程中就可以看到页面在不停输出,直到我设置的最大限度为止

19.接下来就开始前端的业务了

19.1创建一个html文件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>流式响应</title>
</head>
<body>
    <p>流式响应是指服务器在发送响应时,可以不等待客户端请求,而是直接发送响应,这样可以减少服务器的响应时间,提高服务器的性能。</p>
</body>
</html>

19.2再来看一下req

依然是在刚刚的TS文件当中,在createServer中所有代码之前加上下方代码即可

createServer(async (req, res) => {
    console.log(req.url);// 来看一下req的url,看他请求的是什么
    return res.end(req.url);
}).listen(9001); // 这个9001是监听的端口号

如图所示,这就是浏览器页面中输出的东西

下方图片是vscode终端中输出的东西

所以我们需要实现一个界面,上方url中的地址是/的时候显示html页面,是chat的时候就是接口的东西

19.3请求的是跟地址

但是如果地址后面有参数的话,会被忽略掉,所以我们需要将url转化为一个URL对象。这里就需要改刚刚19.2步的代码了

// console.log(req.url);// 来看一下req的url,看他请求的是什么
// 因为如果地址后面有参数的话,会被忽略掉,所以我们需要将url转化为一个URL对象
const url = new URL(req.url!,'file:///')
// return res.end(url.pathname),// 这里输出url的pathname,也就是请求的接口地址,他就会是一个纯路径
    
// 做个判断,看看请求的是什么接口
switch (url.pathname) {
	// 让数据以流的形式返回给客户端
	case "/":
		createReadStream("./index.html").pipe(res);
		break;
}

如图所示,这就是运行的结果了。页面显示的是刚刚html中写的内容

19.4请求的是chat

这里就是再加一个case,然后后面跟的是之前写的东西,就直接把那一部分的代码XC过来就可以

// 做个判断,看看请求的是什么接口
  switch (url.pathname) {
    // 让数据以流的形式返回给客户端
    case "/":
      createReadStream("./index.html").pipe(res);
      break;
    // 如果请求的是/chat接口的话,执行的就是之前写的ChatGPT接口那一部分的内容了
    case "/chat":
      // 这里是ChatGPT的接口根地址,返回的数据是JSON格式。post默认的是任何格式,但是这里是stream格式
      const { data } = await api.post<Stream>(
        "chat/completions",
        {
          // 这里是ChatGPT的JSON数据
          model: "gpt-3.5-turbo",
          messages: [{ role: "user", content: "你可以讲一个爱情故事吗?" }],
          max_tokens: 100, // 测试的时候加一个max_tokens,目的是控制ChatGPT的输出长度,可以减少请求的时间
          temperature: 0.7,
          stream: true,
        },
        {
          // 设置返回的数据类型为stream。stream是node.js中专门用来处理流数据的。他可以不断的将数据流输出到res响应的对象中,所以它可以实现流式的传输数据
          responseType: "stream",
        }
      );
      data.pipe(res); // 可以直接流向res响应的对象
      break;
  }

19.5如果请求的是其他地址

如果请求的是根地址和chat之外的地址的话,直接返回一个空的字符串

 default:
	// 如果请求的接口地址不在上面定义的接口地址中,就返回一个空的字符串
	res.end('')

19.6注意一点

messages: [{ role: "user", content: "你可以讲一个爱情故事吗?" }]这一部分的内容应该是由前端那边传过来的,所以我们需要把他设置成变量

//   entries()是返回了键值对的一个数组;Object.fromEntries()是将键值对数组转化为一个对象
const query = Object.fromEntries(url.searchParams.entries());
console.log(query);

如下图所示,因为我url后面没有接参数,所以我终端输出的是一个空的对象。为什么会有两个呢?因为一般浏览器还会发一个favicon的请求,所以会多一个空的对象,不用管它

所以下方的请求chat中的message就可以写成query.prompt,但是在写的时候一定要确保query.prompt存在,所以一定要在刚开始加上判断

case "/chat":
	res.setHeader("Content-Type", "text/event-stream");// 一定要加,要不然浏览器控制台会报错,报错信息看下方图片
	if(!query.prompt){
		res.statusCode = 400;
        return res.end(JSON.stringify({
        	message: "请输入提问内容"
        }))
    }
      // 这里是ChatGPT的接口根地址,返回的数据是JSON格式。post默认的是任何格式,但是这里是stream格式
      const { data } = await api.post<Stream>(
        "chat/completions",
        {
          // 这里是ChatGPT的JSON数据
          model: "gpt-3.5-turbo",
          messages: [{ role: "user", content: query.prompt}],// 但是一定要确保prompt是字符串格式且存在,所以在上面加一个判断
          max_tokens: 100, // 测试的时候加一个max_tokens,目的是控制ChatGPT的输出长度,可以减少请求的时间
          temperature: 0.7,
          stream: true,
        },
        {
          // 设置返回的数据类型为stream。stream是node.js中专门用来处理流数据的。他可以不断的将数据流输出到res响应的对象中,所以它可以实现流式的传输数据
          responseType: "stream",
        }
      );
      data.pipe(res); // 可以直接流向res响应的对象
      break;

19.7html中的代码

 <script>
        // 在这里定义一个函数,方便调用
        function send(prompt){
            // 定义一个url,根地址是当前页面的地址
            const url = new URL('/chat',location.href)
            // 将prompt添加到url中,直接可以拼接参数,不需要手写。设置一个参数,键名为prompt,值为上面传进来的prompt
            url.searchParams.set('prompt',prompt)
            // url定义好之后就直接给es就行了
            const es = new EventSource(url)
            // es就可以监听到服务器的推送了。得到一个e是message响应的事件
            es.onmessage = (e) => {
                // e.data就是服务器返回的数据
                console.log(e.data);
            }
        }
        send('你好呀')
    </script>

但是后面你就会发现浏览器中的控制台会报错,这是因为在TS文件里面中的请求chat的那一块少写了东西res.setHeader("Content-Type", "text/event-stream"),这个代码我已经在上面写过了,不过你们可以尝试一下把这句话注了,然后看看报错。当把这个代码加上之后,再刷新一下页面,控制台就没有报错了,但是控制台中会输出一些东西(见下方图片)。但是有一个小bug,就是它会持续不断的输出,因为我们没有关闭它。可以翻到控制台的最后,可以看到[DONE],这个是openai返回的一个特殊的标记,表示这个代码已经完成了

所以我们可以在es.onmessage中加一个判断,当输出[DONE]的时候就关闭es,这样就不会有重复的内容了

es.onmessage = (e) => {
	// e.data就是服务器返回的数据
	console.log(e.data);
	if(e.data ==='[DONE]'){
	// 当服务器推送[DONE]时,关闭es
	return es.close();
}

19.8返回字符串

从浏览器的控制台中可得知,我们每次输出的都是一个对象,所以这里我们把对象中输出的内容拿出来

es.onmessage = (e) => {
	// 定义一个data变量,将服务器返回的数据转换为JSON格式
	const data = JSON.parse(e.data);
	// 获取content。从浏览器的控制台中可以看到服务器返回的数据,里面的内容就在choices下面的delta中的content里。如果content为空,则给一个空的字符串
	const {content = ''} = data.choices[0].delta;
	console.log(content);
}
send('你好呀')

如图所示,这就是我们可以看到的一个实时输出的一个效果

19.9在页面输出

在html中用标签输出,给他设一个id。然后在js代码中获取一下这个标签,在进行连接之前需要清空一下之前的记录,然后在收到任何content后,把这些内容加到这个标签当中。

<!-- 这里用了pre标签,因为它可以保留换行符,方便查看 -->
<pre id="out"></pre>

<script>
// 获取输出元素
const out = document.getElementById('out');
// 在连接之前清空之前的记录
out.innerHTML = ''

es.onmessage = (e) => {
	const {content = ''} = data.choices[0].delta;
	// 然后在收到任何一个content之后把他加进去
	out.innerHTML += content;
	console.log(content);
}
send('你好呀')
</script>

19.10用文本框的形式输出

<!-- 一定要把文本框放到pre的上面,要不然可能会导致答案在上面 -->
<input type="text" id="input">

<script>
 const input = document.getElementById('input');
// 给input加一个事件监听
input.onkeydown = e => {
	// 按下回车键,就触发事件
	if (e.keyCode === 13) {
		// 获取输入框的值
		send(e.target.value)
	}
}
</script>

20.全部代码

TS代码

// 从http中导入createServer方法,目的是创建一个web服务
import { createServer } from "http";
// 导入axios
import axios from "axios";
// 导入dotenv
import "dotenv/config";
// 导入socks-proxy-agent
import { SocksProxyAgent } from "socks-proxy-agent";
// 导入stream
import { Stream } from "stream";
// 导入fs
import { createReadStream } from "fs";

const socksProxy = process.env.SOCKS_PROXY;

// 检查 socksProxy 是否为 undefined
if (socksProxy === undefined) {
  throw new Error("SOCKS_PROXY environment variable is not defined");
}

// 创建axios的一个api实例
const api = axios.create({
  baseURL: "https://api.openai.com/v1", //这里写的就是ChatGPT的接口地址
  timeout: 5000, // 可以不写
  headers: {
    "Content-Type": "application/json", // 这个可以不写,重要的是下面的Authorization
    Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, // 这里是咱们自己的token
  },
  //   因为我们请求的是https,所以需要设置代理
  httpsAgent: new SocksProxyAgent(socksProxy),
});

createServer(async (req, res) => {
  // console.log(req.url);// 来看一下req的url,看他请求的是什么
  // 因为如果地址后面有参数的话,会被忽略掉,所以我们需要将url转化为一个URL对象
  const url = new URL(req.url!, "file:///");
//   entries()是返回了键值对的一个数组;Object.fromEntries()是将键值对数组转化为一个对象
  const query = Object.fromEntries(url.searchParams.entries());
  console.log(query);
  
  // return res.end(url.pathname),// 这里输出url的pathname,也就是请求的接口地址,他就会是一个纯路径

  // 做个判断,看看请求的是什么接口,这个url中会包含接口的很多信息
  switch (url.pathname) {
    // 让数据以流的形式返回给客户端
    case "/":
      createReadStream("./index.html").pipe(res);
      break;
    // 如果请求的是/chat接口的话,执行的就是之前写的ChatGPT接口那一部分的内容了
    case "/chat":
        res.setHeader("Content-Type", "text/event-stream");// 一定要加,要不然浏览器控制台会报错
        if(!query.prompt){
            res.statusCode = 400;
            return res.end(JSON.stringify({
                message: "请输入提问内容"
            }))
        }
      // 这里是ChatGPT的接口根地址,返回的数据是JSON格式。post默认的是任何格式,但是这里是stream格式
      const { data } = await api.post<Stream>(
        "chat/completions",
        {
          // 这里是ChatGPT的JSON数据
          model: "gpt-3.5-turbo",
          messages: [{ role: "user", content: query.prompt}],// 但是一定要确保prompt是字符串格式且存在,所以在上面加一个判断
        //   max_tokens: 100, // 测试的时候加一个max_tokens,目的是控制ChatGPT的输出长度,可以减少请求的时间
          temperature: 0.7,
          stream: true,
        },
        {
          // 设置返回的数据类型为stream。stream是node.js中专门用来处理流数据的。他可以不断的将数据流输出到res响应的对象中,所以它可以实现流式的传输数据
          responseType: "stream",
        }
      );
      data.pipe(res); // 可以直接流向res响应的对象
      break;
    default:
        // 如果请求的接口地址不在上面定义的接口地址中,就返回一个空的字符串
      res.end('')
  }

  //res.end无法直接返回JSON格式,需要利用JSON.stringify()转成字符串。但是因为ChatGPT的返回是stream格式,所以需要用pipe方法将stream转成字符串,这里就把JSON给注销了
  //   res.end(JSON.stringify(data));
}).listen(9002); // 这个9001是监听的端口号

console.log("http://localhost:9002");

env代码

OPENAI_API_KEY = 你自己的key
# 这里我把key放到了.env文件(也就是环境变量)中,这样在代码中就不需要写key了
# 但是如果要在代码中写key,就需要把key写到代码中,这样不好,所以我把key写到了.env文件中,这样在代码中就不需要写key了
# 这里的key是之前在OpenAI官网申请的,也就是群里那个文档中的东西,每个人都有的

# 这里写的是猫的地址
SOCKS_PROXY = socks5://127.0.0.1:7890

html代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>流式响应</title>
    <style>
        #input {
            width: 80%;
            padding: 8px;
            margin-top: 10px;
        }

        #out {
            white-space: pre-wrap;
            text-align: left;
            max-width: 80%;
            /* margin: 20px auto; */
            padding: 10px;
            /* border: 1px solid #ccc; */
        }
    </style>
</head>
<body>
    <!-- 一定要把文本框放到pre的上面,要不然可能会出现问题 -->
    <input type="text" id="input">
    <!-- 这里用了pre标签,因为它可以保留换行符,方便查看 -->
    <pre id="out"></pre>
    

    <script>
        // 获取输出元素
        const out = document.getElementById('out');
        const input = document.getElementById('input');

        // 给input加一个事件监听
        input.onkeydown = e => {
            // 按下回车键,就触发事件
            if (e.keyCode === 13) {
                // 获取输入框的值
                send(e.target.value)
            }
        }


        // 在这里定义一个函数,方便调用
        function send(prompt){
            // 定义一个url,根地址是当前页面的地址
            const url = new URL('/chat',location.href)
            // 将prompt添加到url中,直接可以拼接参数,不需要手写。设置一个参数,键名为prompt,值为上面传进来的prompt
            url.searchParams.set('prompt',prompt)
            // url定义好之后就直接给es就行了,发起请求就是在这一步。new了一个EventSource就是发起
            const es = new EventSource(url)

            // 在连接之前清空之前的记录
            out.innerHTML = ''


            // es就可以监听到服务器的推送了。得到一个e是message响应的事件。onmessage是服务端响应的过程中,每一次响应都会触发onmessage事件;直到输出[DONE],然后return es.close(),把这个页面的监听关掉。
            // 注意:这里的e是message事件,不是message响应的事件。
            es.onmessage = (e) => {
                // e.data就是服务器返回的数据
                // console.log(e.data);
                if(e.data ==='[DONE]'){
                    // 当服务器推送[DONE]时,关闭es。这就意味着服务器EventSource已经推送完毕了。
                    return es.close();
                }
                // 定义一个data变量,将服务器返回的数据转换为JSON格式
                const data = JSON.parse(e.data);
                // 获取content。从浏览器的控制台中可以看到服务器返回的数据,里面的内容就在choices下面的delta中的content里。如果content为空,则给一个空的字符串
                const {content = ''} = data.choices[0].delta;
                // 然后在收到任何一个content之后把他加进去
                out.innerHTML += content;
                console.log(content);
            }
        }
        // send('你好呀')
    </script>
</body>
</html>

转载请注明出处或者链接地址:https://www.qianduange.cn//article/7670.html
标签
chatgpt
评论
会员中心 联系我 留言建议 回顶部
复制成功!