如何实现chatgpt的打字机效果

点击↑上方↑蓝色“编了个程”关注我~

5192e0d6d053bf7e41d7955e3d868553.png

这是Yasin的第 88 篇原创文章

c79fc4ce665fd93992e547c41992ec39.png

打字机效果

最近在搭建chat gpt代理的时候,发现自己的配置虽然能够调通接口,返回数据,但是结果是一次性显示出来的,不像之前的chat gpt的官网demo那样实现了打字机效果,一个字一个字出来。

793d411cc03829c75f3713b49adc5c85.png

所以研究了一下chat gpt打字机效果的原理,后续如果要实现类似的效果可以借鉴。

纯前端实现打字机效果

最开始我搜索打字机效果时,出现的结果大多数是纯前端的方案。其原理也很简单,通过js定时把内容输出到屏幕。下面是chat gpt的答案:

前端实现打字机效果可以通过以下步骤:

  1. 将文本内容嵌入到 HTML 元素中,如 div 或 span

  2. 通过 CSS 样式设置元素的显示方式为隐藏(如 display: none;)。

  3. 使用 JavaScript 获取该元素,并逐个显示其中的字符。

  4. 使用定时器(如 setInterval() 函数)控制每个字符的出现时间间隔,从而实现逐个逐个显示的效果。

  5. 当所有字符都被显示后,停止定时器以避免不必要的计算开销。

下面是一个简单的示例代码:

HTML:

<div id="typewriter">Hello World!</div>

CSS:

#typewriter {display: none;
}

JS:

const element = document.getElementById('typewriter');let i = 0;
const interval = setInterval(() => {element.style.display = 'inline';element.textContent = element.textContent.slice(0, i++) + '_';if (i > element.textContent.length) {clearInterval(interval);element.textContent = element.textContent.slice(0, -1);}
}, 100);

该代码会将 idtypewriter 的元素中的文本逐个显示,每个字符之间相隔 100 毫秒。最终显示完毕后,会将最后一个字符的下划线去除。

流式输出

后面我抓包以及查看了chat gpt的官方文档之后,发现事情并没有这么简单。chat gpt的打字机效果并不是后端一次性返回后,纯前端的样式。而是后端通过流式输出不断向前端输出内容。

在chat gpt官方文档中,有一个参数可以让它实现流式输出:

313160788d7cb93c3e27f2d1a4adf18b.png

这是一个叫“event_stream_format”的协议规范。

event_stream_format(简称 ESF)是一种基于 HTTP/1.1 的、用于实现服务器推送事件的协议规范。它定义了一种数据格式,可以将事件作为文本流发送给客户端。ESF 的设计目标是提供简单有效的实时通信方式,以及支持众多平台和编程语言。

ESF 数据由多行文本组成,每行用 \n(LF)分隔。其中,每个事件由以下三部分组成:

  • 事件类型(event)

  • 数据(data)

  • 标识符(id)

例如:

event: message 
data: Hello, world! 
id: 123

这个例子表示一个名为 message 的事件,携带着消息内容 Hello, world!,并提供了一个标识符为 123 的可选参数。

ESF 还支持以下两种特殊事件类型:

  • 注释(comment):以冒号开头的一行,只做为注释使用。

  • 重传(retry):指定客户端重连的时间间隔,以毫秒为单位。

例如:

: This is a commentretry: 10000event: update
data: {"status": "OK"}

ESF 协议还支持 Last-Event-ID 头部,它允许客户端在断线后重新连接,并从上次连接中断处恢复。当客户端连接时,可以通过该头部将上次最新的事件 ID 传递给服务器,以便服务器根据该 ID 继续发送事件。

ESF 是一种简单的、轻量级的协议,适用于需要实时数据交换和多方通信的场景。由于其使用了标准的 HTTP/1.1 协议,因此可以轻易地在现有的 Web 基础设施上实现。

抓包可以发现这个响应长这样:edea49bbe45b0a963a4f0ed476792afa.png

