您当前的位置:首页 > 电脑百科 > 程序开发 > 编程百科

ChatGPT是如何做到一个字一个字输出的?

时间:2023-05-27 14:13:04  来源:今日头条  作者:赵帅虎
大语言模型生成响应整个结果的过程是比较漫长的,但逐token生成的响应比较快,ChatGPT将这一特性与SSE技术充分结合,一个字一个字地弹出回复,在用户体验上实现了质的提升。

ChatGPT回答问题时,是一个字一个字弹出的,给人一种在认真思考的感觉。

这段对话的基本内容是:

问:大语言生成式模型是如何工作的
答:大型语言生成模型(如GPT-3)是基于深度学习技术的神经网络模型。这些模型通常使用一种称为"Transformer"的架构,该架构在自然语言处理领域取得了巨大的成功。

下面是大型语言生成模型的工作原理概述:

... 省略一段 ...

生成文本:一旦模型经过训练,就可以使用它来生成文本。给定一个初始文本或提示,模型会根据已经学到的概率分布生成下一个单词,然后使用生成的单词作为输入,并继续生成下一个单词,以此类推,直到达到所需的文本长度或生成终止符号。

... 省略一段 ...

大语言模型,Large Language Model,简称LLM。

从模型的视角来看,LLM每进行一次推理生成一个token,直到达到文本长度限制或生成终止符。

从服务端的视角来看,生成的token需要通过HTTPS协议逐个返回到浏览器端。

Client-Server 模式下,常规的交互方式是client端发送一次请求,接收一次响应。显然,这无法满足ChatGPT回复问题的场景。

其次,我们可能想到websocket,它依赖HTTP实现握手,升级成WebSocket。不过WebSocket需要client和server都持续占用一个socket,server侧成本比较高。

ChatGPT使用的是一种折衷方案: server-sent event(简称SSE). 我们从AI target=_blank class=infotextkey>OpenAI的 API 文档可以发现这一点:

SSE 模式下,client只需要向server发送一次请求,server就能持续输出,直到需要结束。整个交互过程如下图所示:

SSE仍然使用HTTP作为应用层传输协议,充分利用HTTP的长连接能力,实现服务端推送能力。

从代码层面来看,SSE模式与单次HTTP请求不同的点有:

  1. client端需要开启 keep-alive,保证连接不会超时。
  2. HTTP响应的Header包含 Content-Type=text/event-stream,Cache-Cnotallow=no-cache 等。
  3. HTTP响应的body一般是 "data: ..." 这样的结构。
  4. HTTP响应里可能有一些空数据,以避免连接超时。

以 ChatGPT API 为例,在发送请求时,将stream参数设置为true就启用了SSE特性,但在读取数据的SDK里需要稍加注意。

在常规模式下,拿到 http.Response 后,用 ioutil.ReadAll 将数据读出来即可,代码如下:

func main() {
  payload := strings.NewReader(`{
    "model": "gpt-3.5-turbo",
    "messages": [{"role": "user", "content": "大语言生成式模型是如何工作的"}],
    "max_tokens": 1024,
    "temperature": 1,
    "top_p": 1,
    "n": 1,
    "stream": false
}`)

  client := &http.Client{}
  req, _ := http.NewRequest("POST", "https://api.openai.com/v1/chat/completions", payload)
  req.Header.Add("Content-Type", "Application/json")
  req.Header.Add("Authorization", "Bearer <OpenAI-Token>")

  resp, err := client.Do(req)
  if err != nil {
    fmt.Println(err)
    return
  }
  defer resp.Body.Close()

  body, _ := ioutil.ReadAll(resp.Body)
  fmt.Println(string(body))
}

执行大概耗费20s+,得到一个完整的结果:

{
  "id": "chatcmpl-7KklTf9mag5tyBXLEqM3PWQn4jlfD",
  "object": "chat.completion",
  "created": 1685180679,
  "model": "gpt-3.5-turbo-0301",
  "usage": {
    "prompt_tokens": 21,
    "completion_tokens": 358,
    "total_tokens": 379
  },
  "choices": [
    {
      "message": {
        "role": "assistant",
        "content": "大语言生成式模型通常采用神经网络来实现,具体工作流程如下:nn1. 数据预处理:将语料库中的文本数据进行预处理,包括分词、删除停用词(如“的”、“了”等常用词汇)、去重等操作,以减少冗余信息。nn2. 模型训练:采用递归神经网络(RNN)、长短期记忆网络(LSTM)或变种的Transformers等模型进行训练,这些模型都具有一定的记忆能力,可以学习到语言的一定规律,并预测下一个可能出现的词语。nn3. 模型应用:当模型完成训练后,可以将其应用于实际的生成任务中。模型接收一个输入文本串,并预测下一个可能出现的词语,直到达到一定长度或遇到结束符号为止。nn4. 根据生成结果对模型进行调优:生成的结果需要进行评估,如计算生成文本与语料库文本的相似度、流畅度等指标,以此来调优模型,提高其生成质量。nn总体而言,大语言生成式模型通过对语言的规律学习,从而生成高质量的文本。"
      },
      "finish_reason": "stop",
      "index": 0
    }
  ]
}

如果我们将 stream 设置为 true,不做任何修改,请求总消耗28s+,体现为很多条 stream 消息:

上面这张图是一张Postman调用 chatgpt api的图,走的就是 ioutil.ReadAll 的模式。为了实现stream读取,我们可以分段读取 http.Response.Body。下面是这种方式可行的原因:

  1. http.Response.Body 的类型是 io.ReaderCloser,底层依赖一个HTTP连接,支持stream读。
  2. SSE 返回的数据通过换行符n进行分割

所以修正的方法是通过bufio.NewReader(resp.Body)包装起来,并在一个for-loop里读取, 代码如下:

// stream event 结构体定义
type ChatCompletionRspChoiceItem struct {
  Delta        map[string]string `json:"delta,omitempty"` // 只有 content 字段
  Index        int               `json:"index,omitempty"`
  Logprobs     *int              `json:"logprobs,omitempty"`
  FinishReason string            `json:"finish_reason,omitempty"`
}

type ChatCompletionRsp struct {
  ID      string                        `json:"id"`
  Object  string                        `json:"object"`
  Created int                           `json:"created"` // unix second
  Model   string                        `json:"model"`
  Choices []ChatCompletionRspChoiceItem `json:"choices"`
}

func main() {
  payload := strings.NewReader(`{
    "model": "gpt-3.5-turbo",
    "messages": [{"role": "user", "content": "大语言生成式模型是如何工作的"}],
    "max_tokens": 1024,
    "temperature": 1,
    "top_p": 1,
    "n": 1,
    "stream": true
}`)

  client := &http.Client{}
  req, _ := http.NewRequest("POST", "https://api.openai.com/v1/chat/completions", payload)
  req.Header.Add("Content-Type", "application/json")
  req.Header.Add("Authorization", "Bearer "+apiKey)
  req.Header.Set("Accept", "text/event-stream")
  req.Header.Set("Cache-Control", "no-cache")
  req.Header.Set("Connection", "keep-alive")

  resp, err := client.Do(req)
  if err != nil {
    fmt.Println(err)
    return
  }
  defer resp.Body.Close()

  reader := bufio.NewReader(resp.Body)
  for {
    line, err := reader.ReadBytes('n')
    if err != nil {
      if err == io.EOF {
        // 忽略 EOF 错误
        break
      } else {
        if.NETErr, ok := err.(net.Error); ok && netErr.Timeout() {
          fmt.Printf("[PostStream] fails to read response body, timeoutn")
        } else {
          fmt.Printf("[PostStream] fails to read response body, err=%sn", err)
        }
      }
      break
    }
    line = bytes.TrimSuffix(line, []byte{'n'})
    line = bytes.TrimPrefix(line, []byte("data: "))
    if bytes.Equal(line, []byte("[DONE]")) {
      break
    } else if len(line) > 0 {
      var chatCompletionRsp ChatCompletionRsp
      if err := json.Unmarshal(line, &chatCompletionRsp); err == nil {
        fmt.Printf(chatCompletionRsp.Choices[0].Delta["content"])
      } else {
        fmt.Printf("ninvalid line=%sn", line)
      }
    }
  }

  fmt.Println("the end")
}

