引言

最近, 人工智能技能在各行各业都有了广泛的运用, 其中最受欢迎的之一便是 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 官网

在 Koa 中基于 gpt-3.5 模型实现一个最基本的流式问答 DEMO

右上角挑选 Personal -> View API keys 进入 API keys 办理页面

在 Koa 中基于 gpt-3.5 模型实现一个最基本的流式问答 DEMO

挑选创立即可

二、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 格局数据

在 Koa 中基于 gpt-3.5 模型实现一个最基本的流式问答 DEMO

正常运用中咱们调用接口时是需求带上 上下文 的, 这样咱们的 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 恳求是不太相同的

在 Koa 中基于 gpt-3.5 模型实现一个最基本的流式问答 DEMO

3.1 SSE 简述

SSE(Server-Sent Events, 服务器推送事情) 经过 HTTP 协议完结服务器到客户端的 单向通讯 的一种办法, 服务端向客户端推送流数据, 客户端与服务端树立起一个 长链接 , 接纳客户端推送的 数据。

服务端发送的不是一次性的数据包, 而是一个 数据流, 会接连不断地发送过来; 这时, 客户端不会封闭衔接, 会一直等着服务器发过来的新的数据流, 有点类似 视频播放、直播数据推送; 本质上, 这种通讯便是以 信息的办法, 完结服务端和客户端长期的 单向通讯

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

在 Koa 中基于 gpt-3.5 模型实现一个最基本的流式问答 DEMO

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}\n\n`); // 写入数据(推送数据)
    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]: value\n, 其中 field 可选字段有:

  1. data: 表明推送数据内容, 格局为 data: message \n\n 也是上面代码用到的格局, 一起假如数据很长, 能够分成多行, 最后一行用 \n\n 完毕, 前面每行都用 \n 完毕, 如下代码所示
// 数据拆成多行, 一起进行推送
data: begin message\n
data: continue message\n\n
// 下面是一个发送 JSON 数据的比如
data: {\n
data: "foo": "bar",\n
data: "baz", 555\n
data: }\n\n
  1. event: 用于界说事情的类型, 默许是 message 事情, 浏览器(客户端)能够用 addEventListener() 来监听该事情, 如下代码创造了三条信息, 第一条事情名为 foo, 将触发浏览器的 foo 事情; 第二条未界说事情名, 表明默许类型, 将触发浏览器的 message 事情; 第三条事情名为 close, 将触发浏览器的 close 事情
event: foo\n
data: a foo event\n\n
data: an unnamed event\n\n
event: close\n
data: close connect\n\n
  1. id: 界说每条数据的 ID, 浏览器能够经过 lastEventId 特点读取到这个值, 一旦衔接断线, 浏览器会发送一个 HTTP 头, 里面包含一个特殊的 Last-Event-ID 头信息, 将这个值发送回来, 能够用来帮助服务器端重建衔接
id: msg1\n
data: message\n\n
  1. retry: 用于指定浏览器从头建议衔接的时间距离, 两种状况会导致浏览器从头建议衔接, 一种是时间距离到期, 二是因为网络过错等原因, 导致衔接犯错
retry: 10000\n

除了上面几种格局, 还支持 冒号(:) 最初的行表明注释, 一般服务器为了保证衔接不中止, 每隔一段时间就会向浏览器发送一个注释

3.3 客户端

客户端经过 EventSource 目标来树立 SSE 衔接, 如下代码: 当咱们履行 new EventSource(url) 创立目标时, 将会与 url 对应的服务树立长衔接

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

在 Koa 中基于 gpt-3.5 模型实现一个最基本的流式问答 DEMO

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);
});

在 Koa 中基于 gpt-3.5 模型实现一个最基本的流式问答 DEMO

假如服务端自界说了事情类型 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', () => {
 // ....
})

四、简易版 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);
});

上面代码履行成果如下:

在 Koa 中基于 gpt-3.5 模型实现一个最基本的流式问答 DEMO

本节参阅文档:

  • 官方文档描述
  • Issue with using ‘stream’ option in TypeScript with CreateChatCompletionResponse
  • How to use stream: true?

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]\n\n');
        return;
      }
      // 解析数据
      const parsed = JSON.parse(message);
      // 写入流
      stream.write(`data: ${parsed.choices[0].delta.content || ''}\n\n`);
    } catch (e) {
      // 呈现过错, 完毕流
      stream.write('data: [DONE]\n\n');
    }
  });
};
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>
  );
};

下图是以上演示代码的一个演示作用

上图能够看出 gpt-3.5-turbo 模型回来的是 Markdown 格局的内容, 在完结运用中咱们还需求运用 Markdown 解析器将其转为 HTML

五、总结

本文首要目的是要演示前后端怎样完结一个基于 gpt-3.5-turbo 模型的一个问答 DEMO, 若真要想完结一个完整的 CharGpt 咱们或许还需求许多工作, 文本就不作更多的赘述了

六、参阅

  • Can Server Sent Events (SSE) with EventSource pass parameter by POST

  • Server-Sent Events 教程

本文正在参加「金石方案」