php+nginx+豆包(deepseek、kimi等AI)实现流式输出的对话效果

下面为你详细介绍如何使用 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>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值