看完client端,我们再看server端。现在我们尝试mock chatgpt server逐字返回一段文字。这里涉及到两个点:

  1. Response Header 需要设置 Connection 为 keep-alive 和 Content-Type 为 text/event-stream。
  2. 写入 respnose 以后,需要flush到client端。

代码如下:

func streamHandler(w http.ResponseWriter, req *http.Request) {
  w.Header().Set("Connection", "keep-alive")
  w.Header().Set("Content-Type", "text/event-stream")
  w.Header().Set("Cache-Control", "no-cache")

  var chatCompletionRsp ChatCompletionRsp
  runes := []rune(`大语言生成式模型通常使用深度学习技术,例如循环神经网络(RNN)或变压器(Transformer)来建模语言的概率分布。这些模型接收前面的词汇序列,并利用其内部神经网络结构预测下一个词汇的概率分布。然后,模型将概率最高的词汇作为生成的下一个词汇,并递归地生成一个词汇序列,直到到达最大长度或遇到一个终止符号。

在训练过程中,模型通过最大化生成的文本样本的概率分布来学习有效的参数。为了避免模型产生过于平凡的、重复的、无意义的语言,我们通常会引入一些技巧,如dropout、序列扰动等。
  
大语言生成模型的重要应用包括文本生成、问答系统、机器翻译、对话建模、摘要生成、文本分类等。`)
  for _, r := range runes {
    chatCompletionRsp.Choices = []ChatCompletionRspChoiceItem{
      {Delta: map[string]string{"content": string(r)}},
    }

    bs, _ := json.Marshal(chatCompletionRsp)
    line := fmt.Sprintf("data: %sn", bs)
    fmt.Fprintf(w, line)
    if f, ok := w.(http.Flusher); ok {
      f.Flush()
    }

    time.Sleep(time.Millisecond * 100)
  }

  fmt.Fprintf(w, "data: [DONE]n")
}

func main() {
  http.HandleFunc("/stream", streamHandler)
  http.ListenAndServe(":8088", nil)
}

在真实场景中,要返回的数据来源于另一个服务或函数调用,如果这个服务或函数调用返回时间不稳定,可能导致client端长时间收不到消息,所以一般的处理方式是:

  1. 对第三方的调用放到一个 goroutine 中。
  2. 通过 time.Tick 创建一个定时器,向client端发送空消息。
  3. 创建一个timeout channel,避免响应时间太久。

为了能够从不同的channel读取数据,select 是一个不错的关键字,比如这段演示代码:

// 声明一个 event channel
// 声明一个 time.Tick channel
// 声明一个 timeout channel

select {
case ev := <-events:
  // send data event
case <- timeTick:
  // send empty event
case <-timeout:
    fmt.Fprintf(w, "[Done]nn")
}

小结一下

大语言模型生成响应整个结果的过程是比较漫长的,但逐token生成的响应比较快,ChatGPT将这一特性与SSE技术充分结合,一个字一个字地弹出回复,在用户体验上实现了质的提升。

纵观生成式模型,不管是LLAMA/小羊驼 (不能商用),还是Stable Diffusion/Midjourney。在提供线上服务时,均可利用SSE技术节省提升用户体验,节省服务器资源。



