DeepSeek之前端利用SSE技术逐步显示推理结果

一、背景

        DeepSeek、文心一言、豆包、通义千问, 大家用这些大模型的时候,都会有一个对话框。你输入问题,然后大模型回答你的答案,都会有一个逐步显示的过程效果,  而不是一次性返回整个答案。

        一开始我以为是使用到了Websocket技术,后端拿到问题数据以后,逐步往前端推送内容,前端再累加显示整个内容。  但是F12开启以后,没有抓到WS的相关连接信息。 我这就很好奇了,怎么实现这个过程的呢?

        是前端本来就一次性拿到了结果,只是做了一个逐步显示的效果,还是说确实就是后端往前端逐步推送内容呢? 实践是检验真理的唯一标准,测试几个大模型的网络请求就知道了。

二、F12测试各大模型的实现原理

1、DeepSeek的实现

        响应头信息Context-type:  text/event-stream, 和普通的application/json有所差异。

        看着就是一种事件stream流的机制。server端往前端逐步推送消息,前端进行逐步渲染显示.

2、豆包AI的实现

        继续查看豆包AI的实现,和DeepSeek没有差异,基本也是使用了text/event-stream的响应内容头. 没发现Websocket相关链接信息

三、SSE技术

1、简介

        查询资料才知道,上面使用到的是HTTP SSE技术.

        HTTP SSE(Server-Sent Events,服务器发送事件)是一种在 Web 应用中实现服务器向客户端推送实时数据的技术,它基于 HTTP 协议,以下是关于它的详细介绍:

  1. 基本概念SSE 允许服务器将实时更新的数据主动推送给已建立连接的客户端。在传统的 Web 交互中,通常是客户端向服务器发送请求,然后服务器返回响应,这是一种 “请求 - 响应” 模式。而 SSE 打破了这种单向的模式,使服务器能够在有新数据时主动将数据推送给客户端,无需客户端频繁地发送请求来获取更新
  2. 工作原理
    • 建立连接:客户端通过 JavaScript 的 EventSource 对象向服务器发起一个 HTTP GET 请求来建立连接。这个连接会保持打开状态,以便服务器能够随时向客户端发送数据。
    • 服务器推送数据:服务器可以在任意时刻向客户端发送数据,数据以特定格式的文本(通常是 text/event-stream 类型)发送。数据格式一般包含事件类型(如 message 等)和实际的数据内容。
    • 客户端接收处理:客户端的 EventSource 对象会监听连接上的事件,当接收到服务器发送的数据时,会根据数据的类型触发相应的事件处理函数,从而对数据进行处理和展示。
  3. 特点
    • 单向通信SSE 是一种单向的通信机制,即数据只能从服务器推送到客户端。如果客户端需要向服务器发送数据,仍然需要使用传统的 HTTP 请求(如 XMLHttpRequestfetch)。
    • 轻量级相比其他实时通信技术(如 WebSocket),SSE 的实现相对简单,不需要复杂的握手过程和协议管理,对服务器和客户端的资源消耗较小
    • 兼容性好:SSE 得到了大多数现代浏览器的支持,包括 Chrome、Firefox、Safari 等,这使得它在 Web 应用开发中具有广泛的适用性。
  4. 应用场景
    • 实时新闻和通知:例如新闻网站可以使用 SSE 实时推送最新的新闻文章和更新通知给用户。
    • 股票行情和金融数据:金融应用可以通过 SSE 实时获取股票价格、汇率等金融数据的更新。
    • 监控和日志系统:服务器可以将系统的监控信息、日志记录等实时推送给客户端的监控界面,方便管理员及时了解系统状态。

2、对比和分析

      为什么没采用Websocket来实现呢?    Websocket来实现成本比SSE高。 因为在这种场景下,用户每次的回答,都可以理解单独发送一次HTTP请求即可,只是Server往客户端推送需要流的处理方式,因为推理过程不是一次性拿到结果的,所以正好结合SSE技术,往客户端推送一次即可。

        为了这个需求单独弄Websocket服务开发和维护成本太高了,没必要。 

四、代码演示

1、golang编写服务端

        编写了一个监听8080端口,提供一个/stream的接口。这个接口会设置content-type: text/event-stream, 和客户端保持长连接,只要客户端/服务端不主动断开这个链接,server端可以往客户端推送数据流。

package main

import (
	"fmt"
	"log"
	"net/http"
	"time"
)

// 处理 SSE 请求
func sseHandler(w http.ResponseWriter, r *http.Request) {
	// 设置响应头,指定内容类型为 text/event-stream
	w.Header().Set("Content-Type", "text/event-stream")
	w.Header().Set("Cache-Control", "no-cache")
	w.Header().Set("Connection", "keep-alive")
	w.Header().Set("Access-Control-Allow-Origin", "*")

	// 确保连接是可写的
	flusher, ok := w.(http.Flusher)
	if !ok {
		http.Error(w, "Streaming unsupported!", http.StatusInternalServerError)
		return
	}

	// 模拟定时发送消息
	ticker := time.NewTicker(2 * time.Second)
	defer ticker.Stop()

	for {
		select {
		case <-r.Context().Done():
			return
		case <-ticker.C:
			// 生成要发送的数据
			message := fmt.Sprintf("data: %s\n\n", time.Now().Format(time.RFC3339))
			// 将数据写入响应
			fmt.Fprint(w, message)
			// 刷新缓冲区,确保数据立即发送到客户端
			flusher.Flush()
		}
	}
}

