ChatGPT是如何实现逐字输出的

之前写过一篇『实现接口流式响应数据』提到了服务端如何分多次将数据传给前端,前端如何接收一点数据处理一点。其应用场景主要是对话式应用,细心的同学可能已经发现它和 ChatGPT 中使用的技术还不太一样,在上一篇文章中响应使用的是application/octet-stream看看浏览器的网络面板

再看看 ChatGPT 的请求响应信息

在这张图上发现 chatgpt 的请求的响应进类型使用的是text/event-stream 就是这这篇文章的主角

Server-sent events

服务器发送事件,顾名思义,是由服务端发送数据给前端,简称 SSE。在客户端通过特定事件来接受数据。通常情况下,客户端想要从服务端获取数据,需要向服务端发送请求,附加特定参数,在服务处理完成之后,将数据响应给客户端。一次请求只能响应一次数据。在没有请求时,服务端是无法主要向客户端发送数据的。如果需要服务端主要发送数据,则一般会用 websocket, 这是一种双向通信的协议,连接一量建立以后,客户端和服务端均可以主动向对方发送数据。而 SSE 不同,它使用的依然是 http协议,建立连接之后,只能由服务端向客户端发送数据。而要使用 SSE 就需要服务端在响应数据的时候,将类型设置为text/event-stream 并且响应的数据也需要按照指定的格式来发送,客户在收到数据后按规范解析,同时触发相应的事件

EventSource

EventSource是浏览器提供的一个用于和服务器建立连接,接收服务器发送事件的接口。其基本用法如下

js 复制代码
const eventSource = new EventSource('/api/ask2'); 
eventSource.addEventListener('message', (e) => {  
  resultContainer.innerText += e.data; 
})  

通过 EventSource 构造函数创建一个实例,将会对指定的 url 的 http 服务开启持久连接,服务端以text/event-stream 格式发送事件及数据。此连接将会一直保持开启,直到通过调用 EventSource 的实例的 close 方法关闭

数据格式

服务器除了要设置响应头Content-Type: text/event-stream 之外 ,发送的数据也是有固定格式的

服务器按行发送数据。每行的数据都是以下格式

makefile 复制代码
field: value 

其中 field 可以是event , data , id , retry

不是此格式的数据都会被客户端忽略,通过空行来分割多个 message。每遇到一个空行都会将空行前的数据作为一个 message 触发相应的事件

makefile 复制代码
data: 初始化数据
id: 1

event: update
data: 更新数据

上面的数据将会触发两次事件,第一次事件名为默认的 message, 数据就是 data 部分的内容,第二次触发的事件名为指定的 update, 数据也是data部分的内容,如果一个 message 内, data, id, retry 部分都没有,这个 message 也变得没有意义,会被忽略。

自动重试

当连接建立后,如果因为某些原因,连接和服务端的连接断开了,客户端会自动尝试重新建立连接,同时会携带一个特殊的请求头Last-Event-ID, 这个值会在以前触发的事件中查找,从最后一次有 id 传回的消息中获取,使用 lastEventId 属性名从事件对象中获取。在上面的示例中,如果第二个消息触发后,连接中断,那么自动重试的时候,请求关中将会增加 Last-Event-ID: 1

当连接中断,客户端并不是立即重连,而是在等待一断时间后再发起重连。而这等待时间正是通过 retry 指定的,单位是 ms, 在服务端发送事件时可以通过 retry: 5000 来告诉客户端如果连接中断,需要等5s 后再发起重连。

请求方式

虽然浏览器给我们提供了 EventSource 接口,但是使用 EventSource 建立的连接使用的是 get 方式,如果仔细观察 ChatGPT 的请求信息,将会发现它使用的 Post 请求

这就说明 ChatGPT 并不是使用的浏览器提供的EventSource 接口,而使用的 Azure/fetch-event-source, 其内部使用 fetch 发送请求,同时在接收到数据后按照规则解析,然后触发相应事件

自己动手

虽然已经有很多封装好的库了,但是为了学习,还是自己动手写一个吧。仓库地址leemotive/fetch-event-source

除了支持 EventSource 和 fetch 的接口配置外, 还支持一些额外的配置

参数 类型 默认值 描述
json boolean false 如果 data 为 json 是否自动解析返回的 json
serverEnd boolean false 如果服务端主动关闭连接,客户端不再尝试重连

示例

js 复制代码
const eventSource = new FetchEventSource.default('/api/ask2', {
  json: true, 
  serverEnd: true
}); 
eventSource.addEventListener('msg', (e) => {
  resultContainer.innerText += e.data; 
}) 

服务端代码,midway.js作后端示例,只截取了部分代码

js 复制代码
@Get('/ask2') 
async ask2() {   
  const result = [...'人间四月芳菲尽,山寺桃花始盛开,长恨春归无觅处,不知转入此中来。'];   
  const passthrough = new PassThrough();   
  const push = (i: number) => {     
    if (result[i]) {       
      passthrough.write(`event: msg\ndata:${result[i]}\nid: ${i}\n\n`);       setTimeout(push.bind(null, i + 1), 100);     
    } else {       
      passthrough.write(`data: 结束了\n\n`);       
      passthrough.end(); // 服务端主动断开连接     
    }   
  };    
  setTimeout(() => {     
    passthrough.write('retry: 8000\n\n');     
    push(0);   
  });    
  this.ctx.response.set({     
    'Content-Type': 'text/event-stream',   
  });   
  return passthrough; 
} 
相关推荐
lilu888888836 分钟前
AI代码生成器赋能房地产:ScriptEcho如何革新VR/AR房产浏览体验
前端·人工智能·ar·vr
LCG元39 分钟前
Vue.js组件开发-实现对视频预览
前端·vue.js·音视频
傻小胖40 分钟前
shallowRef和shallowReactive的用法以及使用场景和ref和reactive的区别
javascript·vue.js·ecmascript
阿芯爱编程1 小时前
vue3 react区别
前端·react.js·前端框架
烛.照1031 小时前
Nginx部署的前端项目刷新404问题
运维·前端·nginx
YoloMari1 小时前
组件中的emit
前端·javascript·vue.js·微信小程序·uni-app
CaptainDrake1 小时前
力扣 Hot 100 题解 (js版)更新ing
javascript·算法·leetcode
浪浪山小白兔2 小时前
HTML5 Web Worker 的使用与实践
前端·html·html5
疯狂小料2 小时前
React 路由导航与传参详解
前端·react.js·前端框架
追光少年33223 小时前
Learning Vue 读书笔记 Chapter 2
前端·javascript·vue.js·vue3