Tags:ChatGPT   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
ChatGPT官宣免注册,全球互联网变天!OpenAI将取代谷歌搜索?
新智元报道编辑:编辑部【新智元导读】OpenAI这份愚人节礼物,实在是太大了:今天起,ChatGPT不用注册,可以直接使用。用户狂欢,竞品颤抖,我们仿佛已经听到,谷歌搜索引擎这位巨人轰然倒...【详细内容】
2024-04-02  Search: ChatGPT  点击:(9)  评论:(0)  加入收藏
无需注册!OpenAI宣布放开ChatGPT使用限制
工智能初创公司OpenAI宣布,即日起用户无须注册即可开始使用ChatGPT的功能。OpenAI在最新公告中写道:“让ChatGPT等工具广泛可用,让人们能够体验到人工智能的好处,这是我们使命的...【详细内容】
2024-04-02  Search: ChatGPT  点击:(7)  评论:(0)  加入收藏
ChatGPT 突然放开了账户限制,面向所有人开放
大门终于打开。奥特曼 OpenAI 的旗舰产品 ChatGPT 突然宣布:将面向所有人开放,无论你有没有注册账户。从今天开始,访问 Chat.openai.com 将不再要求用户登录,用户将直接进入与 C...【详细内容】
2024-04-02  Search: ChatGPT  点击:(3)  评论:(0)  加入收藏
今天起,ChatGPT无需注册就能用了!
 来源:量子位    金磊 克雷西 发自 凹非寺  就在刚刚,OpenAI狠狠地open了一把:从今天起,ChatGPT打开即用,无需再注册帐号和登录了!  像这样,直接登录网站,然后就可以开启对...【详细内容】
