一、SSE流式回传

在平时开发的时候,想要模拟ChatGPT、DeepSeek等大模型对话的流式响应,基于 SSE 的事件流传输是一个好的选择。

在调用大模型 API 的时候,处理自然语言需要大量的计算资源和时间,响应速度肯定比普通的读数据库要慢很多。

访问接口的等待时间过长,从用户的使用体验上就不合适。对于这种对话场景,我们将用户的 prompt 传入到 大模型API 中后,将先计算出的数据 ”推送“ 给用户,采用 SSE 技术边计算边返回,避免用户因为等待时间过长关闭页面。

从后端代码实现上分析:

// 创建 SSE 发射器
SseEmitter emitter = new SseEmitter(0L);

在 SpringBoot 中,SseEmitter 是用于支持 Server-Sent Events(SSE)的类,允许服务器向客户端推送数据。SseEmitter 的构造函数可以接受一个 long 类型的参数,表示客户端连接的超时时间(以毫秒为单位)。当连接超过了设置的时间,连接就会自动关闭。

我们在项目中可以根据实际场景需求任意设置连接时间。这里为了测试,我们设置了 0L ,表示没有超时限制。

//设置火山引擎连接,接入 大模型API
ArkService service = ArkService.builder()
                .timeout(Duration.ofSeconds(1800))
                .connectTimeout(Duration.ofSeconds(600))
                .dispatcher(dispatcher)//开启并管理异步请求
                .connectionPool(connectionPool)//设置连接池
                .baseUrl("火山引擎的api网址")
                .apiKey("你的apiKey")
                .build();

构建请求参数 ChatCompletionRequest ,通过 ArkService 发送对话请求,最终以流式形式返回

try {
    service.streamChatCompletion(streamChatCompletionRequest)
            .doOnError(Throwable::printStackTrace)
            .blockingForEach(
                delta -> {
                          if (!delta.getChoices().isEmpty()) {
                             if(StringUtils.isNotEmpty(delta.getChoices().get(0)
                                .getMessage().getReasoningContent())) {    System.out.print(delta.getChoices()                                                                     .get(0).getMessage().getReasoningContent());
                               emitter.send(delta.getChoices().get(0).getMessage());
                        } else {                                                   System.out.print(delta.getChoices()
.get(0).getMessage().getContent());
                               emitter.send(delta.getChoices().get(0).getMessage());
                                           
                                }
                              }
                           });
    //发送
    emitter.complete();
} catch (Exception e) {
     emitter.completeWithError(e);
} finally {
     service.shutdownExecutor();
     executor.shutdown();
}

最终实现的效果图如下:

效果非常的nice啊!

二、Nginx部署

在本地前后端联调结束,效果达到预期。开启下一步吧,部署到服务器上!!

配置Nginx信息,在 nginx.conf 配置文件中,在 server 代码块内设置监听端口号(HTTPS,端口号为443),域名地址(server_name),ssl证书信息(ssl_certificate,ssl_certificate_key)。在 location 代码块中设置代理地址。

具体设置如下:

server {
        listen 443 ssl;
        server_name chat.med-ke.com;

        ssl_certificate 配置证书.crt;
        ssl_certificate_key 配置证书.key;

        # 代理地址
        location /chat-api/ {
            proxy_pass http://localhost:8080/;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

}

Nginx配置就此完成,然后对打包好的 Java web 工程放到服务器上运行。

查看端口号8080是否被占用

sudo lsof -i :8080

如果被占用,那就 kill 这个进程号

sudo kill -9 进程ID

上传 jar 包,开启后台运行命令

nohup java -jar 程序代码.jar &

大功告成,登录小程序端开启我们的AI对话之旅吧。

以上操作都没问题的情况下,不出意外的话意外就要发生了。

三、SSE与Nginx之间的冲突

当我们登录小程序去测试时发现,响应内容有时会出现卡顿,AI 给出的对话会分段给出,每段之间隔个3、4秒,非常不连贯,导致使用体验非常差。

开始排查问题原因:

一开始我们都以为是数据传的太大了,导致的返回响应慢。因为我们在做测试的时候都是传的照片信息,然后附带提问内容来测试的。然后我们就进行了多次测试:短问题、短问题+图片信息、长问题、长问题+照片信息,甚至当输入”你好“的时候,也会出现卡顿的现象。说明不是数据量的问题。

继续排查:

我们在部署到 Nginx 服务器之前,本地前后端联调的时候,页面内容的响应还是很丝滑的。有没有可能是 Nginx 出现了什么问题。最终,问题果然出现在了 Nginx 配置上。

在 Nginx 的配置文件中,有两个字段需要我们手动关闭。分别是 proxy_buffering proxy_cache

proxy_buffering :这个字段默认是开启的,本来的目的是传输数据时要等待缓存区满才返回,但是这与我们采用 SSE 流式实时传输相违背。所以要禁掉缓冲,才能正确处理 SSE 流式数据。

proxy_cache :这个字段默认也是开启的,本来的目的是启用 Nginx 的代理缓存,它会把传回前端的数据给缓存起来。导致在 SSE 流式传输的时候,会返回相同的缓存数据,破坏了 SSE 的实时性和准确性。

修改完的配置:

server {
        listen 443 ssl;
        server_name chat.med-ke.com;

        ssl_certificate 配置证书.crt;
        ssl_certificate_key 配置证书.key;

        # 代理地址
        location /chat-api/ {
            # 取消缓冲
            proxy_buffering off;
            # 关闭代理缓存
            proxy_cache off;
            proxy_pass http://localhost:8080/;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
}

OK,到这里我们的问题就解决了!!

Categories:

Tags:

No responses yet

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注