如何在 Node 中使用 OpenAI 的 gpt-3.5-turbo 模型

释放双眼,带上耳机,听听看~!
本文介绍了如何在 Node 中使用 OpenAI 的 gpt-3.5-turbo 模型,包括获取 API keys,调用接口,以及使用 SSE 实现服务器端流式推送数据的方法。同时也介绍了如何打通前后端,完成一个基本的问答系统。

引言

最近, 人工智能技术在各行各业都有了广泛的应用, 其中最受欢迎的之一便是 ChatGPT 了, 随后 OpenAI 也开放了对应的 gpt-3.5-turbo 模型, 想着在个人项目「昆仑虚」 接入 OpenAI 本篇是前期调研后整理出来的所有内容, 通过本文可以了解到:

  1. 如何在 Node 中使用 gpt-3.5-turbo 模型?
  2. 如何通过 SSE 实现服务端 流式 推送数据?
  3. Koa 中如何实现 SSE?
  4. 如何使用 gpt-3.5-turbo 模型, 打通前后端, 完成一个最基本问答?

一、获取 api-keys

登录 openai 官网

如何在 Node 中使用 OpenAI 的 gpt-3.5-turbo 模型

右上角选择 Personal -> View API keys 进入 API keys 管理页面

如何在 Node 中使用 OpenAI 的 gpt-3.5-turbo 模型

选择创建即可

二、Node 如何执行

Node 我们可以使用官方提供的 openai 依赖包, 来调用 API, 下面代码是一个简单的 DEMO, 代码拷自 官网文档

import { Configuration, OpenAIApi  } from 'openai';

const configuration = new Configuration({
  apiKey: '你的 API KEY',
});

const openai = new OpenAIApi(configuration);

const completion = await openai.createChatCompletion({
  model: 'gpt-3.5-turbo',
  messages: [{ role: 'user', content: 'js Map 类型怎么用' }],
});

console.log(completion.data.choices[0].message);

上面代码执行结果有: 返回 Markdown 格式数据

如何在 Node 中使用 OpenAI 的 gpt-3.5-turbo 模型

正常应用中我们调用接口时是需要带上 上下文 的, 这样我们的 AI 才能更准确的回答我们的问题, 这里我们可以通过 messages 将所有上下文信息带上

const completion = await openai.createChatCompletion({
  model: 'gpt-3.5-turbo',
  messages: [
    { role: 'user', content: 'js Map 类型怎么用' },   // 用户提问(通过 role 区分)
    { role: 'assistant', content: 'JS 中 Map..........' }, // AI 回答(通过 role 区分)
    { role: 'user', content: 'Map 有哪些应用场景呢' }, // 用户提问(通过 role 区分)
    ....
  ],
});

三、Server-Sent Events(SSE) 简介

玩过 ChatGpt 的同学都知道在 ChatGptAI 并不是 一次性 给出所以回答, 而是 逐句逐句 给出问题答案的, 可能你会认为这个只是前端做了动画效果, 但是实际上并不是的, 这里是用到了 SSE 技术, 查看 ChatGptNetWork 会发现它和常规的 GETPOST 请求是不太一样的

如何在 Node 中使用 OpenAI 的 gpt-3.5-turbo 模型

3.1 SSE 简述

SSE(Server-Sent Events, 服务器推送事件) 通过 HTTP 协议实现服务器到客户端的 单向通信 的一种方式, 服务端向客户端推送流数据, 客户端与服务端建立起一个 长链接 , 接收客户端推送的 数据。

服务端发送的不是一次性的数据包, 而是一个 数据流, 会连续不断地发送过来; 这时, 客户端不会关闭连接, 会一直等着服务器发过来的新的数据流, 有点类似 视频播放、直播数据推送; 本质上, 这种通信就是以 信息的方式, 实现服务端和客户端长时间的 单向通信

SSEWebSocket 作用相似, 都是建立浏览器与服务器之间的通信渠道, 然后服务器向浏览器推送信息; 但是呢 WebSocket 更强大和灵活, 因为它是全 双工通道, 可以 双向通信; SSE 是单向通道, 只能服务器向浏览器发送, 因为流信息本质上就是下载; SSE 很适合用于实现日志推送、数据大屏数据推送等场景

