最近在给一个PHP项目接入AI对话的功能,类似于ChatGPT,但是使用的是百度的千帆大模型。在参考ChatGPT和其他同类功能产品上发现他们的输出方式都是流式输出的方式进行信息返回,为了实现相同的效果特此学习并且记录在这里。
什么是流式输出
流式输出是指在数据产生的同时将其逐步发送给客户端,而不是等待所有数据生成完毕后再发送。
为什么要使用流式输出
ChatGPT作为一个基于深度学习的大型语言模型,需要处理大量的自然语言数据,这无疑需要大量的计算资源和时间。相较于普通的读取数据库操作,其响应速度自然会慢许多。
对于这种可能需要长时间等待响应的对话场景,ChatGPT采用了一种巧妙的策略:它会将已经计算出的数据“推送”给用户。使用流式返回模型一旦计算出结果立即推送给用户,避免用户长时间等待。
实现流式输出的方法
一个是使用WebSocket,另一种则是SSE(Server-Sent Events)
WebSocket:全双工通信的实现,WebSocket允许在单个TCP上进行全双工通讯,客户端和服务端在握手成功之后会一直保持打开状态直到显式关闭。在打开状态客户端和服务端能够自由通信,客户端能主动发送消息给服务端而服务端也能主动推送消息给客户端。
SSE(Server-Sent Events):单项通讯。和WebSocket一样也是一种长连接但是SSE不能由客户端给服务端发消息,只能服务端向客户端发送消息。如果SSE连接中断会自动尝试重现连接,确保连接的稳定性。
选择SSE的原因
首先对两者进行一个比较:
数据推送方向:SSE主要支持从服务器到客户端的单向通信,这意味着服务器可以主动地向客户端推送数据。而WebSocket则支持双向通信,允许服务器和客户端之间进行实时的数据交换。
连接建立:SSE利用基于HTTP的长连接,通过常规的HTTP请求和响应来建立连接,进而实现数据的实时推送。相反,WebSocket采用自定义的协议,通过创建WebSocket连接来实现双向通信。
兼容性:由于SSE基于HTTP协议,因此它可以在大多数现代浏览器中使用,并且无需进行额外的协议升级。虽然WebSocket在绝大多数现代浏览器中也得到了支持,但在某些特定的网络环境下可能会遇到问题。
适用场景:SSE适合于需要服务器向客户端实时推送数据的场景,例如股票价格更新、新闻实时推送等。而WebSocket则适合于需要实时双向通信的场景,如聊天应用、多人在线协作编辑等。
从上述对比中可以知道:在通信模式上,对于ChatGPT这样的应用来说,大多数情况下,用户的请求是稀疏的,而服务器的响应是密集的,因此,SSE的单向通信模式更为合适。在网络协议上,SSE运行在HTTP协议上,因此,它可以提供更高的兼容性和灵活性。举个例子,如果你的产品已经部署在Web服务器上,那么你大概率无需做任何改动,就可以使用SSE技术。而WebSocket则需要单独的服务器和端口。
除此之外,SSE和WebSocket在消息大小、连接数量、跨域支持等方面都有一些细微的差别,我们在具体设计时需要根据实际需求和制约因素做出选择。
简单的代码实现
这里使用的是百度的千帆大模型
<?php
class Sample {
const API_KEY = "你的API_KEY";
const SECRET_KEY = "你的秘钥 SECRET_KEY";
public function run() {
// 这里要告诉浏览器你的返回类型:text/event-stream 表明这是一个SSE连接
header('Content-Type: text/event-stream');
// 用于指示浏览器或其他客户端不要缓存该响应
header('Cache-Control: no-cache');
// 这里非常重要!! 这是必须要有的
header('X-Accel-Buffering: no');
// 用于控制 HTTP 连接在请求/响应之后是否应该保持打开状态
header('Connection: keep-alive');
header("Access-Control-Allow-Origin: *");
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions_pro?access_token={$this->getAccessToken()}",
CURLOPT_TIMEOUT => 0,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS =>'{"messages":[{"role":"user","content":"你好"}],"stream": true,"temperature":0.95,"top_p":0.8,"penalty_score":1,"disable_search":false,"enable_citation":false}',
CURLOPT_HTTPHEADER => array(
'Content-Type: application/x-www-form-urlencoded'
),
CURLOPT_WRITEFUNCTION => function($curl, $data) {
echo "data: " . $data . "\n\n";
ob_flush();
flush();
return strlen($data);
}
));
curl_exec($curl);
curl_close($curl);
}
/**
* 使用 AK,SK 生成鉴权签名(Access Token)
* @return string 鉴权签名信息(Access Token)
*/
private function getAccessToken(){
$curl = curl_init();
$postData = array(
'grant_type' => 'client_credentials',
'client_id' => self::API_KEY,
'client_secret' => self::SECRET_KEY
);
curl_setopt_array($curl, array(
CURLOPT_URL => 'https://aip.baidubce.com/oauth/2.0/token',
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => http_build_query($postData)
));
$response = curl_exec($curl);
curl_close($curl);
$rtn = json_decode($response);
return $rtn->access_token;
}
}
$rtn = (new Sample())->run();特别注意!!

上述代码中的 header('X-Accel-Buffering: no');颇为重要,在我进行开发的时候因为没加上这一条导致踩了很多坑,如果你不加上这一条那么你的结果应该是模型的输出信息一直会存储在缓冲流中,等到所有结果都返回之后再从缓冲流全部输出,很显然这并不是我想要的。这个指令并不是HTTP标准的一部分,而是Nginx的一个特定指令。当你在代码中出现header('X-Accel-Buffering: no');时你实际上是在与 Nginx 的某些内部功能进行交互。
X-Accel-Buffering: no的主要目的是告诉 Nginx 不要缓冲响应到客户端。默认情况下,Nginx 可能会尝试缓冲整个响应,然后再将其发送给客户端。但如果你设置了这个头部,Nginx 将实时地将响应发送给客户端,而不是等待整个响应被完全接收。
最后实现效果如下:
