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