如何在 Node 中使用 OpenAI 的 gpt-3.5-turbo 模型

3.2 Koa 实现 SSE

Koa 中实现 SSE 并不复杂, 只需要完成三件事即可:

  1. 设置对应 API 相关响应头
  2. 将响应体设置为 stream 类型
  3. 不断往 stream 写入数据(向客户端推送数据)
// 发送消息
const sendMessage = async (stream) => {
  const data = [
    '现在科学技术的发展速度叫人惊叹',
    '同样在数码相机的技术创新上',
    '随着数码相机越来越普及',
    '数码相机现已成为大家生活中不可缺少的电子产品',
    '而正是因为这样,技术的创新也显得尤为重要',
  ];

  // 循环上面数组: 推送数据、休眠 2 秒
  for (const value of data) {
    stream.write(`data: ${value}nn`); // 写入数据(推送数据)
    await new Promise((resolve) => setTimeout(resolve, 2000));
  }

  // 结束流
  stream.end();
};

router.get('/demo', async (ctx) => {
  // 1. 设置响应头
  ctx.set({
    'Connection': 'keep-alive',
    'Cache-Control': 'no-cache',
    'Content-Type': 'text/event-stream', // 表示返回数据是个 stream
  });

  // 2. 创建流、并作为接口数据进行返回
  const stream = new PassThrough();
  ctx.body = stream;
  ctx.status = 200;

  // 3. 推送流数据
  sendMessage(stream, ctx);
});

上面 写入数据格式为为 [field]: valuen, 其中 field 可选字段有:

  1. data: 表示推送数据内容, 格式为 data: message nn 也是上面代码用到的格式, 同时如果数据很长, 可以分成多行, 最后一行用 nn 结尾, 前面每行都用 n 结尾, 如下代码所示
// 数据拆成多行, 同时进行推送
data: begin messagen
data: continue messagenn

// 下面是一个发送 JSON 数据的例子
data: {n
data: "foo": "bar",n
data: "baz", 555n
data: }nn
  1. event: 用于定义事件的类型, 默认是 message 事件, 浏览器(客户端)可以用 addEventListener() 来监听该事件, 如下代码创造了三条信息, 第一条事件名为 foo, 将触发浏览器的 foo 事件; 第二条未定义事件名, 表示默认类型, 将触发浏览器的 message 事件; 第三条事件名为 close, 将触发浏览器的 close 事件
event: foon
data: a foo eventnn

data: an unnamed eventnn

event: closen
data: close connectnn
  1. id: 定义每条数据的 ID, 浏览器可以通过 lastEventId 属性读取到这个值, 一旦连接断线, 浏览器会发送一个 HTTP 头, 里面包含一个特殊的 Last-Event-ID 头信息, 将这个值发送回来, 可以用来帮助服务器端重建连接
id: msg1n
data: messagenn
  1. retry: 用于指定浏览器重新发起连接的时间间隔, 两种情况会导致浏览器重新发起连接, 一种是时间间隔到期, 二是由于网络错误等原因, 导致连接出错
retry: 10000n

除了上面几种格式, 还支持 冒号(:) 开头的行表示注释, 通常服务器为了保证连接不中断, 每隔一段时间就会向浏览器发送一个注释

3.3 客户端

客户端通过 EventSource 对象来建立 SSE 连接, 如下代码: 当我们执行 new EventSource(url) 创建对象时, 将会与 url 对应的服务建立长连接

const source = new EventSource('http://127.0.0.1:4000/demo');

如何在 Node 中使用 OpenAI 的 gpt-3.5-turbo 模型

EventSource 对象自带 open error message 事件: 如果需要为 EventSource 对象绑定事件可通过 onopen onerror onmessage 等方法, 当然也可以通过 addEventListener() 方法来绑定事件

  1. open 当客户端与服务端建立连接时触发
  2. error 当连接出现错误时触发
  3. message 当接收到服务端推送数据时, 并且当事件类型为 message 时将触发该事件
// 通过 new EventSource 开启 SSE
const source = new EventSource('http://127.0.0.1:4000/demo');

source.onopen = () => {
  console.log('建立连接');
};

source.onerror = (err) => {
  console.log('连接出错:', err);
};

source.onmessage = (event) => {
  console.log('推送数据:', event);
};

// 上面代码等价于:
source.addEventListener('open', () => {
  console.log('建立连接');
});

source.addEventListener('error', (err) => {
  console.log('连接出错:', err);
});

source.addEventListener('message', (event) => {
  console.log('推送数据:', event);
});

如何在 Node 中使用 OpenAI 的 gpt-3.5-turbo 模型

如果服务端自定义了事件类型 A, 客户端可通过 addEventListener() 来监听该事件

source.addEventListener('A', (event) => {
  console.log('推送数据:', event);
});

除了上面几个事件外, EventSource 对象还有下面两个属性、一个方法:

  1. EventSource.readyState: 只读属性, 代表连接状态, 可能值有 CONNECTING(0) OPEN(1) CLOSED(2)
  2. EventSource.url: 只读属性,代表事件源的 URL
  3. EventSource.close(): 用于主动关闭连接, 如果目前处于连接中, 则关闭连接, 并设置readyState 属性为 CLOSED, 如果连接已经被关闭, 该方法不会进行任何操作

补充: 在 Koa 中可通过以下方法来监听客户端关闭请求的事件

ctx.req.on('close', () => {
 // ....
})

3.4 使用「Fetch」处理「API」流数据

是滴, 你没有看错, 现在大部分浏览器都已支持在 Fetch 中处理 流数据 了, 如下代码所示:

const handle = async () => {
  // 1. 请求接口
  const response = await fetch('http://127.0.0.1:4000/demo');
  const reader = response.body.getReader(); // 获取reader
  const decoder = new TextDecoder(); // 文本解码器

  // 2. 循环取值
  while (true) {
    // 取值, value 是后端返回流信息, done 表示后端结束流的输出
    const { value, done } = await reader.read();
    if (done) break;
    // 打印值: 对 value 进行解码
    console.log('推送数据', decoder.decode(value));
  }
};

handle();

如何在 Node 中使用 OpenAI 的 gpt-3.5-turbo 模型

EventSource 最大的限制可能就是不支持 POST 请求了, 使用 Fect 就可以解决该问题, 支持所以请求参数的设置, 相对来说就很灵活了

const handle = async () => {
  // 1. 请求接口
  const response = await fetch('http://127.0.0.1:4000/demo', {
    method: 'POST',
    headers: {},
    body: JSON.stringify({ name: 'moyuanjun' }),
  });
  const reader = response.body.getReader(); // 获取reader
  const decoder = new TextDecoder(); // 文本解码器

  // 2. 循环取值
  while (true) {
    // 取值, value 是后端返回流信息, done 表示后端结束流的输出
    const { value, done } = await reader.read();
    if (done) break;
    // 打印值: 对 value 进行解码
    console.log('推送数据', decoder.decode(value));
  }
};

handle();

四、简易版 ChartGpt

了解了什么是 SSE 接下来我们来实现一个简易版的 ChartGpt

4.1 开启 openai stream 配置

上文中我们演示了如何使用 openai 来使用 gpt-3.5-turbo 获取数据, 但是我们执行代码时接口是一次性返回数据的, 需要等待好久; 其实我们可以通过设置 stream 等于 true, 让接口按照流的方式来推送数据, 下面是一个简单的演示 DEMO

import { Configuration, OpenAIApi  } from 'openai';

const configuration = new Configuration({
  apiKey: '你的 API KEY',
});

const openai = new OpenAIApi(configuration);

// 开启 stream 配置并设置 responseType
const completion = await openai.createChatCompletion({
  stream: true,
  model: 'gpt-3.5-turbo',
  messages: [{ role: 'user', content: 'js Map 类型怎么用' }],
},  { responseType: 'stream' });

// 监听事件
completion.data.on('data', (data) => {
  // 对每次推送的数据进行格式化, 得到的是 JSON 字符串、或者 [DONE] 表示流结束
  const message = data
    .toString()
    .trim()
    .replace(/^data: /, '');

  // 流结束
  if (message === '[DONE]') {
    console.log('流结束');
    return;
  }

  // 解析数据
  const parsed = JSON.parse(message);

  // 打印有效内容
  console.log('=>', parsed.choices[0].delta.content);
});

