下面为你详细介绍如何使用 PHP、Nginx 、豆包AI,来实现流式输出,这里的流式输出以模拟 AI 逐步返回内容的场景为例,就像豆包回复消息时那样逐步呈现内容。(其他AI平台只需要更换对应的接口代码,流式输出大同小异)
以下代码仅在nginx上实现,至于apache环境还需深入研究为什么不能达到流式输出。
方案一:独立的php文件,不依赖任何框架
<?php
if (empty($_POST) && empty($_GET)) {
?>
<!DOCTYPE html>
<html>
<head>
<title>豆包AI流式对话</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>
<div>
<input type="text" id="inputText" placeholder="输入你的问题">
<button onclick="startStream()">发送</button>
</div>
<div id="output" style="white-space: pre-wrap; margin-top: 20px;"></div>
<script>
let outputDiv = document.getElementById('output');
function startStream() {
const inputText = document.getElementById('inputText').value;
outputDiv.innerHTML = ''; // 清空之前的输出
const eventSource = new EventSource('http://域名/Chat.php?text=' + encodeURIComponent(inputText));
eventSource.onmessage = function(e) {
try {
const data = JSON.parse(e.data);
console.log(data.content)
outputDiv.innerHTML += data.content; // 实时追加内容
} catch (err) {
console.error('解析错误:', err);
}
};
eventSource.onerror = function(err) {
console.error('EventSource 错误:', err);
eventSource.close();
};
}
</script>
</body>
</html>
<?php
} else {
ob_implicit_flush(true); // 开启隐式刷新
// 设置响应头,启用流式输出
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('X-Accel-Buffering: no'); // 防止Nginx缓存
header('Connection: keep-alive');
ob_end_clean(); // 清除输出缓冲
// 获取用户输入
$input = empty($_REQUEST['text']) ? '建站系统推荐几个好用的CMS' : $_REQUEST['text'];
// 调用豆包AI流式API
callDoubaoStreamAPI($input);
/*
// 豆包API的URL
$apiUrl = 'https://ark.cn-beijing.volces.com/api/v3/chat/completions'; // 替换为实际的API端点
// 准备请求数据
$data = [
'model' => 'ep-20241224163854-tg2d8',
'messages' => [
[
'role' => 'user',
'content' => '易优CMS建站系统怎么样?',
]
],
'stream' => true // 启用流式响应
];
// 初始化Guzzle客户端
$client = new Client();
try {
// 发送POST请求
$response = $client->post($apiUrl, [
'headers' => [
'Authorization' => 'Bearer 31713d10-4bd6-4703-8c7d-efdfc502e0d1', // 替换为实际的访问令牌
'Content-Type' => 'application/json'
],
'json' => $data,
'stream' => true // 启用流式响应
]);
// 逐块读取响应内容并输出
$body = $response->getBody();
// $body = $response->getBody()->getContents();
// $body = !empty($body) ? json_decode($body, true) : [];
// var_dump($body);exit;
while (!$body->eof()) {
$chunk = $body->read(1024);
echo $chunk;
ob_flush();
flush();
}
} catch (\Exception $e) {
// 处理异常
echo 'Error: '. $e->getMessage();
}*/
exit;
return;
}
// 调用豆包流式API
function callDoubaoStreamAPI($prompt)
{
$apiUrl = "https://ark.cn-beijing.volces.com/api/v3/chat/completions"; // 替换为实际API地址
$apiKey = "*****************"; // 替换为实际API密钥
$postData = [
'model' => 'ep-20241224163854-tg2d8',
'messages' => [
[
'role' => 'user',
'content' => $prompt,
]
],
'stream' => true // 启用流式响应
];
$headers = [
'Content-Type: application/json',
'Authorization: Bearer ' . $apiKey
];
httpRequest($apiUrl, 'POST', json_encode($postData, JSON_UNESCAPED_UNICODE), $headers, 300000);
// $ch = curl_init();
// curl_setopt_array($ch, [
// CURLOPT_URL => $apiUrl,
// CURLOPT_POST => true,
// CURLOPT_RETURNTRANSFER => true,
// CURLOPT_HTTPHEADER => [
// 'Content-Type: application/json',
// 'Authorization: Bearer ' . $apiKey
// ],
// CURLOPT_POSTFIELDS => json_encode([
// 'model' => 'ep-20241224163854-tg2d8',
// 'messages' => [
// [
// 'role' => 'user',
// 'content' => $prompt,
// ]
// ],
// 'stream' => true // 启用流式响应
// ]),
// CURLOPT_WRITEFUNCTION => function($ch, $data) {
// var_dump($data);exit;
// // 处理流式数据(按豆包API实际格式解析)
// processStreamData($data);
// return strlen($data); // 必须返回处理的数据长度
// }
// ]);
// curl_exec($ch);
// curl_close($ch);
}
function httpRequest($url, $method = "GET", $postfields = null, $headers = array(), $timeout = 30, $debug = false)
{
$method = strtoupper($method);
$ci = curl_init();
/* Curl settings */
// curl_setopt($ci, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); // 使用哪个版本
curl_setopt($ci, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 6.2; WOW64; rv:34.0) Gecko/20100101 Firefox/34.0");
curl_setopt($ci, CURLOPT_CONNECTTIMEOUT, 60); /* 在发起连接前等待的时间,如果设置为0,则无限等待 */
// curl_setopt($ci, CURLOPT_TIMEOUT, 7); /* 设置cURL允许执行的最长秒数 */
curl_setopt($ci, CURLOPT_TIMEOUT, $timeout); /* 设置cURL允许执行的最长秒数 */
curl_setopt($ci, CURLOPT_RETURNTRANSFER, true);
switch ($method) {
case "POST":
curl_setopt($ci, CURLOPT_POST, true);
if (!empty($postfields)) {
if (is_string($postfields) && preg_match('/^([\w\-]+=[\w\-]+(&[\w\-]+=[\w\-]+)*)$/', $postfields)) {
parse_str($postfields, $output);
$postfields = $output;
}
if (is_array($postfields)) {
$tmpdatastr = http_build_query($postfields);
} else {
$tmpdatastr = $postfields;
}
curl_setopt($ci, CURLOPT_POSTFIELDS, $tmpdatastr);
}
break;
default:
curl_setopt($ci, CURLOPT_CUSTOMREQUEST, $method); /* //设置请求方式 */
break;
}
curl_setopt($ci, CURLOPT_WRITEFUNCTION, function($ci, $data){
// 处理流式数据(按豆包API实际格式解析)
processStreamData($data);
return strlen($data); // 必须返回处理的数据长度
});
$ssl = preg_match('/^https:\/\//i', $url) ? TRUE : FALSE;
curl_setopt($ci, CURLOPT_URL, $url);
if ($ssl) {
curl_setopt($ci, CURLOPT_SSL_VERIFYPEER, FALSE); // https请求 不验证证书和hosts
curl_setopt($ci, CURLOPT_SSL_VERIFYHOST, FALSE); // 不从证书中检查SSL加密算法是否存在
// curl_setopt($ci, CURLOPT_SSLVERSION, 4); //因为之前的POODLE 病毒爆发,许多网站禁用了sslv3(nginx默认是禁用的,ssl_protocols 默认值为TLSv1 TLSv1.1 TLSv1.2;),最新使用sslv4
}
//curl_setopt($ci, CURLOPT_HEADER, true); /*启用时会将头文件的信息作为数据流输出*/
if (ini_get('open_basedir') == '' && ini_get('safe_mode' == 'Off')) {
curl_setopt($ci, CURLOPT_FOLLOWLOCATION, 1);
}
curl_setopt($ci, CURLOPT_MAXREDIRS, 2);/*指定最多的HTTP重定向的数量,这个选项是和CURLOPT_FOLLOWLOCATION一起使用的*/
curl_setopt($ci, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ci, CURLINFO_HEADER_OUT, true);
/*curl_setopt($ci, CURLOPT_COOKIE, $Cookiestr); * *COOKIE带过去** */
$response = curl_exec($ci);
$requestinfo = curl_getinfo($ci);
$http_code = curl_getinfo($ci, CURLINFO_HTTP_CODE);
/*if ($debug) {
echo "=====post data======\r\n";
var_dump($postfields);
echo "=====info===== \r\n";
print_r($requestinfo);
echo "=====response=====\r\n";
print_r($response);
}*/
curl_close($ci);
}
// 处理流式数据块
function processStreamData($data)
{
// 示例解析逻辑(根据豆包API实际协议调整)
$lines = explode("\n", $data);
foreach ($lines as $line) {
if (strpos($line, 'data: ') === 0) {
$json = substr($line, 6);
$response = json_decode($json, true);
// file_put_contents ( ROOT_PATH."/log.txt", date ( "Y-m-d H:i:s" ) . " " . var_export($response,true) . "\r\n", FILE_APPEND );
if (isset($response['choices'][0]['delta']['content'])) {
// file_put_contents ( ROOT_PATH."/log.txt", $response['choices'][0]['delta']['content'], FILE_APPEND );
// 实时输出到前端
$content = $response['choices'][0]['delta']['content'];
echo "data: " . json_encode(['content' => $content]) . "\n\n";
ob_flush();
flush();
}
}
}
}
?>
方案二:基于thinkphp框架
创建一个新的控制器Chat.php,代码如下:
<?php
namespace app\home\controller;
use think\Controller;
/**
* 插件的控制器
*/
class Chat extends Controller
{
/**
* 构造方法
*/
public function __construct()
{
parent::__construct();
function_exists('set_time_limit') && set_time_limit(0);
@ini_set('memory_limit','-1');
}
public function index()
{
return $this->fetch(':chat');
}
public function streamChat()
{
ob_implicit_flush(true); // 开启隐式刷新
// 设置响应头,启用流式输出
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('X-Accel-Buffering: no'); // 防止Nginx缓存
header('Connection: keep-alive');
ob_end_clean(); // 清除输出缓冲
// 获取用户输入
$input = input('param.text/s', '');
empty($input) && $input = '易优cms建站系统怎么样?';
// 调用豆包AI流式API
$this->callDoubaoStreamAPI($input);
exit;
}
// 调用豆包流式API
private function callDoubaoStreamAPI($prompt)
{
$apiUrl = "https://ark.cn-beijing.volces.com/api/v3/chat/completions"; // 替换为实际API地址
$apiKey = "*****************"; // 替换为实际API密钥
$postData = [
'model' => 'ep-20241224163854-tg2d8',
'messages' => [
[
'role' => 'user',
'content' => $prompt,
]
],
'stream' => true // 启用流式响应
];
$headers = [
'Content-Type: application/json',
'Authorization: Bearer ' . $apiKey
];
$this->httpRequest($apiUrl, 'POST', json_encode($postData, JSON_UNESCAPED_UNICODE), $headers, 300000);
}
private function httpRequest($url, $method = "GET", $postfields = null, $headers = array(), $timeout = 30, $debug = false)
{
$method = strtoupper($method);
$ci = curl_init();
/* Curl settings */
// curl_setopt($ci, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); // 使用哪个版本
curl_setopt($ci, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 6.2; WOW64; rv:34.0) Gecko/20100101 Firefox/34.0");
curl_setopt($ci, CURLOPT_CONNECTTIMEOUT, 60); /* 在发起连接前等待的时间,如果设置为0,则无限等待 */
// curl_setopt($ci, CURLOPT_TIMEOUT, 7); /* 设置cURL允许执行的最长秒数 */
curl_setopt($ci, CURLOPT_TIMEOUT, $timeout); /* 设置cURL允许执行的最长秒数 */
curl_setopt($ci, CURLOPT_RETURNTRANSFER, true);
switch ($method) {
case "POST":
curl_setopt($ci, CURLOPT_POST, true);
if (!empty($postfields)) {
if (is_string($postfields) && preg_match('/^([\w\-]+=[\w\-]+(&[\w\-]+=[\w\-]+)*)$/', $postfields)) {
parse_str($postfields, $output);
$postfields = $output;
}
if (is_array($postfields)) {
$tmpdatastr = http_build_query($postfields);
} else {
$tmpdatastr = $postfields;
}
curl_setopt($ci, CURLOPT_POSTFIELDS, $tmpdatastr);
}
break;
default:
curl_setopt($ci, CURLOPT_CUSTOMREQUEST, $method); /* //设置请求方式 */
break;
}
curl_setopt($ci, CURLOPT_WRITEFUNCTION, function($ci, $data){
// 处理流式数据(按豆包API实际格式解析)
$this->processStreamData($data);
return strlen($data); // 必须返回处理的数据长度
});
$ssl = preg_match('/^https:\/\//i', $url) ? TRUE : FALSE;
curl_setopt($ci, CURLOPT_URL, $url);
if ($ssl) {
curl_setopt($ci, CURLOPT_SSL_VERIFYPEER, FALSE); // https请求 不验证证书和hosts
curl_setopt($ci, CURLOPT_SSL_VERIFYHOST, FALSE); // 不从证书中检查SSL加密算法是否存在
// curl_setopt($ci, CURLOPT_SSLVERSION, 4); //因为之前的POODLE 病毒爆发,许多网站禁用了sslv3(nginx默认是禁用的,ssl_protocols 默认值为TLSv1 TLSv1.1 TLSv1.2;),最新使用sslv4
}
//curl_setopt($ci, CURLOPT_HEADER, true); /*启用时会将头文件的信息作为数据流输出*/
if (ini_get('open_basedir') == '' && ini_get('safe_mode' == 'Off')) {
curl_setopt($ci, CURLOPT_FOLLOWLOCATION, 1);
}
curl_setopt($ci, CURLOPT_MAXREDIRS, 2);/*指定最多的HTTP重定向的数量,这个选项是和CURLOPT_FOLLOWLOCATION一起使用的*/
curl_setopt($ci, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ci, CURLINFO_HEADER_OUT, true);
/*curl_setopt($ci, CURLOPT_COOKIE, $Cookiestr); * *COOKIE带过去** */
$response = curl_exec($ci);
$requestinfo = curl_getinfo($ci);
$http_code = curl_getinfo($ci, CURLINFO_HTTP_CODE);
/*if ($debug) {
echo "=====post data======\r\n";
var_dump($postfields);
echo "=====info===== \r\n";
print_r($requestinfo);
echo "=====response=====\r\n";
print_r($response);
}*/
curl_close($ci);
}
// 处理流式数据块
private function processStreamData($data)
{
// 示例解析逻辑(根据豆包API实际协议调整)
$lines = explode("\n", $data);
foreach ($lines as $line) {
if (strpos($line, 'data: ') === 0) {
$json = substr($line, 6);
$response = json_decode($json, true);
// file_put_contents ( ROOT_PATH."/log.txt", date ( "Y-m-d H:i:s" ) . " " . var_export($response,true) . "\r\n", FILE_APPEND );
if (isset($response['choices'][0]['delta']['content'])) {
// file_put_contents ( ROOT_PATH."/log.txt", $response['choices'][0]['delta']['content'], FILE_APPEND );
// 实时输出到前端
$content = $response['choices'][0]['delta']['content'];
echo "data: " . json_encode(['content' => $content]) . "\n\n";
ob_flush();
flush();
}
}
}
}
}
渲染模板文件chat.html,代码如下:
<!DOCTYPE html>
<html>
<head>
<title>豆包AI流式对话</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>
<div>
<input type="text" id="inputText" placeholder="输入你的问题">
<button onclick="startStream()">发送</button>
</div>
<div id="output" style="white-space: pre-wrap; margin-top: 20px;"></div>
<script>
let outputDiv = document.getElementById('output');
function startStream() {
const inputText = document.getElementById('inputText').value;
outputDiv.innerHTML = ''; // 清空之前的输出
const eventSource = new EventSource('http://域名/home/Chat/streamChat?text=' + encodeURIComponent(inputText));
eventSource.onmessage = function(e) {
try {
const data = JSON.parse(e.data);
console.log(data.content)
outputDiv.innerHTML += data.content; // 实时追加内容
} catch (err) {
console.error('解析错误:', err);
}
};
eventSource.onerror = function(err) {
console.error('EventSource 错误:', err);
eventSource.close();
};
}
</script>
</body>
</html>