使用 FastAPI 构建生成式 AI 服务——与生成模型的实时通信

在本章中,你将学习以下内容:

  • 何时在 AI 工作流中实现实时通信
  • 通过比较它们的特性、差异和相似性,了解 Web 通信机制
  • 实时通信机制,包括服务器推送事件(SSE)和 WebSocket(WS)
  • 根据你的使用场景选择正确的流媒体技术
  • 从头开始实现模拟流媒体端点用于测试和原型设计
  • 使用 SSE 和 WebSocket 机制实现实时 API 端点
  • 如何优雅地处理异常并关闭流媒体连接
  • 简化流媒体端点的 API 设计模式

本章将探讨 AI 流媒体工作负载,如聊天机器人,详细介绍如何使用实时通信技术,如 SSE 和 WebSocket。你将了解这些技术之间的区别,以及如何通过构建端点实现模型流媒体,特别是实时文本到文本的交互。

Web 通信机制

在上一章中,你学习了通过利用异步编程、后台任务和连续批处理来实现 AI 工作流中的并发。通过并发,你的服务在多个用户同时访问应用时,能够更好地应对增加的需求。并发解决了允许同时用户访问服务的问题,并有助于减少等待时间,但 AI 数据生成仍然是一个资源密集型且耗时的任务。

到目前为止,你一直在使用传统的 HTTP 通信构建端点,其中客户端向服务器发送请求,Web 服务器处理传入请求并通过 HTTP 消息进行响应。

图 6-1 显示了客户端-服务器架构。

由于 HTTP 协议是无状态的,服务器将每个传入的请求视为完全独立且与其他请求无关。这意味着来自不同客户端的多个传入请求不会影响服务器对每个请求的响应。例如,在一个不使用数据库的对话式 AI 服务中,每个请求可能会提供完整的对话历史,并从服务器获取正确的响应。

HTTP 请求-响应模型是一个广泛采用的 API 设计模式,由于其简单性,被广泛应用于 Web 上。然而,当客户端或服务器需要实时更新时,这种方法就变得不够用了。

在标准的 HTTP 请求-响应模型中,服务通常会在完全处理完用户请求后进行响应。然而,如果数据生成过程既漫长又缓慢,用户将不得不等待很长时间,并随后一次性接收大量信息。想象一下,你正在与一个聊天机器人对话,机器人需要几分钟才能回应,回应后你却看到一大段令人难以消化的文本。

作为替代方案,如果你在数据生成的过程中将数据提供给客户端,而不是等到整个生成过程完成后再提供,你可以减少长时间的延迟,并将信息以易于消化的块呈现。这种方法不仅可以提升用户体验,还能在请求处理过程中保持用户的参与感。

然而,实施实时功能有时可能会适得其反,增加开发负担。例如,一些开源模型或 API 缺乏实时生成能力。此外,添加数据流式端点会增加服务器和客户端两边的系统复杂性。这意味着需要以不同的方式处理异常,并管理并发连接,以避免内存泄漏。如果客户端在流式传输过程中断开,可能会导致数据丢失或服务器与客户端之间的状态漂移。此外,你可能需要实现复杂的重新连接和状态管理逻辑,以处理连接中断的情况。

维护大量并发的开放连接也会给服务器带来负担,并导致托管和基础设施成本的增加。

同样重要的是,你还需要考虑如何扩展以处理大量并发流的能力、应用程序的延迟要求以及浏览器对你选择的流媒体协议的兼容性。

注意

与传统的 Web 应用程序相比,AI 应用程序除了拥有某种形式的 I/O 或数据处理延迟外,还具有 AI 模型推理延迟,具体取决于你使用的模型。

由于这种延迟可能很大,你的 AI 服务应该能够处理服务器和客户端两侧的更长等待时间,并包括管理用户体验。

如果你的用例确实需要实时功能,那么你可以实现几种架构设计模式:

  • 常规/短轮询
  • 长轮询
  • SSE
  • WebSocket (WS)

选择取决于你的用户体验、可扩展性、延迟、开发成本和可维护性需求。

接下来,让我们更详细地探讨每个选项。

常规/短轮询

一种受益于半实时更新的方法是使用常规/短轮询,如图 6-2 所示。在这种轮询机制中,客户端定期向服务器发送 HTTP 请求,以便在预配置的时间间隔内检查更新。间隔越短,更新越接近实时,但同时也意味着你需要管理更高的流量。

你可以在构建批量生成数据(如图像)的服务时使用这种技术。客户端只需提交请求以启动批量作业,并获得一个唯一的作业/请求标识符。然后,客户端会定期与服务器检查,确认请求作业的状态和输出。如果输出尚未计算,服务器则返回新的数据,或者提供空响应(并可能提供状态更新)。

正如你所想,短轮询会导致大量的传入请求,即使没有新信息,服务器也需要响应这些请求。如果你有多个并发用户,这种方法可能会迅速压垮服务器,限制应用程序的可扩展性。然而,你仍然可以通过使用缓存响应(即以可容忍的频率在后台执行状态检查)和实现速率限制来减轻服务器负担,你将在第 9 和第 10 章中了解更多关于速率限制的内容。

在 AI 服务中,短轮询的潜在使用场景是当你有一些正在进行中的批量或推理作业时。你可以暴露端点,让客户端使用短轮询来跟踪这些作业的状态,并在作业完成时获取结果。

另一种选择是改为使用长轮询。

长轮询

如果你希望在继续利用实时轮询机制的同时减轻服务器负担,可以实现长轮询(见图 6-3),这是常规/短轮询的改进版本。

通过长轮询,服务器和客户端都被配置为防止超时(如果可能的话),超时通常发生在客户端或服务器放弃处理长时间请求时。

提示

在典型的 HTTP 请求-响应周期中,当请求解决需要较长时间,或出现网络问题时,超时现象更为常见。

为了实现长轮询,服务器保持传入请求开放(即挂起),直到有数据可以返回。例如,当你有一个处理时间不可预测的 LLM 时,这种方式非常有用。客户端会被指示等待较长时间,并避免提前中断或重复请求。

如果你需要一个简单的 API 设计和应用架构来处理长时间运行的任务(例如多个 AI 推理),可以使用长轮询。这种技术使你避免了实现批量作业管理器来跟踪大规模数据生成的作业。相反,客户端请求保持开放,直到它们被处理,从而避免了可能导致服务器超负荷的短轮询请求-响应循环。

虽然长轮询听起来类似于典型的 HTTP 请求-响应模型,但它在客户端如何处理请求方面有所不同。在长轮询中,客户端通常每个请求只接收一条消息。一旦服务器发送响应,连接就会关闭。然后,客户端立即打开一个新的连接,等待下一条消息。这个过程会重复进行,允许客户端随着时间的推移接收多条消息,但每个 HTTP 请求-响应周期只处理一条消息。