30df52348ba40ffe50f20379bf3f01e9.png

可以看到是data: 加上一个json,每次的流式数据在delta里面。

http response中有几个重要的头:e1c647824f932db35e6ca8b29b4dbf72.png

其中,keep-alive是保持客户端和服务端的双向通信,这个大家应该都比较了解。下面解释一下另外两个头.

这里其实openai返回的是text/event-streamtext/event-stream 是一种流媒体协议,用于在 Web 应用程序中推送实时事件。它的内容是文本格式的,每个事件由一个或多个字段组成,以换行符(\n)分隔。这个 MIME 类型通常用于服务器到客户端的单向通信,例如服务器推送最新的新闻、股票报价等信息给客户端。

我这里使用的开源项目chatgpt-web抓的包,请求被nodejs包了一层,返回了application/octet-stream (不太清楚这么做的动机是什么),它是一种 MIME 类型,通常用于指示某个资源的内容类型为二进制文件,也就是未知的二进制数据流。该类型通常不会执行任何自定义处理,并且可以由客户端根据需要进行下载或保存。

Transfer-Encoding: chunked 是一种 HTTP 报文传输编码方式,用于指示报文主体被分为多个等大小的块(chunks)进行传输。每个块包含一个十六进制数字的长度字段,后跟一个 CRLF(回车换行符),然后是实际的数据内容,最后以另一个 CRLF 结束。

使用 chunked 编码方式可以使服务器在发送未知大小的数据时更加灵活,同时也可以避免一些限制整个响应主体大小的限制。当接收端收到所有块后,会将它们组合起来,解压缩(如果需要),并形成原始的响应主体。

总之,Transfer-Encoding: chunked 允许服务器在发送 HTTP 响应时,动态地生成报文主体,而不必事先确定其大小,从而提高了通信效率和灵活性。

服务端的实现

作为chat gpt代理

如果写一个golang http服务作为chat gpt的代理,只需要循环扫描chat gpt返回的每行结果,每行作为一个事件输出给前端就行了。核心代码如下:

// 设置Content-Type标头为text/event-stream  
w.Header().Set("Content-Type", "text/event-stream")  
// 设置缓存控制标头以禁用缓存  
w.Header().Set("Cache-Control", "no-cache")  
w.Header().Set("Connection", "keep-alive")  
w.Header().Set("Keep-Alive", "timeout=5")  
// 循环读取响应体并将每行作为一个事件发送到客户端  
scanner := bufio.NewScanner(resp.Body)  
for scanner.Scan() {  eventData := scanner.Text()  if eventData == "" {  continue  }  fmt.Fprintf(w, "%s\n\n", eventData)  flusher, ok := w.(http.Flusher)  if ok {  flusher.Flush()  } else {  log.Println("Flushing not supported")  }  
}

自己作为服务端

这里模仿openai的数据结构,自己作为服务端,返回流式输出:

const Text = `  
proxy_cache:通过这个模块,Nginx 可以缓存代理服务器从后端服务器请求到的响应数据。当下一个客户端请求相同的资源时,Nginx 可以直接从缓存中返回响应,而不必去请求后端服务器。这大大降低了代理服务器的负载,同时也能提高客户端访问速度。需要注意的是,使用 proxy_cache 模块时需要谨慎配置缓存策略,避免出现缓存不一致或者过期的情况。  proxy_buffering:通过这个模块,Nginx 可以将后端服务器响应数据缓冲起来,并在完整的响应数据到达之后再将其发送给客户端。这种方式可以减少代理服务器和客户端之间的网络连接数,提高并发处理能力,同时也可以防止后端服务器过早关闭连接,导致客户端无法接收到完整的响应数据。  综上所述, proxy_cache 和 proxy_buffering 都可以通过缓存技术提高代理服务器性能和安全性,但需要注意合理的配置和使用,以避免潜在的缓存不一致或者过期等问题。同时, proxy_buffering 还可以通过缓冲响应数据来提高代理服务器的并发处理能力,从而更好地服务于客户端。  
`  type ChatCompletionChunk struct {  ID      string `json:"id"`  Object  string `json:"object"`  Created int64  `json:"created"`  Model   string `json:"model"`  Choices []struct {  Delta struct {  Content string `json:"content"`  } `json:"delta"`  Index        int     `json:"index"`  FinishReason *string `json:"finish_reason"`  } `json:"choices"`  
}  func handleSelfRequest(w http.ResponseWriter, r *http.Request) {  // 设置Content-Type标头为text/event-stream  w.Header().Set("Content-Type", "text/event-stream")  // 设置缓存控制标头以禁用缓存  w.Header().Set("Cache-Control", "no-cache")  w.Header().Set("Connection", "keep-alive")  w.Header().Set("Keep-Alive", "timeout=5")  w.Header().Set("Transfer-Encoding", "chunked")  // 生成一个uuid  uid := uuid.NewString()  created := time.Now().Unix()  for i, v := range Text {  eventData := fmt.Sprintf("%c", v)  if eventData == "" {  continue  }  var finishReason *string  if i == len(Text)-1 {  temp := "stop"  finishReason = &temp  }  chunk := ChatCompletionChunk{  ID:      uid,  Object:  "chat.completion.chunk",  Created: created,  Model:   "gpt-3.5-turbo-0301",  Choices: []struct {  Delta struct {  Content string `json:"content"`  } `json:"delta"`  Index        int     `json:"index"`  FinishReason *string `json:"finish_reason"`  }{  {               Delta: struct {  Content string `json:"content"`  }{  Content: eventData,  },  Index:        0,  FinishReason: finishReason,  },  },  }  fmt.Println("输出:" + eventData)  marshal, err := json.Marshal(chunk)  if err != nil {  return  }  fmt.Fprintf(w, "data: %v\n\n", string(marshal))  flusher, ok := w.(http.Flusher)  if ok {  flusher.Flush()  } else {  log.Println("Flushing not supported")  }  if i == len(Text)-1 {  fmt.Fprintf(w, "data: [DONE]")  flusher, ok := w.(http.Flusher)  if ok {  flusher.Flush()  } else {  log.Println("Flushing not supported")  }  }      time.Sleep(100 * time.Millisecond)  }  
}

核心是每次写进一行数据data: xx \n\n,最终以data: [DONE]结尾。

前端的实现

前端代码参考https://github.com/Chanzhaoyu/chatgpt-web的实现。

这里核心是使用了axios的onDownloadProgress钩子,当stream有输出时,获取chunk内容,更新到前端显示。

await fetchChatAPIProcess<Chat.ConversationResponse>({  prompt: message,  options,  signal: controller.signal,  onDownloadProgress: ({ event }) => {  const xhr = event.target  const { responseText } = xhr  // Always process the final line  const lastIndex = responseText.lastIndexOf('\n')  let chunk = responseText  if (lastIndex !== -1)  chunk = responseText.substring(lastIndex)  try {  const data = JSON.parse(chunk)  updateChat(  +uuid,  dataSources.value.length - 1,  {  dateTime: new Date().toLocaleString(),  text: lastText + data.text ?? '',  inversion: false,  error: false,  loading: false,  conversationOptions: { conversationId: data.conversationId, parentMessageId: data.id },  requestOptions: { prompt: message, options: { ...options } },  },  )  if (openLongReply && data.detail.choices[0].finish_reason === 'length') {  options.parentMessageId = data.id  lastText = data.text  message = ''  return fetchChatAPIOnce()  }  scrollToBottom()  }  catch (error) {  //  }  },  
})

在底层的请求代码中,设置对应的header和参数,监听data内容,回调onProgress函数。

const responseP = new Promise((resolve, reject) => {  const url = this._apiReverseProxyUrl;  const headers = {  ...this._headers,  Authorization: `Bearer ${this._accessToken}`,  Accept: "text/event-stream",  "Content-Type": "application/json"  };  if (this._debug) {  console.log("POST", url, { body, headers });  }  fetchSSE(  url,  {  method: "POST",  headers,  body: JSON.stringify(body),  signal: abortSignal,  onMessage: (data) => {  var _a, _b, _c;  if (data === "[DONE]") {  return resolve(result);  }  try {  const convoResponseEvent = JSON.parse(data);  if (convoResponseEvent.conversation_id) {  result.conversationId = convoResponseEvent.conversation_id;  }  if ((_a = convoResponseEvent.message) == null ? void 0 : _a.id) {  result.id = convoResponseEvent.message.id;  }  const message = convoResponseEvent.message;  if (message) {  let text2 = (_c = (_b = message == null ? void 0 : message.content) == null ? void 0 : _b.parts) == null ? void 0 : _c[0];  if (text2) {  result.text = text2;  if (onProgress) {  onProgress(result);  }  }  }  } catch (err) {  }  }  },  this._fetch  ).catch((err) => {  const errMessageL = err.toString().toLowerCase();  if (result.text && (errMessageL === "error: typeerror: terminated" || errMessageL === "typeerror: terminated")) {  return resolve(result);  } else {  return reject(err);  }  });  
});

nginx配置

在搭建过程中,我还遇到另一个坑。因为自己中间有一层nginx代理,而「nginx默认开启了缓存,所以导致流式输出到nginx这个地方被缓存了」,最终前端拿到的数据是缓存后一次性输出的。同时gzip也可能有影响。

这里可以通过nginx配置,把gzip和缓存都关掉。

gzip off;location / {proxy_set_header   Host             $host;proxy_set_header   X-Real-IP        $remote_addr;proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;proxy_cache off;proxy_cache_bypass $http_pragma;proxy_cache_revalidate on;proxy_http_version 1.1;proxy_buffering off;proxy_pass http://xxx.com:1234;
}

proxy_cacheproxy_buffering 是 Nginx 的两个重要的代理模块。它们可以显著提高代理服务器的性能和安全性。

  • proxy_cache:通过这个模块,Nginx 可以缓存代理服务器从后端服务器请求到的响应数据。当下一个客户端请求相同的资源时,Nginx 可以直接从缓存中返回响应,而不必去请求后端服务器。这大大降低了代理服务器的负载,同时也能提高客户端访问速度。需要注意的是,使用 proxy_cache 模块时需要谨慎配置缓存策略,避免出现缓存不一致或者过期的情况。

  • proxy_buffering:通过这个模块,Nginx 可以将后端服务器响应数据缓冲起来,并在完整的响应数据到达之后再将其发送给客户端。这种方式可以减少代理服务器和客户端之间的网络连接数,提高并发处理能力,同时也可以防止后端服务器过早关闭连接,导致客户端无法接收到完整的响应数据。

实测只配置proxy_cache没有用,配置了proxy_buffering后流式输出才生效。

a4735ddbcda28cd67876c5f7340ef165.png

关于作者

我是Yasin,一个爱写博客的技术人

微信公众号:编了个程(blgcheng)

个人网站:https://yasinshaw.com

欢迎关注这个公众号d3627f23dc5b43617bed8ac123cd006b.png

1f0d5e1060cd8e7d8754d6cd1e60a032.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/8360.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何使用ChatGPT提升自己的“码”力?

如何使用chatGPT提升自己的"码"力? 代码评审(CodeReview)代码优化代码释义提供解决方案代码生成设计模式和架构建议学习新知识总结 ChatGPT是什么&#xff0c;我就不用再多介绍了吧&#xff01;相信大家已经看到了它在文本领域所展现出来的实力&#xff0c;虽然目前…

云孚快写:自动生成多级目录,一键生成万字长文

1.产品简介 云孚快写是云孚科技自主研发的一款智能写作产品&#xff0c;基于大模型技术打造&#xff0c;针对长文写作场景深度优化&#xff0c;可根据文章标题一键生成目录&#xff0c;再根据目录一键生成正文&#xff0c;文章字数无上限&#xff0c;可极大提升用户的长文写作…

低调且强大--iVX低代码平台

iVX目录 前言一、低代码那么多 为什么选择iVX&#xff1f;二、“拼”出来的低代码平台&#xff0c;真的好用吗&#xff1f;三、iVX与其他低代码有啥可比性&#xff1f; 前言 首先我们应该明白自动编程突破可能是&#xff1a;领域内Mini LLM 现在的思路都是搞LLM&#xff0c;几乎…

多方合作时,系统间的交互是怎么做的?

大家好&#xff01;我是sum墨&#xff0c;一个一线的底层码农&#xff0c;平时喜欢研究和思考一些技术相关的问题并整理成文&#xff0c;限于本人水平&#xff0c;如果文章和代码有表述不当之处&#xff0c;还请不吝赐教。 以下是正文&#xff01; 文章背景 我们最近做了很多…

你真正了解低代码么?(国内低代码平台状况分析)

■ 写在前面■ 低代码产品如何分类&#xff0c;90% 的人都没有搞清楚■ 低代码平台如何比较&#xff1f;Point 在哪儿&#xff1f;一个比喻大家全听懂■ “拼”出来的低代码平台&#xff0c;真的好用吗&#xff1f;■ 推荐一款 C 端低代码产品 ■ 写在前面 都说技术是生产力&a…

【AI实战】给类ChatGPT的大语言模型外挂私有知识库

【AI实战】给类ChatGPT的大语言模型外挂私有知识库 原理准备环境代码下载 chatglm-6b 模型权重文件下载 Embedding 模型 GanymedeNil/text2vec-large-chinese安装依赖库我的计算资源 外挂知识库开启服务外挂知识库 测试参考 本文使用 langChain 来给大语言模型 ChatGLM-6B 外挂…

一块GPU搞定ChatGPT;ML系统入坑指南;理解GPU底层架构

1. 跑ChatGPT体量模型&#xff0c;从此只需一块GPU 在发展技术&#xff0c;让大模型掌握更多能力的同时&#xff0c;也有人在尝试降低AI所需的算力资源。最近&#xff0c;一种名为FlexGen的技术因为「一块RTX 3090跑ChatGPT体量模型」而获得了人们的关注。 虽然FlexGen加速后的…

谷歌研究科学家:ChatGPT秘密武器的演进与局限

来源&#xff5c;TalkRL OneFlow编译 翻译&#xff5c;徐佳渝、贾川 同样是基于GPT预训练模型&#xff0c;为什么ChatGPT的效果要远远超出GPT-3等前几代模型&#xff1f;答案已经揭晓&#xff0c;成就ChatGPT的秘密武器在于RLHF&#xff0c;也就是人类反馈的强化学习。 在预训…

用ChatGPT搞定K8s!

Kubernetes&#xff08;K8s&#xff09;非常火&#xff0c;但被人诟病最多的还是其复杂性&#xff0c;并且不管是在云中还是本地&#xff0c;都没有很好的集群故障排除的方法。因此&#xff0c;尽管K8s的采用率持续增长&#xff0c;但许多开发人员和运维团队对这项较新的技术感…

任正非:ChatGPT对我们的机会是什么,内部讲话实录!

ChatGPT狂飙160天&#xff0c;世界已经不是之前的样子。 我新建了人工智能中文站https://ai.weoknow.com 每天给大家更新可用的国内可用chatGPT资源 为感谢全国火花奖获奖者对于产业界及科学界做出的重大贡献&#xff0c;华为组织了与部分获奖老师与专家的座谈会。座谈会上&…

ChatGPT突遭大面积封号!网友应急出解封教程

一觉醒来&#xff0c;ChatGPT大面积封号的消息突然炸了。 据传闻&#xff0c;受影响的账户已经达到了数百万&#xff0c;亚洲是重灾区&#xff0c;其中不少都是国内的账号。 社交媒体上哀鸿遍野&#xff0c;网友纷纷表示自己的账号忽然就被封禁了&#xff0c;没有得到任何警告…

复旦发布类ChatGPT模型Moss;Linux 6.2 发布|极客头条

「极客头条」—— 技术人员的新闻圈&#xff01; CSDN 的读者朋友们早上好哇&#xff0c;「极客头条」来啦&#xff0c;快来看今天都有哪些值得我们技术人关注的重要新闻吧。 整理 | 梦依丹 出品 | CSDN&#xff08;ID&#xff1a;CSDNnews&#xff09; 一分钟速览新闻点&…

理想汽车CEO李想:要挑战30到50万SUV市场20%份额

雷递网 雷建平 2月28日 理想汽车&#xff08;纳斯达克&#xff1a;LI&#xff1b;香港交易所&#xff1a;2015&#xff09;今日公布财报。财报显示&#xff0c;理想汽车2022年收入总额为452.9亿元&#xff08;65.7亿美元&#xff09;&#xff0c;较2021年的270.1亿元增加67.7%。…

计算机状态下的语言学问题

【赏析】计算机状态下的语言学问题 ——读《传神的汉字》 摘要&#xff1a;两手都要硬--科学与马克思主义 传神的汉字——中国对世界最杰出的第五大发明。    修改为&#xff1a;    1、传神的汉字——中国对世界杰出的第五大发明。    评注&#xff1a;逻辑错误&#…

任正非正面回应!万字问答全文来了,涉及AI、教育、基础科学等多个重磅命题...

来源&#xff1a; 深城物联 3月17日&#xff0c;上海交通大学先进产业技术研究院披露了一篇《擦亮花火、共创未来——任正非在“难题揭榜”花火奖座谈会上的讲话》的文章&#xff0c;全文约1万字&#xff0c;曝光了华为创始人任正非对天才少年、校企合作等热点话题的看法&#…

ChatGPT爆火,AI正在冲击传统教育

ChatGPT横扫校园&#xff1f;遭受封杀&#xff1f; 01.黑马如何诞生 OpenAI开发的聊天机器人ChatGPT发布于2022年11月30日&#xff0c;随即在全球刮起了一股猛烈的旋风。市场研究机构Similarweb数据显示&#xff0c;2023年1月&#xff0c;ChatGPT独立访问用户达1.015亿&#xf…

探讨教育如何应对ChatGPT的冲击

许多年之后&#xff0c;史书写到 AI 时代时&#xff0c;将会提及 2023 年那个狂飙突进的春天。 2月初&#xff0c;瑞银&#xff08;UBS&#xff09;分析师指&#xff0c;ChatGPT 发布仅仅两个月就突破了 1 亿月活用户&#xff0c;成为史上普及最快的消费级应用。 ▲史上各项技…

任正非内部讲话:ChatGPT对我们的机会是什么(实录)

因公众号更改推送规则&#xff0c;请点“在看”并加“星标”第一时间获取精彩技术分享 点击关注#互联网架构师公众号&#xff0c;领取架构师全套资料 都在这里 0、2T架构师学习资料干货分 上一篇&#xff1a;ChatGPT研究框架&#xff08;80页PPT&#xff0c;附下载&#xff09;…

任正非最新谈中美、科技、ChatGPT

我们还是要把科学和技术分开&#xff0c;如果一讲做事要有目的性&#xff0c;目的性就是技术&#xff0c;不是科学。科学就是你的兴趣爱好&#xff0c;为了搞清楚不惜一切代价 文 | 任正非 近日&#xff0c;华为在深圳坂田总部隆重举办“难题揭榜”火花奖颁奖典礼&#xff0c;为…

第20课:解决痛点|如何让ChatGPT记住指令并随时调用呢?

经常使用ChatGPT的人,会有一些烦恼,比如如何把调试好的一些prompt 管理起来?如何在使用ChatGPT的时候随时调用我之前输入过的历史指令呢?等等吧。。。 我们先把这个问题抛给ChatGPT来看看它是怎么回复的? 从ChatGPT的回答,我们可以获取到一些信息,ChatGPT本身并不具备…