上面代码执行结果如下:

如何在 Node 中使用 OpenAI 的 gpt-3.5-turbo 模型

本节参考文档:

4.2 和 Koa 结合使用

如下代码: 抽离封装了 useOpenai() 方法, 方法接收两个参数 streammessages, streamKoa 接口返回的流, messages 则是 openai 对应参数, 方法内调用 openai 接口, 将读到的流数据写入 stream

import { PassThrough } from 'stream';
import { Configuration, OpenAIApi  } from 'openai';

// 封装方法
const writeStream =  async ({ stream, messages }) => {
  const configuration = new Configuration({
    apiKey: '你的 API KEY',
  });

  const openai = new OpenAIApi(configuration);

  // 开启 stream 配置并设置 responseType
  const completion = await openai.createChatCompletion({
    messages,
    stream: true,
    model: 'gpt-3.5-turbo',
  },  { responseType: 'stream' });

  // 监听事件
  completion.data.on('data', (data) => {
    try {
      // 对每次推送的数据进行格式化, 得到的是 JSON 字符串、或者 [DONE] 表示流结束
      const message = data
        .toString()
        .trim()
        .replace(/^data: /, '');

      // 流结束
      if (message === '[DONE]') {
        stream.write('data: [DONE]nn');
        return;
      }

      // 解析数据
      const parsed = JSON.parse(message);

      // 写入流
      stream.write(`data: ${parsed.choices[0].delta.content || ''}nn`);
    } catch (e) {
      // 出现错误, 结束流
      stream.write('data: [DONE]nn');
    }
  });
};

export default async (ctx) => {
  const stream = new PassThrough();

  ctx.set({
    'Connection': 'keep-alive',
    'Cache-Control': 'no-cache',
    'Content-Type': 'text/event-stream',
  });

  ctx.body = stream;
  ctx.status = 200;

  // 写入流: 调用 openai 往 stream 不断写入流
  writeStream({ 
    stream, 
    messages: [{ role: 'user', content: ctx.request.query.message }] 
  });
};

4.3 客户端接口调用演示

如下代码简单演示了如何在 React 中发起一个 SSE 连接, 并在页面回显

import { useEffect, useState } from 'react';

export default () => {
  const [answers, setAnswers]  = useState([]);

  useEffect(() => {
    // 通过 new EventSource 开启 SSE
    const source = new EventSource('http://127.0.0.1:4000/demo?message=JS Map 类型');

    source.addEventListener('open', () => {
      console.log('建立连接');
    });

    source.addEventListener('error', (err) => {
      console.log('连接出错:', err);
      source.close();
    });

    source.addEventListener('message', (event) => {
      // 结束则关闭链接
      if (event.data.trim() === '[DONE]') {
        source.close();
      }

      setAnswers((pre) => [...pre, event.data || '']);
    });
  }, []);

  return (
    <div>
      答复:
      {answers.join('')}
    </div>
  );
};

下图是以上演示代码的一个演示效果

如何在 Node 中使用 OpenAI 的 gpt-3.5-turbo 模型

上图可以看出 gpt-3.5-turbo 模型返回的是 Markdown 格式的内容, 在实现运用中我们还需要使用 Markdown 解析器将其转为 HTML

五、总结

本文主要目的是要演示前后端如何实现一个基于 gpt-3.5-turbo 模型的一个问答 DEMO, 若真要想实现一个完整的 CharGpt 我们可能还需要很多工作, 文本就不作更多的赘述了

六、参考

本文正在参加「金石计划」

本网站的内容主要来自互联网上的各种资源,仅供参考和信息分享之用,不代表本网站拥有相关版权或知识产权。如您认为内容侵犯您的权益,请联系我们,我们将尽快采取行动,包括删除或更正。
AI教程

迷你大脑和AI系统的语音识别能力大幅提升,类脑研究突破

2023-12-19 15:59:00

AI教程

Mistral AI 完成 4.15 亿美元 A 轮融资并开放商业化平台

2023-12-19 16:02:00

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索