由于长轮询保持连接开放直到有消息可用,因此与短轮询相比,它减少了请求的频率,并实现了近实时的通信机制。然而,服务器仍然需要保持未完成的请求,这会消耗服务器资源。此外,如果同一客户端有多个打开的请求,消息的顺序可能难以管理,可能导致消息乱序。

如果你没有特别的轮询机制需求,一种更现代的实时通信替代方案是通过事件源接口(SSE)实现的轮询机制。

服务器推送事件 (SSE)

服务器推送事件(SSE)是一种基于 HTTP 的机制,用于在服务器与客户端之间建立持久的单向连接。只要连接保持打开,服务器可以在数据可用时不断向客户端推送更新。

一旦客户端与服务器建立了持久的 SSE 连接,就不需要重新建立连接,这与长轮询机制不同,在长轮询中客户端需要反复发送请求到服务器以保持连接。

在为 GenAI 模型提供服务时,SSE 将比长轮询更适合作为实时通信机制。SSE 专为处理实时事件设计,并且比长轮询更高效。由于长轮询需要重复打开和关闭连接,导致资源消耗高,延迟和开销增加。相比之下,SSE 支持自动重新连接和事件 ID 以恢复中断的流,这些功能是长轮询所不具备的。

在 SSE 中,客户端发送一个标准的 HTTP GET 请求,并带有 Accept:text/event-stream 头,服务器则以 200 状态码和 Content-Type: text/event-stream 头进行响应。在完成握手后,服务器可以通过相同的连接向客户端发送事件。

SSE 和 EventSource 接口

SSE 规范描述了一个内置的 EventSource 接口,该接口与服务器建立持久连接,用于从服务器发送事件。与 WebSocket 类似,该连接是持久的。

EventSource 是一种与服务器通信的方式,但比 WebSocket 功能稍弱。

为什么要使用它?主要原因是它更简单。在许多应用程序中,WebSocket 的强大功能可能有点过于复杂。我们将稍后讨论 WebSocket。

我们需要从服务器接收数据流------可能是聊天消息、市场价格或其他内容。这正是 EventSource 擅长的地方。此外,它支持自动重新连接,而 WebSocket 需要我们手动实现此功能。并且,它是普通的 HTTP,而不是新协议。

在创建时,新的 EventSource 会连接到服务器,如果连接断开,它会自动重新连接。这非常方便,因为我们不需要关心它。重新连接之间有一个小的延迟,默认情况下是几秒钟。服务器可以在响应中使用 retry: 来设置推荐的延迟(毫秒)。

虽然 SSE 应该是实时应用程序的首选,但如果更新不频繁,或者你的环境不支持持久连接,你仍然可以选择更简单的长轮询机制。

最后需要注意的一个重要细节是,SSE 连接是单向的,这意味着你向服务器发送常规的 HTTP 请求,并通过 SSE 获取响应。因此,它们只适用于那些不需要向服务器发送数据的应用程序。你可能在新闻推送、通知和实时仪表板(如股票数据图表)中见过 SSE 的应用。

毫不奇怪,SSE 在聊天应用程序中也表现出色,尤其是当你需要在对话中流式传输 LLM 的响应时。在这种情况下,客户端可以建立一个独立的持久连接,直到服务器完全流式传输 LLM 的响应给用户。

注意

ChatGPT 在后台利用 SSE 实现对用户查询的实时响应。

图 6-4 展示了 SSE 通信机制的运作方式。

为了加深理解,我们将在本章中构建两个小项目,使用 SSE 技术。一个是从模拟数据生成器流式传输数据,另一个是流式传输 LLM 响应。在这些项目中,你将深入了解 SSE 机制。

总结来说,SSE 非常适合建立持久的单向连接,但如果你需要在持久连接中同时发送和接收消息呢?这时 WebSocket 会派上用场。

WebSocket

接下来我们将介绍 WebSocket,它是最后一种实时通信机制。

WebSocket 是一种极好的实时通信机制,用于在客户端和服务器之间建立持久的双向连接,适用于实时聊天以及带有 AI 模型的语音和视频应用程序。双向连接意味着双方可以在任何顺序中发送和接收实时数据,只要客户端和服务器之间保持持久连接。它设计时就考虑了通过标准 HTTP 端口工作,以确保与现有安全措施的兼容性。需要双向通信的 Web 应用程序最能从这种机制中受益,因为它们可以避免 HTTP 轮询带来的开销和复杂性。

你可以在多种应用中使用 WebSocket,包括社交信息流、多人游戏、金融信息流、基于位置的更新、多媒体聊天等。

Webhook 与 WebSocket

不要将 WebSocket 与 Webhook 混淆。这些术语常常被交替使用,但它们指的是不同的实时 Web 通信机制。

Webhook 用于实时的服务器对服务器通信,支持事件驱动的应用架构。因此,当一个服务器暴露一个 Webhook 端点时,它是在告诉其他服务器,当某个事件发生时立即将数据发送到该 Webhook 端点。

你可以将 Webhook 看作是一种非持久的但实时的单向通信机制,其中一个服务器在生成数据时将消息发送到另一个服务器。实际上,服务器之间并没有建立握手或开放连接。

与我们之前讨论的所有其他通信机制不同,WebSocket 协议在初始握手后不再通过 HTTP 传输数据。相反,WebSocket 协议根据 RFC 6455 规范实现了一个双向消息机制(全双工),通过单个 TCP 连接进行数据传输。因此,WebSocket 比 HTTP 更适合数据传输,因为它具有较少的协议开销,并在网络协议栈中操作得更低层次。这是因为 HTTP 是在 TCP 之上的,去除 HTTP 后直接使用 TCP 会更快。

提示

WebSocket 在客户端和服务器上都保持一个套接字连接,直到连接结束。请注意,这也使得服务器是有状态的,这增加了扩展的难度。

你可能现在会好奇 WebSocket 协议是如何工作的。

根据 RFC 6455,为了建立 WebSocket 连接,客户端向服务器发送一个 HTTP "升级"请求,请求打开 WebSocket 连接。这被称为开通握手,开启 WebSocket 连接生命周期并进入 CONNECTING 状态。

警告

你的 AI 服务应该能够处理多个并发的握手请求,并在打开连接之前进行身份验证。新的连接会消耗服务器资源,因此必须由服务器妥善处理。

HTTP 升级请求应包含一组必需的头部,如示例 6-1 所示。

示例 6-1. 通过 HTTP 发起 WebSocket 开始握手

vbnet 复制代码
GET ws://localhost:8000/generate/text/stream HTTP/1.1 
Origin: http://localhost:3000
Connection: Upgrade 
Host: http://localhost:8000
Upgrade: websocket 
Sec-WebSocket-Key: 8WnhvZTK66EVvhDG++RD0w== 
Sec-WebSocket-Protocol: html-chat, text-chat 
Sec-WebSocket-Version: 13
  • 向 WebSocket 端点发起 HTTP 升级请求。WebSocket 端点以 ws:// 开头,而不是典型的 http://
  • 请求升级并打开一个 WebSocket 连接。
  • 使用一个随机的 16 字节的 Base64 编码字符串,确保服务器支持 WebSocket 协议。
  • 如果 html-chat 不可用,则使用 text-chat 子协议。子协议决定将交换的数据类型。

警告

在生产环境中,始终使用安全的 WebSocket wss:// 端点。
wss:// 协议类似于 https://,不仅加密数据,而且更可靠。因为 ws:// 数据未加密并且对任何中介是可见的,老旧的代理服务器可能不知道 WebSocket,它们可能会看到"奇怪"的头部并中止连接。

另一方面,wss:// 是 WebSocket 的安全版本,运行在传输层安全性(TLS)之上,对数据进行加密并在接收方解密。因此,数据包通过代理时是加密的,它们无法看到内容,因此可以顺利传递。

一旦 WebSocket 连接建立,文本或二进制消息可以通过消息帧的形式在两个方向上传输。连接生命周期进入 OPEN 状态。

你可以在图 6-5 中查看 WebSocket 通信机制的运作方式。

消息帧是用于在客户端和服务器之间打包和传输数据的方式。它们并不是 WebSocket 独有的,因为它们适用于所有通过 TCP 协议建立的连接,而 TCP 协议是 HTTP 的基础。然而,WebSocket 消息帧由几个组成部分构成:

  • 固定头部
    描述关于消息的基本信息。
  • 扩展有效负载长度(可选)
    当有效负载长度超过 125 字节时,提供实际的有效负载长度。
  • 掩码键
    对从客户端发送到服务器的帧中的有效负载数据进行掩码处理,防止某些类型的安全漏洞,特别是缓存中毒和跨协议攻击。
  • 有效负载
    包含实际的消息内容。

与 HTTP 请求中的冗长头部不同,WebSocket 帧有最小化的头部,包含以下内容:

  • 文本帧
    用于 UTF-8 编码的文本数据。
  • 二进制帧
    用于二进制数据。
  • 分片
    用于将消息分割成多个帧,接收方将重新组装这些帧。

WebSocket 协议的优势还在于其能够通过控制帧保持持久连接。

控制帧是用于管理连接的特殊帧:

  • Ping/Pong 帧
    用于检查连接状态。
  • 关闭帧
    用于优雅地终止连接。

当 WebSocket 连接需要关闭时,客户端或服务器会发送关闭帧。关闭帧可以选择性地指定状态码和/或关闭连接的原因。此时,WebSocket 连接进入 CLOSING 状态。

CLOSING 状态在另一方响应另一个关闭帧时结束。此时,WebSocket 连接的完整生命周期结束,进入 CLOSED 状态,如图 6-6 所示。

如你所见,对于一些不需要额外开销的简单应用,使用 WebSocket 通信机制可能有点过于复杂。对于大多数 GenAI 应用程序,SSE 连接可能已经足够。

然而,确实有一些 GenAI 用例是 WebSocket 发挥优势的,例如多媒体聊天、语音对语音应用程序、协作 GenAI 应用程序和基于双向通信的实时转录服务。为了获得一些实践经验,你将在本章后面构建一个语音转文本应用。

现在你已经了解了几种用于实时应用的独特 Web 通信机制,让我们快速总结一下它们的比较。

比较通信机制

图 6-7 概述了 Web 开发中使用的上述五种通信机制。

正如你从图 6-7 中看到的,每种方法的消息传递模式有所不同。

HTTP 请求-响应是最常见的模型,所有 Web 客户端和服务器都支持,适用于不需要实时更新的 RESTful API 和服务。

短/常规轮询涉及客户端在设定的时间间隔内检查数据,尽管实现简单,但在扩展服务时可能会消耗大量资源。通常用于进行不频繁更新的应用程序,例如分析仪表板。

长轮询通过保持连接直到服务器上有数据可用,来更高效地进行实时更新。然而,它仍然可能消耗服务器资源,适合用在近实时功能中,如通知。

SSE 使用 HTTP 协议保持单一的持久连接,且仅限于服务器到客户端。它易于设置,利用浏览器的 EventSource API,并提供像重新连接这样的内建功能。这些因素使 SSE 适合需要实时更新、聊天功能和实时仪表板的应用程序。

WebSocket 提供全双工(双向)通信,具有低延迟和二进制数据支持,但实现起来较为复杂。它广泛应用于需要高度互动和实时数据交换的应用程序,如多人游戏、聊天应用程序、协作工具和实时转录服务。

随着 SSE 和 WebSocket 的发明及其日益流行,短/常规轮询和长轮询在 Web 应用中的实时通信机制变得越来越不常见。

表 6-1. Web 通信机制比较

通信机制 特性 挑战 应用场景
HTTP 请求-响应 简单的请求-响应模型,无状态协议,所有 Web 客户端和服务器都支持 实时更新的延迟较高,不适合频繁的服务器到客户端数据传输 RESTful API、实时更新不重要的 Web 服务
短/常规轮询 客户端定期按间隔请求数据,易于实现 在没有新数据时浪费资源,延迟取决于轮询间隔 更新不频繁的应用程序、简单的近实时仪表板、提交作业的状态更新
长轮询 比短轮询更高效,保持连接直到数据可用 可能对服务器资源消耗大,管理多个连接复杂 实时通知、旧版聊天应用
服务器推送事件 (SSE) 单一持久连接用于更新,内建重新连接和事件 ID 支持 单向通信,只能从服务器到客户端 实时信息流、聊天应用、实时分析仪表板
WebSocket 全双工通信、低延迟、支持二进制数据 实现和管理较为复杂,需要服务器支持 WebSocket 多人游戏、聊天应用、协作编辑工具、视频会议和网络研讨会应用、实时转录和翻译应用

在详细回顾了实时通信机制后,我们将通过实现自己的流式端点,深入探讨 SSE 和 WebSocket。接下来的部分,你将学习如何实现同时使用这两种技术的流式端点。

实现 SSE 端点

在第 3 章中,你学习了 LLM,它们是自回归模型,通过基于先前输入来预测下一个令牌。在每个生成步骤之后,输出令牌会附加到输入中,并再次通过模型直到生成一个 <stop> 令牌以打破循环。你可以将输出令牌在生成时通过数据流的形式传送给用户,而不必等到整个循环完成。

模型提供者通常会为你提供一个选项,允许你将输出模式设置为数据流,使用 stream=True。启用该选项后,模型提供者可以返回数据生成器,而不是最终输出,你可以将其直接传递给你的 FastAPI 服务器进行流式处理。

为了演示这一点,请参阅示例 6-2,它使用 openai 库实现了一个异步数据生成器。

提示

要运行示例 6-2,你需要在 Azure 门户上创建一个 Azure OpenAI 实例,并创建一个模型部署。记下 API 端点、密钥和模型部署名称。对于示例 6-2,你可以使用 2023-05-15 的 API 版本。

示例 6-2. 实现 Azure OpenAI 异步聊天客户端用于流式响应

python 复制代码
# stream.py

import asyncio
import os
from typing import AsyncGenerator
from openai import AsyncAzureOpenAI

class AzureOpenAIChatClient: 
    def __init__(self):
        self.aclient = AsyncAzureOpenAI(
            api_key=os.environ["OPENAI_API_KEY"],
            api_version=os.environ["OPENAI_API_VERSION"],
            azure_endpoint=os.environ["OPENAI_API_ENDPOINT"],
            azure_deployment=os.environ["OPENAI_API_DEPLOYMENT"],
        )

    async def chat_stream(
        self, prompt: str, model: str = "gpt-3.5-turbo"
    ) -> AsyncGenerator[str, None]: 
        stream = await self.aclient.chat.completions.create(
            messages=[
                {
                    "role": "user",
                    "content": prompt,
                }
            ],
            model=model,
            stream=True, 
        )

        async for chunk in stream:
            yield f"data: {chunk.choices[0].delta.content or ''}\n\n" 
            await asyncio.sleep(0.05) 

        yield f"data: [DONE]\n\n"


azure_chat_client = AzureOpenAIChatClient()
  • 创建一个异步的 AzureOpenAIChatClient 以与 Azure OpenAI API 交互。聊天客户端需要 API 端点、部署名称、密钥和版本才能正常工作。
  • 定义一个 chat_stream 异步生成器方法,该方法从 API 中提取每个输出令牌。
  • 设置 stream=True,以便从 API 获取输出流,而不是一次性返回完整响应。
  • 循环遍历数据流,并为每个输出令牌添加 data: 前缀,确保浏览器能够正确使用 EventSource API 解析内容。
  • 通过限制流速来减缓流式传输速率,从而减少客户端的背压。

在示例 6-2 中,你创建了一个 AsyncAzureOpenAI 实例,使你能够通过 API 在私有 Azure 环境中与 Azure OpenAI 模型进行对话。通过设置 stream=TrueAsyncAzureOpenAI 返回数据流(一个异步生成器函数),而不是完整的模型响应。你可以遍历数据流并为令牌添加 data: 前缀,以符合 SSE 规范。这将使浏览器能够自动使用广泛支持的 EventSource Web API 解析流内容。

警告

在暴露流式端点时,你需要考虑客户端消耗你发送的数据的速度。一个好的做法是像示例 6-2 中那样减少流式传输速率,以减少客户端的背压。你可以通过在不同设备上的不同客户端测试你的服务来调整流量控制。

使用 GET 请求实现 SSE

现在,你可以通过将聊天流传递给 FastAPI 的 StreamingResponse,将其作为 GET 端点来实现 SSE 端点,如示例 6-3 所示。

示例 6-3. 使用 FastAPI 的 StreamingResponse 实现 SSE 端点

python 复制代码
# main.py

from fastapi.responses import StreamingResponse
from stream import azure_chat_client

...

@app.get("/generate/text/stream") 
async def serve_text_to_text_stream_controller(
    prompt: str,
) -> StreamingResponse:
    return StreamingResponse( 
        azure_chat_client.chat_stream(prompt), media_type="text/event-stream"
    )
  • 使用 GET 方法实现 SSE 端点,以便通过浏览器的 EventSource API 使用。
  • 将聊天流生成器传递给 StreamingResponse,将输出流实时生成并转发给客户端。根据 SSE 规范设置 media_type="text/event-stream",以便浏览器正确处理响应。

在服务器上设置好 GET 端点后,你可以在客户端创建一个简单的 HTML 表单,通过 EventSource 接口消费 SSE 流,如示例 6-4 所示。

提示

示例 6-4 没有使用任何 JavaScript 库或 Web 框架。然而,有很多库可以帮助你在你选择的框架(如 React、Vue 或 SvelteKit)中实现 EventSource 连接。

示例 6-4. 使用浏览器的 EventSource API 在客户端实现 SSE

xml 复制代码
{# pages/client-sse.html #}

<!DOCTYPE html>
<html lang="en">
<head>
    <title>SSE with EventSource API</title>
</head>
<body>
<button id="streambtn">Start Streaming</button>
<label for="messageInput">Enter your prompt:</label>
<input type="text" id="messageInput" placeholder="Enter your prompt"> 
<div style="padding-top: 10px" id="responseContainer"></div> 

<script>
    let source;
    const button = document.getElementById('streambtn');
    const container = document.getElementById('container');
    const input = document.getElementById('messageInput');

    function resetForm(){
        input.value = '';
        container.textContent = '';
    }

    function handleOpen() {
        console.log('Connection was opened');
    }
    function handleMessage(e){
        if (e.data === '[DONE]') {
            source.close();
            console.log('Connection was closed');
            return;
        }

        container.textContent += e.data;
    }
    function handleClose(e){
        console.error(e);
        source.close()
    }

    button.addEventListener('click', function() { 
        const message = input.value;
        const url = 'http://localhost:8000/generate/text/stream?prompt=' +
            encodeURIComponent(message);
        resetForm() 

        source = new EventSource(url); 
        source.addEventListener('open', handleOpen, false);
        source.addEventListener('message', handleMessage, false);
        source.addEventListener('error', handleClose, false); 
    });

</script>
</body>
</html>
  • 创建一个简单的 HTML 输入框和按钮,用于启动 SSE 请求。
  • 创建一个空的容器,用于作为流内容的接收器。
  • 监听按钮点击事件并执行 SSE 回调。
  • 重置内容表单和响应容器,以便清除上一个内容。
  • 创建一个新的 EventSource 对象并监听连接状态变化以处理事件。
  • 在 SSE 连接打开时记录日志。处理每个消息,将消息内容渲染到响应容器中,直到收到 [DONE] 消息,表示连接应关闭。此外,如果发生任何错误,关闭连接并将错误记录到浏览器控制台中。

在示例 6-4 中实现了 SSE 客户端后,你现在可以使用它来测试你的 SSE 端点。但是,首先你需要提供 HTML 文件。

创建一个 pages 目录,并将 HTML 文件放在其中。然后,将该目录挂载到 FastAPI 服务器上,将其内容作为静态文件提供,如示例 6-5 所示。通过挂载,FastAPI 会处理将 API 路径映射到每个文件,从而使你能够通过浏览器从与服务器相同的源访问它们。

示例 6-5. 将 HTML 文件作为静态资源挂载到服务器上

python 复制代码
# main.py

from fastapi.staticfiles import StaticFiles

app.mount("/pages", StaticFiles(directory="pages"), name="pages") 
  • pages 目录挂载到 /pages 路径,以将其内容作为静态资源提供。一旦挂载,你可以通过访问 <origin>/pages/<filename> 来访问每个文件。

通过实现示例 6-5,你将 HTML 文件从与 API 服务器相同的源提供。这避免了触发浏览器的 CORS 安全机制,后者可能会阻止发往服务器的请求。

现在,你可以通过访问 http://localhost:8000/pages/sse-client.html 来访问 HTML 页面。

跨域资源共享(CORS)

如果你尝试直接在浏览器中打开示例 6-4 的 HTML 文件,并点击"Start Streaming"按钮,你会发现什么也没有发生。你可以检查浏览器的网络标签页,查看发出的请求发生了什么。

经过一些调查,你应该会注意到,浏览器已阻止了对服务器的外发请求,因为其跨域资源共享(CORS)预检与服务器的检查失败。

CORS 是浏览器中实现的一种安全机制,用于控制如何从另一个域请求网页上的资源,只有当直接从浏览器而不是服务器发送请求时,CORS 才会生效。浏览器使用 CORS 来检查是否允许从不同源(即不同域)向服务器发送请求。

例如,如果你的客户端托管在 https://example.com,并且需要从托管在 https://api.example.com 上的 API 获取数据,浏览器会阻止此请求,除非 API 服务器启用了 CORS。

现在,你可以通过在服务器上添加 CORS 中间件来绕过这些 CORS 错误,如示例 6-6 所示,从而允许来自浏览器的任何传入请求。

示例 6-6. 应用 CORS 设置

ini 复制代码
# main.py

from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"], 
)
  • 允许来自任何来源、方法(GET、POST 等)和头部的传入请求。

