用户在使用由大模型驱动的应用时,例如ChatGPT(OpenAI)、Gemini(Google)、Deepseek、豆包(字节)和元宝(腾讯),常常会注意到一种引人入胜的“打字机效果”。文本似乎是一个字一个字(更准确地说,是一个token一个token)地逐渐显现,营造出一种类似人类实时打字的互动感和模型以人类似的方式即时生成响应的印象。虽然这种效果在用户界面上增强了交互体验,但它并非仅仅是一个视觉上的技巧,而是 大模型(LLM)底层文本生成方式的直接体现。

语言的基本单元——Token

要理解 LLM 如何产生这种“打字机效果”,首先需要认识到这些模型并非像人类那样直接处理词语,而是以更细粒度的单位——“token”为基础进行操作。文本可以按照不同的粒度进行切分和表示,这个过程称为 tokenization。之前写过一篇科普的文章《大模型中常说的 token 和 Tokenizer 是指什么?》专门介绍过这个知识,这里再稍微讲一下。

常见的 tokenization 方法包括:词语级别、字符级别和子词级别。词语级别的 tokenization 是最直观的方法,它根据空格和标点符号将文本分割成单独的词语。例如,“The quick brown fox jumps.”会被分割成。字符级别的 tokenization 则将文本中的每一个字符(包括字母、数字、空格和标点符号)都视为一个独立的 token。对于同样的句子,字符级别的 tokenization 会产生。

在现代LLM中,子词级别的 tokenization 是一种更为复杂且广泛应用的技术。这种方法旨在平衡词语级别和字符级别 tokenization 的优缺点,将词语分解成更小但更频繁出现的单元。例如,“unbreakable”可能会被分解成[“un”, “break”, “able”]。在子词tokenization的技术中,字节对编码(Byte-Pair Encoding, BPE)被包括 OpenAI、Gemini和 Deepseek 在内的许多主流 LLM 所采用。BPE 的优势在于能够有效地处理罕见词或词汇表外的词语,通过将它们分解成已知的子词来表示,同时还能减小模型的整体词汇量。在使用 LLM 时,用户输入的文本(即prompt)首先会经过 tokenization 处理,转换成模型能够理解的数字 token 序列。同样地,LLM 生成的输出也是一个 token 序列,需要通过 detokenization 的过程转换回人类可读的文本。

因此,用户所观察到的“打字机效果”本质上是这些生成的 token 被顺序地呈现或揭示的过程。根据 LLM 所采用的具体 tokenization 策略,在“打字”的每一个步骤中出现的可能是一个完整的词语、一个词语的片段(子词)甚至是单个字符。理解这一点对于解读模型的可视化输出至关重要。值得注意的是,由于不同的 LLM 可能使用不同的 tokenization 方法和维护着各自独特的 token 词汇表,因此在不同的平台之间可能会观察到“打字机效果”在粒度和视觉呈现上的细微差异。例如,一个模型可能倾向于以较大的文本块(完整的词语或频繁出现的子词)进行“打字”,而另一个模型由于采用了不同的 tokenization 策略,可能会呈现出更接近于逐字符出现的效果。另外,由于底层模型的输出直接与前端交互可能存在安全隐患问题,如暴露 API 密钥,通常采用“双重流式传输”模式,也就是 LLM 将数据流式传输到后端,然后后端再将数据重新流式传输到前端,大家最终看到的“打字机效果其实并不能真实的反映出底层 LLM 的 tokenization 策略。

Server-Sent Events (SSE) 协议

在 LLM 应用中,Server-Sent Events (SSE) 协议是实现服务器向客户端流式传输数据的主要技术之一。SSE 之所以被广泛采用,在于其简洁性、对标准 HTTP 的依赖以及对于单向数据流的高效性。LLM 的响应主要是从服务器发送到客户端的,SSE 的单向特性与这种通信模式非常契合,与需要双向通信的协议(如 WebSockets)相比,开销更小。

从技术原理上看,SSE 基于标准的 HTTP 连接工作。客户端发起一个 HTTP 请求后,服务器会保持连接打开,以便在有新的事件可用时随时推送给客户端,这是一个长连接的过程。服务器在响应时会使用 text/event-stream 的 MIME 类型,这个类型告知客户端这是一个事件流。

