超越基础:让 LLM 令牌流可恢复并支持多设备

对于许多开发者来说,从同步聊天机器人转向异步 AI 代理——在用户工作时在后台运行的工具——似乎只是一次简单的传输方式更改。常见的建议是使用服务器发送事件 (SSE) 并配合 Last-Event-ID 头部来创建持久流。理论上听起来很容易,实际在大规模实现时会带来显著的架构摩擦。

当从简单演示转向生产级别的代理体验时,会出现三个核心需求:可恢复的流、可靠的取消以及多设备同步。虽然 SSE 在技术上可以处理这些,但实现细节暴露出“可做”和“容易”之间的差距。

令牌流的开销

在深入架构挑战之前,了解 LLM 响应的本质很重要。无论你使用的是 Vercel AI SDK、OpenAI 还是 Anthropic,返回的数据不仅仅是文本流。每个“令牌”都被大量元数据包裹。

例如,来自 Anthropic API 的单个事件可能包含 125 个字符的 JSON 元数据,却只传递 5 个字符的实际文本增量。当你决定通过将流存储在数据库中来实现持久化时,这种元数据开销立即成为关键瓶颈。

可恢复流的挑战

要使流可恢复,客户端必须能够在掉线后重新连接并请求缺失的令牌。SSE 规范提供了 Last-Event-ID 头部用于此目的。如果每个事件都有唯一的 ID(例如 response_id:token_index),客户端就能精确告知服务器它停在了哪里。

然而,在现代的横向可扩展架构中,服务器是无状态的副本,这会导致巨大的写放大问题。由于任何副本都可能处理重新连接请求,所有令牌必须实时写入共享的数据库或缓存。

这导致一种矛盾的局面:你为每几字符的文本都进行一次数据库写入,而这些请求往往根本不会掉线。当 LLM 完成响应后,这些单独的令牌记录就变得毫无用处,需要被清理,以保留最终的“完整响应”记录。结果是为一个仅惠及少数用户的功能带来了高成本的基础设施负担。

在持久化环境中处理取消

在基本的 SSE 设置中,掉线通常会向服务器发出取消 LLM 请求的信号。但一旦实现了可恢复流,掉线不再意味着用户想要停止;它可能只是用户进入了隧道或刷新了浏览器。

取消现在需要专门的带外机制。你必须实现一个单独的端点(例如 POST /cancel/{response_id}),在共享数据库中写入“取消标记”。处理 LLM 推理的服务器副本随后必须在生成令牌之间不断轮询此共享存储,以检查是否应中止上游调用。这会给推理循环增加延迟和复杂度。

多设备同步的缺口

支持多设备会带来两个不同的问题:

  1. 状态恢复: 由于令牌已经存入数据库以实现可恢复性,第二台设备可以获取历史记录并接入当前流。
  2. 实时通知: 设备 B 如何知道设备 A 已发送提示且响应正在流式传输?

如果没有持久的双向连接,设备 B 必须依赖轮询。正如原文所指出的,轮询是一种两败俱伤的权衡:轮询频率低会导致高延迟;轮询频率高则会给服务器带来不必要的流量压力。

替代架构:发布/订阅

鉴于这些摩擦,有人认为 HTTP 本质上并不适合作为异步代理应用的传输层。发布/订阅(Publisher/Subscriber)模式通过将连接生命周期与代理生命周期解耦,提供了更优雅的解决方案。

在发布/订阅模型中:

  • 持久性: 通道独立于客户端存在。令牌被发布到通道,并在客户端重新连接时可供“倒回”并收集。
  • 多设备: 多个客户端可以订阅同一通道,实时接收相同的流,无需轮询。
  • 效率: 传输层可以将令牌增量压缩为完整响应,减少客户端在追赶时需要处理的消息数量。
  • 路由: 取消和中断可以作为消息发布到通道,服务器进程会立即消费,从而无需轮询数据库获取取消标记。

社区观点

虽然 SSE 的挑战很大,但一些开发者认为“难度”只是实现方式的问题。例如,有贡献者建议使用基于 Redis Streams 的 SSE 实现,可以在不完全放弃 SSE 协议的前提下解决许多状态和持久性难题。另一些人则指向新兴工具,如 Durable Streams 或专门设计用于自动处理令牌事件和套接字关闭的 API。

最终,选择取决于应用的规模。对于简单的机器人,SSE 已足够。但对于需要高可靠性和低延迟的复杂多设备代理,将状态管理从数据库迁移到传输层是一种强大的架构转变。

Sources