在接入大模型对话或AI文本生成能力时,如果等待服务端把全部结果计算完再一次性返回,用户往往要盯着加载圈十几秒甚至更久。PHP作为典型的服务端脚本,天然习惯“算完再发”,但借助HTTP本身的流式特性和SSE协议,完全可以做到边生成边把片段推送到前端,让打字机效果在浏览器上实时呈现。

一、理解SSE与PHP输出机制的关系
SSE全称Server-Sent Events,是浏览器原生支持的单向推送协议,基于HTTP且使用文本流格式。它和PHP传统的“脚本执行结束才刷出内容”并不冲突,只是要求我们在代码运行期间主动把缓冲区里的数据发给客户端,而不是等脚本退出。很多新手以为PHP不能做流式,其实是被默认开启的多层输出缓冲误导了。
在PHP中,数据从脚本到浏览器会经过SAPI缓冲、用户缓冲(ob_start)、Web服务器缓冲(如Nginx的proxy_buffering)几道关卡。只要任何一层没关,前端就收不到实时片段。因此处理AI流式响应的第一步,是显式关闭这些缓冲并告诉浏览器不要等结束。下面这段代码展示了最基础的环境准备。
<?php
// 关闭PHP用户级输出缓冲
while (ob_get_level() > 0) {
ob_end_flush();
}
// 关闭PHP自动压缩造成的缓冲
ini_set('zlib.output_compression', 'Off');
// 设置SSE所需响应头
header('Content-Type: text/event-stream; charset=utf-8');
header('Cache-Control: no-cache');
header('Connection: keep-alive');
header('X-Accel-Buffering: no'); // 针对Nginx关闭代理缓冲
?>
上面的header('X-Accel-Buffering: no')非常关键,若部署在Nginx后面且未关闭代理缓冲,Nginx会攒够一定字节才转发,SSE就会失效。另外AI接口通常返回自身也是流式的,比如OpenAI的stream=true模式,PHP需要用fopen或curl_setopt带CURLOPT_WRITEFUNCTION来边读边转发,而不是file_get_contents等阻塞式读取。
二、从AI接口读取并实时转发给浏览器
假设我们调用一个支持流式输出的AI网关,它返回的是形如data: {...}nn的SSE片段。PHP作为中间层,应当用非阻塞或回调方式读取上游流,每拿到一段就立刻包装成自己的SSE事件推给前端。下面示例用curl的写回调实现边收边发,并加了心跳防止代理断开。
<?php
header('Content-Type: text/event-stream; charset=utf-8');
header('Cache-Control: no-cache');
header('Connection: keep-alive');
header('X-Accel-Buffering: no');
while (ob_get_level() > 0) { ob_end_flush(); }
$ch = curl_init('https://ipipp.com/ai/v1/stream');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Accept: text/event-stream'));
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $chunk) {
// 上游可能是多个SSE块拼在一起,简单按行处理
$lines = explode("n", $chunk);
foreach ($lines as $line) {
if (strpos($line, 'data:') === 0) {
// 向前端推送同样格式
echo "event: tokenn";
echo "data: " . trim(substr($line, 5)) . "nn";
flush();
}
}
return strlen($chunk);
});
// 每20秒发一个注释心跳保活
$start = time();
curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, function($dltotal, $dlnow) use (&$start) {
if (time() - $start > 20) {
echo ": heartbeatnn";
flush();
$start = time();
}
return 0;
});
curl_setopt($ch, CURLOPT_NOPROGRESS, false);
curl_exec($ch);
curl_close($ch);
?>
这段代码里flush()的作用是强制把PHP已输出的内容送到Web服务器。若前面缓冲没关干净,这一句就不会生效。前端使用EventSource对象监听token事件,拿到数据就追加到页面,体验上就是字一个个蹦出来。相比前端直接连AI服务商,用PHP中转能隐藏密钥、统一鉴权并做内容审计。
值得注意的是,AI流式响应常带usage统计或结束标记,PHP应当识别上游的data: [DONE]之类的信号,向前端发一个event: done再exit,避免连接悬空。同时也要捕获curl错误,用event: error通知前端,否则用户会一直等待。
三、前端接收与实时渲染的常见做法
浏览器端不需要任何额外库,原生EventSource即可消费PHP推来的流。它自动处理重连,且只支持GET,因此若AI中转接口需要POST参数,常见方案是前端先拿一个带token的GET流式地址,或由PHP用会话态绑定。下面代码展示如何把流式文本填进一个div容器。
<div id="output"></div>
<script>
var es = new EventSource('/ai_stream.php?sid=abc123');
var box = document.getElementById('output');
es.addEventListener('token', function(e) {
var data = JSON.parse(e.data);
box.textContent += data.content;
window.scrollTo(0, document.body.scrollHeight);
});
es.addEventListener('done', function(e) {
es.close();
});
es.addEventListener('error', function(e) {
box.textContent += 'n[连接异常]';
es.close();
});
</script>
在真实项目里,直接用textContent累加可能性能不佳,若AI每秒推几十个片段,应改用DocumentFragment或先存数组再节流渲染。另外SSE只能服务端到客户端单向,如果用户要中断生成,前端可以调用另一个接口设置Redis标记,PHP在转发循环里每次usleep后读一下标记,发现取消就退出流。
对比轮询方案,SSE省去了大量重复HTTP握手和空响应,延迟通常低一个数量级;对比WebSocket,SSE不需自己管协议帧,断线重连浏览器原生支持,非常适合AI文本这种“服务器一直说、客户端只听”的场景。PHP虽然常被认为不适合长连接,但配合SSE与正确缓冲设置,完全能撑起中小规模的实时数据显示需求。
PHPAI_streamingSSE修改时间:2026-08-15 00:54:40