每个 SSE 事件都遵循特定的结构:

  • data: 这是事件的实际载荷,可以包含多行文本。
  • event: 这是一个可选的限定符,用于定义事件的类型,例如可以是 “ping” 这样的心跳事件,也可以是应用自定义的事件类型。通过 event 字段,可以在同一个 SSE 连接上发送不同类型的消息,从而实现更丰富的通信。
  • id: 这是每个事件的可选标识符。id 字段帮助客户端跟踪事件的顺序,并可以在连接中断后用于恢复流。
  • retry: 这是一个可选的重连时间,以毫秒为单位。retry 字段允许服务器建议客户端在连接丢失后尝试重新连接的时间间隔,这个过程通常由浏览器内置的 EventSource API 自动处理。

主流大模型平台的流式响应实现

OpenAI

OpenAI 的 API 通过在 API 请求中设置 stream=True 参数来支持流式响应,这个参数可以在调用 Chat Completions 接口时使用。一旦启用流式传输,API 会将响应以数据块的形式增量地发送回客户端,每个数据块都是一个仅包含数据的服务器发送事件。开发者可以使用 for 循环等方式遍历这个事件流,并逐个处理接收到的数据块。在每个流式响应的数据块中,会包含一个 delta 字段,这个字段存储着模型新生成的增量内容。

Gemini

Gemini 也提供了流式传输功能,可以通过 generateContent 方法并设置 stream=True 来实现。值得一提的是,Gemini 还提供了 Multimodal Live API,该 API 使用 WebSockets 来实现低延迟的双向语音和视频交互。不过,对于更简单的文本流式传输场景,SSE 仍然可能是一个合适的选择。

Deepseek

Deepseek 的 API 同样支持流式响应,通过将 stream 参数设置为 true 即可启用。Deepseek API 与 OpenAI API 格式兼容。其实大多数 LLM 基本都是兼容 OpenAI格式的,gemini 现在也已经兼容,主要方便开发者切换到不同模型,也算是一个行业标准吧。

其他流式传输技术简介

除了 SSE 之外,还有其他一些技术可以用于流式传输数据。WebSockets 是一种双向通信协议,它在客户端和服务器之间建立一个持久的全双工连接,允许双方随时发送和接收数据。WebSockets 更适合需要双向实时交互的应用场景,例如聊天应用或协同编辑工具。

轮询(Polling)是另一种数据传输方法,它包括客户端定期向服务器查询是否有新的数据可用。轮询可以分为短轮询和长轮询。短轮询是客户端以固定的时间间隔发送请求,而长轮询是客户端发送请求后,如果服务器没有新数据,则保持连接打开,直到有新数据才发送响应,然后连接关闭。客户端收到响应后会立即再次发送请求。与 SSE 和 WebSockets 相比,轮询效率较低,因为它会产生不必要的网络流量,尤其是在服务器没有新数据时。

下表总结了这几种流式传输技术的关键特性:

特性Server-Sent Events (SSE)WebSockets轮询 (长/短)
通信方向单向(服务器到客户端)双向单向(客户端发起请求)
底层协议HTTPWebSocket 协议HTTP
连接类型持久连接持久连接重复连接
效率较高,适合单向数据流较高,适合双向数据流较低
复杂性较低较高较低
适用场景(包括 LLM 流式传输适用性)实时更新(如 LLM 响应),通知实时双向交互(如聊天),游戏简单场景,作为 SSE/WebSockets 的备选方案(LLM 流式传输不太适用)

SSE 在大模型流式响应中的优势与局限

在 LLM 流式响应的场景中,SSE 具有多项显著的优势,当然也存在一些局限性:

优势描述局限描述
简单易用设置和实现相对简单,客户端使用 EventSource API单向通信只支持服务器向客户端发送数据
高效的单向数据传输适合服务器到客户端的文本数据流HTTP/1.1 连接限制每个域名可能存在连接数限制(通常为 6 个)
广泛的浏览器支持现代主流浏览器都原生支持数据格式限制主要用于文本数据,二进制数据支持有限
易于集成基于标准 HTTP,兼容现有基础设施
自动重连浏览器自动处理连接断开和重连
较低的服务器负载无数据时不会产生额外的请求

最后

大模型应用回复的“打字机效果”,即流式响应通过逐个生成并增量传输 token 的方式,极大地改善了用户与大型语言模型的交互体验。Server-Sent Events (SSE) 作为一种简单而高效的协议,在实现这一目标中发挥着核心作用。