chatgpt逐字输出原理及实现

1,993 阅读2分钟

当我们在使用gpt时,会发现他的回复不是一下子显示,而是一段段输出,很有意思,那我们能不能模拟出这种效果呢? 首先,我们分析一下他的请求

image.png

可以看到,他发送的post请求中,返回的是EventStream

EventStream是什么

EventStream为事件流处理,服务端返回的数据以流的形式传输。
由此可见,GPT使用的是SSE的技术传输。

developer.mozilla.org/en-US/docs/…

如果我们来实现类似的效果,应该怎么实现呢。

需求分析

通过观察接口,我们可以发现有两个feature image.png

  • 使用了sse技术
  • 请求为post请求

服务端实现

我们先来模拟服务端,这里我们以koa为例

import koa from 'koa';
import Router from 'koa-router';
import bodyParser from 'koa-bodyparser';
import { PassThrough } from 'stream';



const app = new koa();
const router = new Router();
app.use(bodyParser(
    {
        enableTypes: ['json', 'form', 'text']
    }
));

app.use(router.routes());



router.post('/sse', async (ctx) => {
    const { message } = JSON.parse(ctx.request.body);
    // sse 
    ctx.set({
        'Access-Control-Allow-Origin': '*',
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive'
    });
    const sseStream = new PassThrough()
    ctx.body = sseStream

    sendEvent()
    async function sendEvent() {
        let count = 0;
        const timer = setInterval(() => {
            sseStream.write(JSON.stringify({data: message}))
            count++;
            if (count === 5) {
                clearInterval(timer)
                sseStream.end()
            }
        }, 300)
    }
})
app.listen(3000, () => {
    console.log('server is running at http://localhost:3000');
})

client 端实现

那我们应该怎么接收服务端传过来的消息呢

  • 接收推送过来的消息
  • 发送一个post请求
  • 传统的eventSource只能接受get请求

这时候,我们可以找到一个微软的一个库Fetch Event Source github.com/Azure/fetch… image.png 我们可以简单的实现功能 image.png


如果我们想要自己实现,应该怎么实现呢 通过参考源码,我们可以写出一个简易实现的版本

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="output"></div>
    <div>
      <button>发送请求</button>
    </div>

    <script>
      const output = document.getElementById("output");
      const button = document.querySelector("button");

      button.addEventListener("click", () => {
        const controller = new AbortController();
        fetchEventSource("http://127.0.0.1:3000/sse", {
          method: "POST",
          mode: "cors",
          body: JSON.stringify({
            message: "Hello JueJin",
          }),
          signal: controller.signal,
          onopen() {
            output.innerHTML += "连接已建立<br>";
          },
          onmessage(event) {
            const { data } = JSON.parse(event);
            output.innerHTML += `收到数据:${data}<br>`;
          },
          onerror(error) {
            output.innerHTML += `连接已断开:${error}<br>`;
          },
          onclose() {
            output.innerHTML += "连接已关闭<br>";
          },
        });
      });
      function fetchEventSource(url, options) {
        curRequestController = new AbortController();
        options.signal?.addEventListener("abort", () => {
          dispose();
        });
        fetch(url, { ...options, signal: curRequestController.signal })
          .then((response) => {
            if (response.status === 200) {
              options.onopen && options.onopen();
              return response.body;
            }
          })
          .then((readableStream) => {
            const reader = readableStream.getReader();
            const push = () => {
              return reader.read().then(({ done, value }) => {
                if (done) {
                  options.onclose && options.onclose();
                  return;
                }
                options.onmessage &&
                  options.onmessage(new TextDecoder().decode(value));

                return push();
              });
            };
            return push();
          })
          .catch((error) => {
            console.log(error);
            dispose();
            options.onerror && options.onerror(error);
          });
        function dispose() {
          curRequestController.abort();
        }
      }
    </script>
  </body>
</html>

通过调试,我们可以实现如下的效果: Untitled.gif

总结

通过上述的实现和分析,我们能够了解gpt 逐字回复的原理,以及我们通过观察源码,简易的模拟出这个过程。