Streamlit 通过在其内部服务器上发送请求来避免触发 CORS 机制,即使生成的 UI 在浏览器上运行。另一方面,FastAPI 文档页面则从与服务器相同的源(即 http://localhost:8000)发出请求,因此默认情况下,发出的请求不会触发 CORS 安全机制。

警告

在示例 6-6 中,你配置了 CORS 中间件来处理任何传入请求,有效地绕过了 CORS 安全机制,以便更轻松地进行开发。在生产环境中,你应仅允许少数几个来源、方法和头部被服务器处理。

如果你按照示例 6-5 或 6-6 进行操作,现在你应该能够查看从 SSE 端点传入的流(见图 6-8)。

恭喜!你现在已经有了一个完整的解决方案,其中模型响应会在生成数据可用时直接流式传输到客户端。通过实现这个功能,用户在与聊天机器人互动时将有更愉快的体验,因为他们能够实时收到对查询的响应。

你的解决方案还实现了并发,使用异步客户端与 Azure OpenAI API 交互,从而更快地将响应流式传输给用户。你可以尝试使用同步客户端,比较生成速度的差异。使用异步客户端时,生成速度可以非常快,以至于你会一次性接收到一块文本,即使它实际上是被流式传输到浏览器的。

从 Hugging Face 模型流式传输 LLM 输出

现在你已经学会了如何实现与模型提供商(如 Azure OpenAI)一起使用 SSE 端点,你可能会想知道是否可以从你之前从 Hugging Face 下载的开源模型中流式传输模型输出。

尽管 Hugging Face 的 transformers 库实现了一个 TextStreamer 组件,你可以将其传递给模型管道,但最简单的解决方案是运行一个单独的推理服务器,如 HF Inference Server 来实现模型流式传输。

示例 6-7. 通过 HF Inference Server 提供 Hugging Face LLM 模型

ruby 复制代码
$ docker run --runtime nvidia --gpus all \ 
    -v ~/.cache/huggingface:/root/.cache/huggingface \ 
    --env "HUGGING_FACE_HUB_TOKEN=<secret>" \ 
    -p 8080:8000 \ 
    --ipc=host \ 
    vllm/vllm-openai:latest \  
    --model mistralai/Mistral-7B-v0.1 
  • 使用 Docker 下载并运行最新的 vllm/vllm-openai 容器,支持所有可用的 NVIDIA GPU。
  • 与 Docker 容器共享一个卷,避免每次运行时都需要重新下载权重。
  • 设置 HUGGING_FACE_HUB_TOKEN 环境变量,以访问像 mistralai/Mistral-7B-v0.1 这样的受限模型。
  • 通过将主机端口 8080 映射到 Docker 容器暴露的端口 8000,来在 localhost 的 8080 端口上运行推理服务器。
  • 启用容器与主机之间的进程间通信(IPC),以允许容器访问主机的共享内存。

vLLM 推理服务器使用 OpenAI API 规范来为 LLM 提供服务。

从 Hugging Face Hub 下载并使用受限的 mistralai/Mistral-7B-v0.1 模型。

启动模型服务器后,你现在可以使用 AsyncInferenceClient 来生成流式输出,如示例 6-8 所示。

示例 6-8. 从 HF 推理流中消费 LLM 输出流

python 复制代码
import asyncio
from typing import AsyncGenerator
from huggingface_hub import AsyncInferenceClient

client = AsyncInferenceClient("http://localhost:8080")

async def chat_stream(prompt: str) -> AsyncGenerator[str, None]:
    stream = await client.text_generation(prompt, stream=True)
    async for token in stream:
        yield token
        await asyncio.sleep(0.05)
  • 示例 6-8 展示了如何使用 Hugging Face 推理服务器,你仍然可以使用其他支持流式模型响应的模型服务框架,如 vLLM。

在我们继续讨论 WebSocket 之前,让我们看看如何使用 POST 方法消费另一种变体的 SSE 端点。

使用 POST 请求实现 SSE

EventSource 规范期望服务器上的 GET 端点能够正确消费传入的 SSE 流。这使得使用 SSE 实现实时应用变得简单,因为 EventSource 接口能够处理如连接中断和自动重新连接等问题。

然而,使用 HTTP GET 请求有其自身的局限性。GET 请求通常不如其他请求方法安全,且更容易受到 XSS 攻击。此外,由于 GET 请求不能有请求体,因此只能通过 URL 查询参数将数据传输到服务器。问题在于你需要考虑 URL 的长度限制,且所有查询参数必须正确编码到请求 URL 中。因此,你不能仅仅将整个对话历史附加到 URL 作为参数。你的服务器必须处理维护对话历史并跟踪 GET SSE 端点中的对话上下文。

解决上述限制的一种常见方法是实现一个 POST SSE 端点,尽管 SSE 规范不支持它。因此,实施过程将更加复杂。

首先,我们来实现服务器上的 POST 端点,如示例 6-9 所示。

示例 6-9. 在服务器上实现 SSE 端点

python 复制代码
# main.py

from typing import Annotated
from fastapi import Body, FastAPI
from fastapi.responses import StreamingResponse
from stream import azure_chat_client

@app.post("/generate/text/stream")
async def serve_text_to_text_stream_controller(
    prompt: Annotated[str, Body()]
) -> StreamingResponse:
    return StreamingResponse(
        azure_chat_client.chat_stream(prompt), media_type="text/event-stream"
    )
  • 使用 POST 方法实现 SSE 端点,以便与浏览器的 EventSource API 配合使用。
  • 将聊天流生成器传递给 StreamingResponse,将输出流实时生成并转发给客户端。根据 SSE 规范设置 media_type="text/event-stream",以便浏览器正确处理响应。

实现了流式聊天输出的 POST 端点后,你现在可以开发客户端逻辑来处理 SSE 流。

你将不得不手动使用浏览器的 fetch Web 接口处理传入的流,如示例 6-10 所示。

示例 6-10. 使用浏览器的 EventSource API 在客户端实现 SSE

xml 复制代码
{# pages/client-sse-post.html #}

<!DOCTYPE html>
<html lang="en">
<head>
<title>SSE With Post Request</title>
</head>
<body>
<button id="streambtn">Start Streaming</button>
<label for="messageInput">Enter your prompt:</label>
<input type="text" id="messageInput" placeholder="Enter message">
<div style="padding-top: 10px" id="container"></div>

<script>
    const button = document.getElementById('streambtn');
    const container = document.getElementById('container');
    const input = document.getElementById('messageInput');

    function resetForm(){
        input.value = '';
        container.textContent = '';
    }

    async function stream(message){
        const response = await fetch('http://localhost:8000/generate/text/stream', {
            method: "POST",
            cache: "no-cache",
            keepalive: true,
            headers: {
                "Content-Type": "application/json",
                "Accept": "text/event-stream",
            },
            body: JSON.stringify({
                prompt: message, 
            }),
        });

        const reader = response.body.getReader(); 
        const decoder = new TextDecoder(); 

        while (true) { 
            const {value, done} = await reader.read();
            if (done) break;
            container.textContent += decoder.decode(value);
        }
    }

    button.addEventListener('click', async function() { 
        resetForm()
        await stream(input.value)

    });

</script>
</body>
</html>
  • 使用浏览器的 fetch 接口发送 POST 请求到后端。
  • 准备请求的 body,作为 JSON 字符串的一部分。
  • 添加头部,指定发送的请求体和预期的响应类型。
  • 从响应的 body 流中访问读者。
  • 为每个消息创建一个文本解码器实例来处理流中的消息。
  • 运行一个无限循环,使用读者读取流中的下一条消息。如果流已结束,done=true,则跳出循环;否则,使用文本解码器解码消息并将其附加到响应容器的 textContent 上进行渲染。
  • 监听按钮点击事件并执行回调,重置表单状态并与后端端点建立 SSE 连接。

正如你在示例 6-10 中所见,在没有使用 EventSource 的情况下消费 SSE 流可能会变得复杂。

提示

示例 6-10 的替代方案是使用 GET SSE 端点,但通过 POST 请求将大负载数据提前发送到服务器。服务器存储数据并在建立 SSE 连接时使用它。

SSE 也支持 Cookies,因此你可以依赖 Cookies 在 GET SSE 端点中交换大负载数据。

如果你打算在生产环境中消费 SSE 端点,你的解决方案还应支持重试功能、错误处理,甚至能够中止连接。

示例 6-11. 使用指数退避实现客户端重试功能

xml 复制代码
// pages/client-sse-post.html within <script> tag

function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

async function stream(
    message,
    maxRetries = 3,
    initialDelay = 1000,
    backoffFactor = 2,
) {
    let delay = initialDelay;
    for (let attempt = 0; attempt < maxRetries; attempt++) { 
        try { 
            ... // Establish SSE connection here
            return 
        } catch (error) {
            console.warn(`Failed to establish SSE connection: ${error}`);
            console.log(
                `Re-establishing connection - attempt number ${attempt + 1}`,
            );
            if (attempt < maxRetries - 1) {
                await sleep(delay); 
                delay *= backoffFactor; 
            } else {
                throw error 
            }
        }
    }
}
  • 只要没有达到最大重试次数,尝试建立 SSE 连接。
  • 使用 trycatch 来处理连接错误。
  • 如果连接成功,退出函数。
  • 在重试之前,暂停指定的延迟时间。
  • 在每次迭代中通过乘以回退因子来实现指数退避。
  • 如果达到最大重试次数,抛出错误。

现在,你应该能更自信地实现自己的 SSE 端点以进行流式模型响应。SSE 是像 ChatGPT 这样的应用程序用来与模型进行实时对话的首选通信机制。由于 SSE 主要支持基于文本的流,它非常适合用于 LLM 输出流式传输场景。

在下一部分,我们将使用 WebSocket 机制实现相同的解决方案,以便你可以比较实现细节的差异。此外,你还将了解 WebSocket 如何在需要实时双向通信的场景中,如实时转录服务中,发挥重要作用。

实现 WebSocket 端点

在本节中,你将实现一个使用 WebSocket 协议的端点。通过这个端点,你将使用 WebSocket 将 LLM 输出流式传输到客户端,以便与 SSE 连接进行对比。最终,你将了解 SSE 和 WebSocket 在实时流式传输 LLM 输出方面的异同。

使用 WebSocket 流式传输 LLM 输出

FastAPI 支持通过 Starlette Web 框架的 WebSocket 接口来实现 WebSocket。由于 WebSocket 连接需要进行管理,我们首先实现一个连接管理器,用于跟踪活动连接并管理它们的状态。

你可以按照示例 6-12 实现一个 WebSocket 连接管理器。

示例 6-12. 实现 WebSocket 连接管理器

python 复制代码
# stream.py

from fastapi.websockets import WebSocket

class WSConnectionManager: 
    def __init__(self) -> None:
        self.active_connections: list[WebSocket] = []

    async def connect(self, websocket: WebSocket) -> None: 
        await websocket.accept()
        self.active_connections.append(websocket)

    async def disconnect(self, websocket: WebSocket) -> None: 
        self.active_connections.remove(websocket)
        await websocket.close()

    @staticmethod
    async def receive(websocket: WebSocket) -> str: 
        return await websocket.receive_text()

    @staticmethod
    async def send(
        message: str | bytes | list | dict, websocket: WebSocket
    ) -> None: 
        if isinstance(message, str):
            await websocket.send_text(message)
        elif isinstance(message, bytes):
            await websocket.send_bytes(message)
        else:
            await websocket.send_json(message)


ws_manager = WSConnectionManager() 
  • 创建一个 WSConnectionManager 来跟踪和处理活动的 WebSocket 连接。
  • 使用 accept() 方法打开 WebSocket 连接,并将新连接添加到活动连接列表中。
  • 断开连接时,关闭连接并从活动连接列表中移除 WebSocket 实例。
  • 在开放连接时接收传入的文本消息。
  • 使用相关的发送方法将消息发送给客户端。
  • 创建一个 WSConnectionManager 实例,在整个应用中复用。

你还可以通过扩展示例 6-12 中的连接管理器来广播消息(例如实时系统警报、通知或更新)给所有连接的客户端。这在群聊或协作白板/文档编辑工具等应用中非常有用。

由于连接管理器通过 active_connections 列表维护指向每个客户端的指针,你可以广播消息给每个客户端,如示例 6-13 所示。

示例 6-13. 使用 WebSocket 管理器广播消息给已连接的客户端

python 复制代码
# stream.py

from fastapi.websockets import WebSocket

class WSConnectionManager:
    ...
    async def broadcast(self, message: str | bytes | list | dict) -> None:
        for connection in self.active_connections:
            await self.send(message, connection)

实现了 WebSocket 管理器后,你现在可以开发一个 WebSocket 端点,将响应流式传输给客户端。然而,在实现该端点之前,请按照示例 6-14 更新 chat_stream 方法,以便以适合 WebSocket 连接的格式传输流内容。

示例 6-14. 更新聊天客户端流式方法以生成适合 WebSocket 连接的内容

python 复制代码
# stream.py

import asyncio
from typing import AsyncGenerator

class AzureOpenAIChatClient:
    def __init__(self):
        self.aclient = ...

    async def chat_stream(
        self, prompt: str, mode: str = "sse", model: str = "gpt-4o"
    ) -> AsyncGenerator[str, None]:
        stream = ...  # OpenAI 聊天生成流

        async for chunk in stream:
            if chunk.choices[0].delta.content is not None: 
                yield (
                    f"data: {chunk.choices[0].delta.content}\n\n"
                    if mode == "sse"
                    else chunk.choices[0].delta.content 
                )
                await asyncio.sleep(0.05)
        if mode == "sse": 
            yield f"data: [DONE]\n\n"
  • 仅生成非空内容。
  • 根据连接类型(SSE 或 WebSocket)生成流内容。

更新了 chat_stream 方法后,你可以专注于添加 WebSocket 端点。使用 @app.websocket 装饰器来装饰控制器函数,利用 FastAPI 的 WebSocket 类,如示例 6-15 所示。

示例 6-15. 实现 WebSocket 端点

python 复制代码
# main.py

import asyncio
from loguru import logger
from fastapi.websockets import WebSocket, WebSocketDisconnect
from stream import ws_manager, azure_chat_client

@app.websocket("/generate/text/streams") 
async def websocket_endpoint(websocket: WebSocket) -> None:
    logger.info("Connecting to client....")
    await ws_manager.connect(websocket) 
    try: 
        while True: 
            prompt = await ws_manager.receive(websocket) 
            async for chunk in azure_chat_client.chat_stream(prompt, "ws"):
                await ws_manager.send(chunk, websocket) 
                await asyncio.sleep(0.05) 
    except WebSocketDisconnect: 
        logger.info("Client disconnected")
    except Exception as e: 
        logger.error(f"Error with the WebSocket connection: {e}")
        await ws_manager.send("An internal server error has occurred")
    finally:
        await ws_manager.disconnect(websocket) 
  • 创建一个 WebSocket 端点,访问 ws://localhost:8000/generate/text/stream
  • 在客户端和服务器之间打开 WebSocket 连接。
  • 只要连接打开,就继续发送或接收消息。
  • websocket_controller 中处理错误并记录重要事件,以识别错误根源并优雅地处理意外情况。通过服务器或客户端关闭连接时,退出无限循环。
  • 接收到第一个消息时,将其作为提示传递给 OpenAI API。
  • 异步遍历生成的聊天流,将每个片段发送到客户端。
  • 等待一小段时间再发送下一条消息,以减少竞态条件问题,并为客户端提供足够的时间处理流。
  • 客户端关闭 WebSocket 连接时,抛出 WebSocketDisconnect 异常。
  • 如果在开放连接期间发生服务器端错误,记录错误并识别客户端。
  • 如果流已完成、发生内部错误或客户端关闭连接,则退出无限循环并优雅地关闭 WebSocket 连接。从活动的 WebSocket 连接列表中删除连接。

现在你有了 WebSocket 端点,接下来我们将开发客户端 HTML 来测试该端点(见示例 6-16)。

示例 6-16. 在客户端实现 WebSocket 连接,带有错误处理和指数退避重试功能

ini 复制代码
{# pages/client-ws.html #}

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Stream with WebSocket</title>
</head>
<body>
<button id="streambtn">Start Streaming</button>
<button id="closebtn">Close Connection</button>
<label for="messageInput">Enter your prompt:</label>
<input type="text" id="messageInput" placeholder="Enter message">
<div style="padding-top: 10px" id="container"></div>

<script>
    const streamButton = document.getElementById('streambtn');
    const closeButton = document.getElementById('closebtn');
    const container = document.getElementById('container');
    const input = document.getElementById('messageInput');

    let ws;
    let retryCount = 0;
    const maxRetries = 5;
    let isError = false;

    function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    function connectWebSocket() {
        ws = new WebSocket("ws://localhost:8000/generate/text/streams"); 

        ws.onopen = handleOpen;
        ws.onmessage = handleMessage;
        ws.onclose = handleClose;
        ws.onerror = handleError; 
    }

    function handleOpen(){
        console.log("WebSocket connection opened");
        retryCount = 0;
        isError = false;
    }

    function handleMessage(event) {
        container.textContent += event.data;
    }

    async function handleClose(){ 
        console.log("WebSocket connection closed");
        if (isError && retryCount < maxRetries) {
            console.warn("Retrying connection...");
            await sleep(Math.pow(2, retryCount) * 1000);
            retryCount++;
            connectWebSocket();
        }
        else if (isError) {
            console.error("Max retries reached. Could not reconnect.");
        }
    }

    function handleError(error) {
        console.error("WebSocket error:", error);
        isError = true;
        ws.close();
    }

    function resetForm(){
        input.value = '';
        container.textContent = '';
    }

    streamButton.addEventListener('click', function() { 
        const prompt = document.getElementById("messageInput").value;
        if (prompt && ws && ws.readyState === WebSocket.OPEN) {
            ws.send(prompt); 
        }
        resetForm(); 
    });

    closeButton.addEventListener('click', function() { 
        isError = false;
        if (ws) {
            ws.close();
        }
    });

    connectWebSocket(); 
</script>
</body>
</html>
  • 与 FastAPI 服务器建立 WebSocket 连接。
  • 为 WebSocket 连接实例添加回调处理器,以处理打开、关闭、消息和错误事件。
  • 优雅地处理连接错误,并使用指数退避重试功能重新建立连接。
  • 添加事件监听器,在点击流式按钮时发送第一个消息到服务器。
  • 一旦连接建立,发送初始的非空提示作为第一个消息给服务器。
  • 在建立 WebSocket 连接之前重置表单状态。
  • 添加事件监听器,当点击关闭连接按钮时关闭连接。

现在你可以访问 http://localhost:8000/pages/client-ws.html 来测试你的 WebSocket 流式传输端点(见图 6-9)。

你现在已经拥有一个完全工作的 LLM 流式传输应用程序,使用 WebSocket。做得好!

现在你可能会想知道哪个解决方案更好:使用 SSE 还是 WebSocket 连接进行流式传输。答案取决于你的应用需求。SSE 实现简单,且原生支持 HTTP 协议,因此大多数客户端都支持它。如果你只需要单向流式传输到客户端,那么我建议使用 SSE 连接来流式传输 LLM 输出。

WebSocket 连接提供了更多控制流式机制的方式,允许在同一连接内进行双向通信------例如,在多个用户和 LLM 之间的实时聊天应用、语音转文本、文本转语音和语音转语音服务中。然而,使用 WebSocket 需要将连接从 HTTP 升级到 WebSocket 协议,这可能会导致旧版客户端和浏览器不支持。此外,你还需要在处理 WebSocket 端点时稍微以不同的方式处理异常。

处理 WebSocket 异常

处理 WebSocket 异常与传统的 HTTP 连接有所不同。如果你参考示例 6-15,你会注意到,你不再通过返回带有状态码的响应或 HTTPException 来向客户端返回,而是保持连接打开,在接受连接后继续处理消息。

只要连接保持开放,你就可以发送和接收消息。然而,一旦发生异常,你应该通过优雅地关闭连接和/或向客户端发送错误消息,代替 HTTPException 响应来处理它。

由于 WebSocket 协议不支持常规的 HTTP 状态码(如 4xx 或 5xx),因此你不能使用状态码来通知客户端服务器端的问题。相反,你应该向客户端发送 WebSocket 消息,通知他们问题的存在,然后再关闭服务器的任何活动连接。

在关闭连接期间,你可以使用几个与 WebSocket 相关的状态码来指定关闭原因。通过使用这些关闭原因,你可以在服务器或客户端实现任何自定义的关闭行为。

表 6-2 显示了可以与 CLOSE 帧一起发送的几种常见状态码。

表 6-2. WebSocket 协议常见状态码

状态码 描述
1000 正常关闭
1001 客户端导航离开或服务器已关闭
1002 端点(即客户端或服务器)收到违反 WebSocket 协议的数据(例如,未掩码的数据包,无效的负载长度)
1003 端点收到不支持的数据(例如,期望文本,但收到二进制数据)
1007 端点收到编码不一致的数据(例如,文本消息中包含非 UTF-8 数据)
1008 端点收到违反其政策的消息;可以出于安全原因隐藏关闭的详细信息
1011 内部服务器错误

你可以在 WebSocket 协议 RFC 6455 中的第 7.4 节进一步了解其他 WebSocket 状态码。

设计流式 API

现在你已经更加熟悉了 SSE 和 WebSocket 端点的实现,我想在它们的架构设计中介绍一个最后的重要细节。

设计流式 API 的常见陷阱之一是暴露过多的流式端点。例如,如果你正在构建一个聊天机器人应用,你可能会暴露多个流式端点,每个端点都被预配置为处理单个对话中的不同消息。采用这种特定的 API 设计模式,你要求客户端在多个端点之间切换,每一步都提供必要的信息,同时在一次对话中管理流式连接。此设计模式增加了后端和前端应用程序的复杂性,因为需要在两端管理对话状态,同时避免组件之间的竞态条件和网络问题。

一个更简单的 API 设计模式是提供一个单一的入口点,供客户端启动与 GenAI 模型的流式连接,并使用头部、请求体或查询参数触发后端的相关逻辑。采用这种设计,后端逻辑从客户端中抽象出来,这简化了前端的状态管理,同时所有的路由和业务逻辑都在后端实现。由于后端可以访问数据库、其他服务和定制的提示,它可以轻松执行 CRUD 操作,并在提示或模型之间切换来计算响应。因此,一个端点可以作为切换逻辑的单一入口点,管理应用状态,并生成自定义响应。

总结

本章介绍了几种通过数据流传输实现实时通信的策略,适用于 GenAI 服务。

你学习了几种 Web 通信机制,包括传统的 HTTP 请求-响应模型、短轮询/常规轮询、长轮询、SSE 和 WebSocket。然后,你详细比较了这些机制,了解它们的特性、优点、缺点和使用场景,特别是 AI 工作流中的应用。最后,你实现了两个 LLM 流式端点,使用异步的 Azure OpenAI 客户端,学习了如何利用 SSE 和 WebSocket 实现实时通信机制。

在下一章中,你将学习更多关于在 AI 服务中集成数据库的 API 开发工作流。这将包括如何设置、迁移和与数据库进行交互。你还将学习如何使用 FastAPI 的后台任务处理流式端点中的数据存储和检索操作。

下一章涵盖的主题包括设置数据库和设计模式,使用 SQLAlchemy,数据库迁移,以及在流式模型输出时处理数据库操作。

相关推荐
漫谈网络1 小时前
Ollama API 应用指南
ai·llm·aigc·api·ollama
数据智能老司机3 小时前
使用 FastAPI 构建生成式 AI 服务——AI集成与模型服务
llm·openai·fastapi
亚里随笔4 小时前
TORL:解锁大模型推理新境界,强化学习与工具融合的创新变革
人工智能·llm·agent·agentic rl
爱吃的小肥羊7 小时前
GPT-4o图片生成功能API全面开放!手把手教你购买和获取OpenAI API 原创 作者:天霸 AI工具导航站
aigc·openai
sophister7 小时前
MCP 协议关于tool 的几个基础问题
llm·cursor·mcp
新智元7 小时前
高考考上 985 的 AI 来了!超强数理推理横扫真题,训练秘籍剑指 AGI
人工智能·openai
货拉拉技术8 小时前
AI Agent搭建神器上线!货拉拉工作流让效率翻倍!
算法·llm
新智元10 小时前
刚刚,OpenAI 最强图像生成 API 上线,一张图 1 毛 5!
人工智能·openai
xianjianlf211 小时前
mcp 啥玩意儿,一起来看看
openai·mcp