在构建面向移动端(网络频繁震荡场景)的医疗级 Agent 系统时,传统的 HTTP/1.1 SSE 或直连 WebFlux 的无状态流(Flux)在面对用户 Wi-Fi/流量切换引发的闪断、网络延迟时显得捉襟见肘。
为此,我们采用了 “长连接网关 + 分布式状态外置” 的异步解耦架构:
- 长连接接入层:基于高性能的 netty-ws-starter 构建 WebSocket 长连接集群,由 Netty 统一接管客户端的连接生命周期、心跳保持(Ping/Pong)与重连寻址。
- 业务解耦层(南向流量) :当用户发起提问,WS 层 Handler 接收到指令后,不采取同步死等,而是将请求异步委派给后端的 Agent 服务(使用 OkHttp 发起 SSE 请求)。
- 状态存储层:Agent 服务从大模型厂商拉取到的增量 Token,不直接原路返回给本地会话,而是作为无状态数据源,实时压入 Redis 响应式队列(List) 中。这种“大模型 ➔ Redis List ➔ WS Handler”的流水线设计,实现了大模型生产速度与客户端消费速度的完全解耦。
将状态外置到 Redis 后,长连接网关(WS Handler)变得极度轻量与无状态化。这也让我们能够以极低的工程成本优雅地解决以下三个分布式核心痛点:
- 秒级断线重连与状态恢复:由于大模型的生成流持久化在 Redis List 中,当移动端因为信号网络震荡断线时,后台的大模型生成任务不会终止(继续向 Redis 灌入数据)。当客户端 WS 秒级重连成功后,只需上报最后接收到的
Token_Seq(序列号),Handler 即可从 Redis 中捞取断线期间积压的 Token 增量,一次性补发,完美实现无感知的流式断点续传。 - 全双工双向控制(Reset 信号) :当用户在中途点击“停止生成”或插话打断时,客户端通过 WS 管道发送一个轻量级的
CANCEL指令,网关收到后可以瞬间精准销毁该消息对应的 Redis Key,并通知上游 OkHttp 停止向大模型厂商拉取,完美闭环计费逻辑。
🚨 隐藏的黑天鹅:大模型“攒批响应(Batching)”引发的视觉卡顿
在实战演练中,我们发现了一个严重的体验问题:部分现代化大模型(如 DeepSeek V4-flash 等)为了追求更高的吞吐量或受限于底层网关的合并策略,其 SSE 流并不是完美地“一个SSE事件吐出一个 Token” ,而是经常将数个甚至十几个 Token “攒成一个批次”通过单次 SSE 事件整体响应。
如果网关不加干预地直接把这个大文本块透传给前端,前端在做文字打字机渲染时,用户就会产生明显的 “视觉卡顿感(一卡一卡、突发性喷涌)” ,极大地伤害了对话体验。
💡 服务端平滑器(Smoother)架构设计:一处改动,多端受益
为了解决这一痛点,如果让 Web 端、iOS、Android 多个客户端各自去写一套复杂的打字机定时器算法,不仅开发成本高,各端体验也难以统一。
我们的做法是:将平滑优化的能力后向坍塌到服务端的网关接入层(WS 投递前置处理) :
- 当 OkHttp 接收到大模型返回的、包含多个 Token 的复合型 SSE 块时,我们在入队前置拦截器中对其进行语义分词(或基于字符长度的细粒度微拆分) 。
- 将一个大块切碎为符合人类正常阅读流速的轻量级元数据。
- 再将这些拆分后的 Token 压入 Redis 队列。
架构收益:对于多端客户端而言,它们通过 WS 扫描到的依然是均匀、连续、平滑的 Token 流,前端不需要做任何复杂的渲染调优,直接无脑把收到的数据塞进 UI 即可 。