2024-04-02  Search: ChatGPT  点击:(8)  评论:(0)  加入收藏
ChatGPT之父Altman两小时对谈,首聊GPT-5何时发布、llya去哪里了、Q*究竟是什么
Altman做客油管博主Lex Fridman科技博客 ,被追问了一个又一个辛辣的问题。长达两个小时的对谈,奥特曼从OpenAI宫斗、马斯克诉讼、Sora,一直聊到AGI与外星文明!本文重点梳理了长...【详细内容】
2024-03-20  Search: ChatGPT  点击:(8)  评论:(0)  加入收藏
ChatGPT主管最新访谈:未来AI和人类如何共处?
Peter Deng在最新访谈中表示:AI不会取代生产力,人类和AI只有合作才能释放真正潜力,ChatGPT比已知的更强大、最大的挑战在于理解用户需求。当地时间3月13日,OpenAI 消费产品副总...【详细内容】
2024-03-19  Search: ChatGPT  点击:(11)  评论:(0)  加入收藏
ChatGPT日耗电超50万度,大模型或带来“电荒”
未来两年内将由“缺硅”变为“缺电”,马斯克的预言可能正在变成现实。据《纽约客》杂志报道,OpenAI的热门聊天机器人ChatGPT每天可能要消耗超过50万千瓦时的电力,以响应用户的...【详细内容】
2024-03-11  Search: ChatGPT  点击:(32)  评论:(0)  加入收藏
OpenAI新功能:ChatGPT可调用自定义机器人,对话更高效!
近日,知名科技公司OpenAI推出了一项新的功能&mdash;&mdash;“对话中调用(@)自定义聊天机器人”,让用户在对话中无缝切换不同领域的机器人。这一功能的出现,为用户带来了极大的便...【详细内容】
2024-02-02  Search: ChatGPT  点击:(53)  评论:(0)  加入收藏
ChatGPT元年之后,AI重塑世界,人类如何与其“智慧共生”?
过去一年,人工智能(AI)凭借大语言模型的爆火迅速进入大众视野。它比以往任何时候都更强大,也更具亲和力。这不仅给未来生活带来了新希望,也在人们心中蒙上了一层担忧&mdash;&mdas...【详细内容】
2024-01-26  Search: ChatGPT  点击:(77)  评论:(0)  加入收藏
年度最热AI应用TOP 50,除了ChatGPT还有这么多宝藏
量子位 | 公众号 QbitAI百模齐发、AI工具乱杀的一年里,谁是真正赢家?ChatGPT访问量遥遥领先位居第一,但单次使用时长没超过平均线。Midjourney访问量年度第四,但下滑量位居第二...【详细内容】
2024-01-02  Search: ChatGPT  点击:(54)  评论:(0)  加入收藏
▌简易百科推荐
即将过时的 5 种软件开发技能!
作者 | Eran Yahav编译 | 言征出品 | 51CTO技术栈(微信号:blog51cto) 时至今日,AI编码工具已经进化到足够强大了吗?这未必好回答,但从2023 年 Stack Overflow 上的调查数据来看,44%...【详细内容】
2024-04-03    51CTO  Tags:软件开发   点击:(6)  评论:(0)  加入收藏
跳转链接代码怎么写?
在网页开发中,跳转链接是一项常见的功能。然而,对于非技术人员来说,编写跳转链接代码可能会显得有些困难。不用担心!我们可以借助外链平台来简化操作,即使没有编程经验,也能轻松实...【详细内容】
2024-03-27  蓝色天纪    Tags:跳转链接   点击:(13)  评论:(0)  加入收藏
中台亡了,问题到底出在哪里?
曾几何时,中台一度被当做“变革灵药”,嫁接在“前台作战单元”和“后台资源部门”之间,实现企业各业务线的“打通”和全域业务能力集成,提高开发和服务效率。但在中台如火如荼之...【详细内容】
2024-03-27  dbaplus社群    Tags:中台   点击:(9)  评论:(0)  加入收藏
员工写了个比删库更可怕的Bug!
想必大家都听说过删库跑路吧,我之前一直把它当一个段子来看。可万万没想到,就在昨天,我们公司的某位员工,竟然写了一个比删库更可怕的 Bug!给大家分享一下(不是公开处刑),希望朋友们...【详细内容】
2024-03-26  dbaplus社群    Tags:Bug   点击:(5)  评论:(0)  加入收藏
我们一起聊聊什么是正向代理和反向代理
从字面意思上看,代理就是代替处理的意思,一个对象有能力代替另一个对象处理某一件事。代理,这个词在我们的日常生活中也不陌生,比如在购物、旅游等场景中,我们经常会委托别人代替...【详细内容】
2024-03-26  萤火架构  微信公众号  Tags:正向代理   点击:(11)  评论:(0)  加入收藏
看一遍就理解:IO模型详解
前言大家好,我是程序员田螺。今天我们一起来学习IO模型。在本文开始前呢,先问问大家几个问题哈~什么是IO呢?什么是阻塞非阻塞IO?什么是同步异步IO?什么是IO多路复用?select/epoll...【详细内容】
2024-03-26  捡田螺的小男孩  微信公众号  Tags:IO模型   点击:(9)  评论:(0)  加入收藏
为什么都说 HashMap 是线程不安全的?
做Java开发的人,应该都用过 HashMap 这种集合。今天就和大家来聊聊,为什么 HashMap 是线程不安全的。1.HashMap 数据结构简单来说,HashMap 基于哈希表实现。它使用键的哈希码来...【详细内容】
2024-03-22  Java技术指北  微信公众号  Tags:HashMap   点击:(11)  评论:(0)  加入收藏
如何从头开始编写LoRA代码,这有一份教程
选自 lightning.ai作者:Sebastian Raschka机器之心编译编辑:陈萍作者表示:在各种有效的 LLM 微调方法中,LoRA 仍然是他的首选。LoRA(Low-Rank Adaptation)作为一种用于微调 LLM(大...【详细内容】
2024-03-21  机器之心Pro    Tags:LoRA   点击:(12)  评论:(0)  加入收藏
这样搭建日志中心,传统的ELK就扔了吧!
最近客户有个新需求,就是想查看网站的访问情况。由于网站没有做google的统计和百度的统计,所以访问情况,只能通过日志查看,通过脚本的形式给客户导出也不太实际,给客户写个简单的...【详细内容】
2024-03-20  dbaplus社群    Tags:日志   点击:(4)  评论:(0)  加入收藏
Kubernetes 究竟有没有 LTS?
从一个有趣的问题引出很多人都在关注的 Kubernetes LTS 的问题。有趣的问题2019 年,一个名为 apiserver LoopbackClient Server cert expired after 1 year[1] 的 issue 中提...【详细内容】
2024-03-15  云原生散修  微信公众号  Tags:Kubernetes   点击:(6)  评论:(0)  加入收藏
站内最新
站内热门
站内头条