func main() {
	// 注册 SSE 处理函数到 /stream 路径
	http.HandleFunc("/stream", sseHandler)
	log.Println("Server started on :8080")
	// 启动 HTTP 服务器
	log.Fatal(http.ListenAndServe(":8080", nil))
}

2、php原生代码实现样例

<?php
// 设置响应头
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: keep-alive');

// 禁用输出缓冲
ob_end_flush();
ob_implicit_flush(true);

// 模拟实时数据推送
$counter = 0;
while ($counter <= 5) {
    // 生成要推送的数据
    $data = "Message number: " . $counter;

    // 按照 SSE 规范发送数据
    echo "data: $data\n\n";

    // 刷新输出缓冲区,确保数据立即发送到客户端
    flush();

    // 增加计数器
    $counter++;

    // 暂停 1 秒
    sleep(1);
}
?>

curl运行效果:

 

3、前端html页面

        创建EventSource对象,第一次直接连接上/stream接口,之后设置对应的事件监听回调函数,处理对应逻辑。每次onmessage拿到新的流数据,则进行文本追加显示即可。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SSE Example</title>
</head>

<body>
    <h1>Server-Sent Events Example</h1>
    <ul id="messages"></ul>

    <script>
        // 创建 EventSource 对象,连接到服务器的 /stream 路径
        const eventSource = new EventSource('http://localhost:8080/stream');

        // 监听 message 事件,当接收到消息时触发
        eventSource.onmessage = function (event) {
            // 获取消息列表元素
            const messagesList = document.getElementById('messages');
            // 创建新的列表项
            const newMessage = document.createElement('li');
            // 将消息内容设置为列表项的文本
            newMessage.textContent = event.data;
            // 将新的列表项添加到消息列表中
            messagesList.appendChild(newMessage);
        };

        // 监听 error 事件,当连接出错时触发
        eventSource.onerror = function (error) {
            console.error('EventSource failed:', error);
        };
    </script>
</body>

</html>

4、运行效果

五、总结

        SSE技术确实很实用,相比Websocket的实现简直是太轻量级了。 如果正好你的场景符合,那么首选SSE技术,而非Websocket。 Websocket开发、运维遇到的相关风险性、难度都比SSE高太多。

### Java DeepSeek 流式输出示例 为了实现在Java中通过DeepSeek库进行流式输出,可以采用Spring Boot框架来构建后服务。此方法允许客户实时接收数据更新而无需频繁发起请求。 #### 后配置与依赖引入 首先,在`pom.xml`文件中加入必要的Maven依赖项以支持WebFlux模块以及DeepSeek SDK: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <!-- 假设这是DeepSeek的SDK --> <dependency> <groupId>com.deepseek.sdk</groupId> <artifactId>deepseek-sdk</artifactId> <version>${latest.version}</version> </dependency> ``` #### 创建Controller类处理HTTP请求并返回流响应 定义一个控制器用于接受来自前的应用程序/JSON格式的消息体,并调用业务逻辑层获取聊天回复内容作为服务器发送事件(Server-Sent Events, SSE)的形式推送给浏览器或其他订阅者: ```java @RestController @RequestMapping("/api/stream") public class ChatStreamController { @Autowired private ChatService chatService; @GetMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<String> getChatResponse(@RequestParam String message){ return chatService.generateResponseAsStream(message); } } ``` 上述代码片段展示了如何设置路由路径为`/api/stream/chat`的服务接口,它能够监听GET类型的网络访问并将结果封装成SSE消息序列化给定文本字符串形式的数据流传回至用户界面[^1]。 #### Service 层实现具体功能 接着是在`ChatService.java`里编写实际执行的任务——即向DeepSeek平台提交查询请求并解析其产生的逐条应答信息转交给上一层级组件进一步加工处理前转换为适合传输结构的对象实例集合: ```java @Service @Slf4j public class ChatService { @Value("${deepseek.api.key}") private String apiKey; public Flux<String> generateResponseAsStream(String userInput){ // 初始化DeepSeek Client对象 var client = new DeepSeekClient(apiKey); // 调用API获得异步流式的回应 return Flux.create(sink -> { try (var responseStream = client.query(userInput)) { responseStream.forEach(responsePart -> sink.next(responsePart)); sink.complete(); } catch (Exception e) { log.error("Error while processing stream.", e); sink.error(e); } }); } } ``` 这段源码实现了利用Reactor Core中的`Flux.create()`函数创建自定义发布者的能力,从而可以在接收到每一个部分的结果之后立即传递出去而不必等待整个过程结束再一次性全部吐出来;同时确保资源得到妥善释放并且异常情况也能被正确捕获报告[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员Rocky

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值