百度智慧雲,流式請求示例

张志健發表於2024-05-23

前端

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Sample</title>
</head>
<body>
  <label for="textInput">Prompt:</label>
  <input type="textarea" id="textInput" placeholder="您有什麼問題">
  <button onclick="run_prompt()">執行prompt</button>
  <p><textarea id="answer" rows="10" cols="50" readonly></textarea></p>
<script>
  current_text = document.getElementById('answer');
  text = "";
  char_index = 0
  function run_prompt() {
    var inputValue = document.getElementById('textInput').value;
    document.getElementById('answer').value = "";
    // 呼叫服務端的流式介面, 修改為自己的伺服器地址和埠號
    fetch('http://<server address>:8000/eb_stream', {
      method: 'post',
      headers: {'Content-Type': 'text/plain'},
      body: JSON.stringify({'prompt': inputValue})
    })
    .then(response => {
      return response.body;
    })
    .then(body => {
      const reader = body.getReader();
      const decoder = new TextDecoder();
      function read() {
        return reader.read().then(({ done, value }) => {
          if (done) { // 讀取完成
            return;
          }
          data = decoder.decode(value, { stream: true });
          text += JSON.parse(data).result;
          type();  // 打字機效果輸出
          return read();
        });
      }
      return read();
    })
    .catch(error => {
      console.error('發生錯誤:', error);
    });
  }

  function type() {
    let enableCursor = true;  // 啟用游標效果
    if (char_index < text.length) {
      let txt = document.getElementById('answer').value;
      let cursor = enableCursor ? "|" : "";
      if (enableCursor && txt.endsWith("|")) {
        txt = txt.slice(0, -1);
      }
      document.getElementById('answer').value = txt + text.charAt(char_index) + cursor;
      char_index++;
      setTimeout(type, 1000/5);  // 打字機速度控制, 每秒5個字
    }
  }
</script>
</body>
</html>

後端

<?php
/*
啟動方式: php server.php
*/
// 執行埠號
$server_port = "0.0.0.0:8000";
// 服務無連線時的超時時間(s), 0表示永不超時
$server_timeout = 0;
// 千帆應用 AK/SK
$ak = "24.9d0562891315801c0f14cef19b39f7b0.2592000.1719019992.282335-73805369";
$sk = "";
// 鑑權介面URL
$token_url = 'https://aip.baidubce.com/oauth/2.0/token';
// 大模型介面URL
$eb_url = 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/eb-instant';
$source = "&sourceVer=0.0.1&source=app_center&appName=streamDemo";

// 建立 socket 服務
$server = stream_socket_server("tcp://$server_port", $errno, $errstr);
if (!$server) {
    die("Failed to create server: $errstr");
}

// 持續等待客戶端請求
$start_time = time();
while (true) {
    // 檢查是否有客戶端連線
    $read = array($server);
    $write = null;
    $except = null;
    if (stream_select($read, $write, $except, 0)) {
        $client = stream_socket_accept($server);
        // 獲取客戶端發來的資料
        $data = fread($client, 1024);
        $postData = '';
        if (preg_match("/\r\n\r\n(.*)/s", $data, $match)) {
            $postData = $match[1];
        }
        // 解析 POST 引數, 請求大模型服務
        $prompt = json_decode($postData);
        $responseBody = get_info_from_llm($prompt->prompt);

        // 返回響應頭
        $responseHeaders = [
            'Content-Type' => 'text/plain',
            'Access-Control-Allow-Origin' => '*',
        ];
        fwrite($client, "HTTP/1.1 200 OK\r\n");
        foreach ($responseHeaders as $header => $value) {
            fwrite($client, "$header: $value\r\n");
        }
        fwrite($client, "\r\n");

        // 返回響應體
        foreach ($responseBody as $chunk) {
            fwrite($client, "$chunk");
            flush();

            // 防止偶發多條合併返回
            usleep(100000);
        }
        fclose($client);
    } else {
        // 0 表示永不超時
        if ($server_timeout == 0) {
            continue;
        }

        // 超時則退出
        if (time() - $start_time >= $server_timeout) {
            echo "Timeout reached. Stopping server.
";
            break;
        }

        // 否則繼續等待
        continue;
    }
}
// 釋放資源
fclose($server);

// 請求大模型服務, 並將大模型的返回資料逐行返回
function get_info_from_llm($prompt){
    // 全域性變數
    global $ak, $sk, $token_url, $eb_url;

    // 獲取 access_token
    $curl = curl_init();
    $postData = array(
        'grant_type' => 'client_credentials',
        'client_id' => $ak,
        'client_secret' => $sk
    );
    curl_setopt_array($curl, array(
        CURLOPT_URL => $token_url,
        CURLOPT_CUSTOMREQUEST => 'POST',
        CURLOPT_SSL_VERIFYPEER  => false,
        CURLOPT_SSL_VERIFYHOST  => false,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POSTFIELDS => http_build_query($postData)
    ));
    $token_rtn = curl_exec($curl);
    curl_close($curl);

    // 請求大模型服務, 並將大模型的返回資料逐行返回給客戶端
    $body = ["messages" => [["role" => "user", "content" => $prompt]],"stream" => true];
    $url = $eb_url . '?access_token=' . json_decode($token_rtn)->access_token . $source;
    $options = [
        'http' => [
            'method' => 'POST',
            'header' => 'Content-Type: text/plain',
            'content' => json_encode($body),
            'ignore_errors' => true,
            'protocol_version' => 1.1,
            'timeout' => 60
        ]
    ];
    $context = stream_context_create($options);
    $stream = fopen($url, 'r', false, $context);
    stream_set_blocking($stream, 0);
    if ($stream) {
        $msg = "";
        // 判斷資料流是否已讀取完
        while (!feof($stream)) {
            // 逐行讀取
            $lines = explode("\n", fgets($stream));
            foreach ($lines as $line) {
                // 去掉空行
                if ($line != "") {
                    echo "正在處理的行資料: $line\n";
                    $msgPart = explode(":", $line, 2);
                    if ($msgPart[0] == "data") {
                        if ($msg != "") {
                            yield $msg;
                        }
                        $msg = $msgPart[1];
                    } else {
                        $msg = $msg . $line;
                    }
                }
            }
        }
        // 最後一次msg不需要yield,原因是最後一次請求大模型介面返回的result是空,只是使用is_end=true標記請求結束,忽略即可
    }
}
?>

相關文章