欢迎光临
我们一直在努力

系统学习agent系列之SSE服务端

系统学习agent系列之SSE服务端

导语:在上一篇文章中,我们彻底搞懂了 SSE(Server-Sent Events)协议的原理。但在实际开发中,前端不可能直接拿着 API Key 去连 OpenAI 或百度的服务器。我们需要在中间加一层“二传手”——Spring Boot 服务端。

怎么用 Spring Boot 优雅地实现流式转发?为什么 SseEmitter 不会阻塞 Tomcat 线程?生产环境又有哪些致命的坑(比如 Nginx 缓冲)?

今天,我们直接上干货,手把手教你搭建企业级的 SSE 服务端。


一、 灵魂拷问:为什么不能让前端直连大模型?

很多小白为了图省事,前端拿到 API Key 后直接发请求给大模型 API,这是极其危险且不专业的做法,原因有四:

  • API Key 暴露:前端代码是公开的,Key 一旦泄露,别人一晚上能刷爆你的信用卡。
  • 跨域限制(CORS):直接请求大模型接口,大概率会被浏览器的同源策略拦截。
  • 无法加业务逻辑:你想做鉴权、计费、敏感词过滤、意图识别?前端直连全做不了。
  • 阻塞 Tomcat 线程(最致命):如果后端用普通的同步 Controller 去接大模型的流,大模型“思考”10秒,你的 Tomcat 线程就被死锁占用 10 秒。并发一高,服务器直接宕机。
  • 破局之道:后端创建一个 SseEmitter 对象,把请求“挂起”后立刻 return。Controller 线程瞬间释放回线程池去处理其他请求,真正的推流工作交给后台异步线程去干。


    二、 揭秘 Spring Boot SseEmitter 核心机制

    SseEmitter 是 Spring MVC 提供的异步响应工具。它最大的特点是:除非你主动调用 emitter.complete(),否则这个 HTTP 连接永远不断开。

    它的线程模型是怎样的?

    这是面试常考、实战必知的核心:

  • Controller 线程(Tomcat 线程池):
    • 接收 HTTP 请求。
    • new SseEmitter() 创建对象。
    • 把 Emitter 丢给异步任务(比如丢给自定义的线程池)。
    • 直接返回 Emitter 对象,此时 Tomcat 线程任务结束,归位!
  • 推送线程(你自己的异步线程):
    • 拿着 Emitter 对象去调用大模型的流式 API。
    • 在回调里,每收到一个字,就调用 emitter.send(数据) 转发给前端。
    • 大模型说完,调用 emitter.complete() 关闭连接。

  • 三、 完整链路:从大模型到前端的“搬运工”

    整个数据流转链路如下:

    前端发起请求
    ↓
    Controller 创建 SseEmitter,启动异步线程,立即 Return
    ↓
    异步线程调用大模型 API(使用 OkHttp 等客户端)
    ↓
    大模型返回数据流,逐行读取 data: 行
    ↓
    解析出 delta.content(真正的内容)
    ↓
    通过 emitter.send() 推给前端
    ↓
    收到大模型的 [DONE] 标记 → emitter.complete() 关闭连接

    💻 Java 核心代码骨架(伪代码)

    @GetMapping("/chat")
    public SseEmitter chat(String question) {
    // 1. 设置超时时间(例如3分钟),防止连接永远挂着
    SseEmitter emitter = new SseEmitter(180000L);

    // 2. 丢给线程池执行,不阻塞主线程
    threadPool.execute(() -> {
    try {
    // 3. 调用大模型流式接口(大模型的回调里)
    llmClient.streamChat(question, new Callback() {
    public void onToken(String token) {
    // 4. 核心:把字发给前端
    emitter.send(SseEmitter.event().data(token));
    }
    public void onComplete() {
    emitter.complete(); // 结束
    }
    public void onError(Exception e) {
    emitter.completeWithError(e); // 报错结束
    }
    });
    } catch (Exception e) {
    emitter.completeWithError(e);
    }
    });

    return emitter;
    }


    四、 生产环境的终极避坑指南(极度重要)

    在本地跑通很容易,但上生产环境,你会遇到一堆诡异的问题。

    1. 超时设置(两道关卡)

    • SseEmitter 的超时:如果设得太短,模型还没生成完就断了;设得太长,空闲连接会占满服务器资源。
    • HTTP 客户端(如 OkHttp)的 readTimeout:大模型如果 30 秒不吐字(可能卡住了),你的客户端要主动断开,否则会引发内存泄漏。

    2. 心跳保活(防中间件杀连接)

    由于 Nginx 或网关有默认的 proxy_read_timeout(通常 60s),如果大模型半天不吐字,网关会认为连接死了直接掐断。

    • 解法:后端开启一个定时任务,每 15 秒调用一次 emitter.send(SseEmitter.event().comment("heartbeat")) 给前端发个空注释。

    3. 客户端断连检测

    用户在网页上点了“停止生成”,或者直接关了浏览器,你的后台可能还在傻傻地接收大模型的数据并 send()。

    • 解法:监听 Emitter 的生命周期。

    emitter.onCompletion(() -> { /* 释放资源,停止调用大模型 */ });
    emitter.onTimeout(() -> { /* 超时清理 */ });
    emitter.onError((e) -> { /* 报错清理 */ });

    4. 🚨 Nginx 反向代理配置(必改)

    如果你用了 Nginx,默认的缓冲机制会导致 SSE 失效。Nginx 会把后端发来的流攒满一个 Buffer 再发给前端,导致前端感觉是一卡一卡地出字。
    必须在 Nginx 配置里关闭缓冲:

    location /api/chat {
    proxy_pass http://backend_server;
    proxy_http_version 1.1;
    proxy_set_header Connection '';

    # 核心:关闭 Nginx 缓存
    proxy_buffering off;
    proxy_cache off;
    chunked_transfer_encoding on;
    }

    5. 负载均衡与无状态设计

    如果你的后端部署了多台机器(如 K8s 集群)。用户第一次请求打到了机器 A,建立长连接。由于长连接可能持续几分钟,期间如果网络波动重连,打到了机器 B,上下文就丢了。

    • 解法:使用Sticky Session(会话粘滞)(如 Nginx 的 ip_hash),或者保证你的后端是无状态的(把会话历史存 Redis,哪台机器接都能续上)。

    五、 前端怎么接?(EventSource vs Fetch)

    1. 原生 EventSource (简单但有局限)

    const es = new EventSource('/api/chat?question=你好');
    es.onmessage = (e) => { console.log(e.data); };

    • 致命缺点:只支持 GET 请求,不能传复杂参数,不能自定义 Header(比如无法传 Token)。

    2. fetch API + ReadableStream (现代主流方案)

    fetch('/api/chat', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer xxx' },
    body: JSON.stringify({ question: '你好' })
    }).then(response => {
    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    // 循环读取流数据…
    });

    • 优点:支持 POST,完全自定义 Header,目前绝大多数 AI 应用的打字机效果都是这么实现的。

    总结

    要做一个好用的 AI 聊天应用,SSE 服务端不仅是“二传手”,更是保证高并发、防资源泄漏、防网关掐断的“大管家”。

    把 SseEmitter 的线程模型搞懂,把 Nginx 缓冲关掉,把心跳和清理回调加上,你的 SSE 服务端就能稳如老狗了。


    如果觉得这篇干货对你有帮助,求个免费的赞和收藏!我们下期见!

    赞(0)
    未经允许不得转载:171主机测评 » 系统学习agent系列之SSE服务端
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址