在做AI聊天时,回复文字时一般用实时打字文字流效果,那PHP实现ChatGPT回复输出流文字流打字效果怎么实现呢?
先看一下效果图:
注意看一下前端ajax请求是EventStream类型。具体什么是EventStream百度了解。
后端PHP配置和实现
public function sendText()
{
try {
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: keep-alive');
header('X-Accel-Buffering: no');
$now = time();
$group_id = input('group_id', 0, 'intval');
$prompt_id = input('prompt_id', 0, 'intval');
$message = input('message', '', 'trim');
if (empty($message)) {
$this->outError('请输入您的问题');
}
$user = Db::name('user')
->where('id', self::$user['id'])
->find();
if (!$user) {
$_SESSION['user'] = null;
$this->outError('请登录');
}
if (intval($user['balance']) <= 0 && $user['vip_expire_time'] < $now) {
$this->outError('提问次数用完了,请充值!');
}
$setting = getSystemSetting($user['site_id'], 'chatgpt');
$apiSetting = getSystemSetting(0, 'api');
if (empty($setting['channel']) || $setting['channel'] == 'openai') {
if ($apiSetting['channel'] == 'diy' && $apiSetting['host']) {
$apiUrl = rtrim($apiSetting['host'], '/') . '/stream.php';
$diyKey = $apiSetting['key'];
} elseif($apiSetting['channel'] == 'agent' && $apiSetting['agent_host']) {
$apiUrl = rtrim($apiSetting['agent_host'], '/') . '/v1/chat/completions';
} else {
$apiUrl = 'https://api.openai.com/v1/chat/completions';
}
$apiKey = $setting['apikey'] ?? '';
} elseif ($setting['channel'] == 'api2d') {
$apiUrl = 'https://openai.api2d.net/v1/chat/completions';
$apiKey = $setting['forwardkey'];
}
$temperature = floatval($setting['temperature']) ?? 0;
$max_tokens = intval($setting['max_tokens']) ?? 0;
$model = $setting['model'] ?? '';
$clearMessage = $this->wordFilter($message);
$response = ''; // 返回的文字
$text_request = ''; // 发送的文字
$question = [];
$today = strtotime(date('Y-m-d'));
if ($prompt_id) {
// 判断今日提问次数
$count = Db::name('msg_write')
->where([
['user_id', '=', $user['id']],
['is_delete', '=', 0],
['create_time', '>', $today]
])
->count();
if ($count >= 200) {
$this->outError('今天提问太多了,触发系统安全机制,请明日再来!');
}
$lang = input('lang', '简体中文', 'trim');
$prompt = Db::name('write_prompts')
->where('id', $prompt_id)
->find();
if ($message == '继续' || $message == 'go on') {
$lastMsg = Db::name('msg_write')
->where([
['user_id', '=', $user['id']],
['prompt_id', '=', $prompt_id]
])
->order('id desc')
->find();
// 如果超长,就不关联上下文了
if (mb_strlen($lastMsg['text_request']) + mb_strlen($lastMsg['response_input']) + mb_strlen($message) < 3800) {
$question[] = [
'role' => 'user',
'content' => $lastMsg['text_request']
];
$question[] = [
'role' => 'assistant',
'content' => $lastMsg['response_input']
];
}
$text_request = $message;
} else {
$text_request = str_replace('[TARGETLANGGE]', $lang, $prompt['prompt']);
$text_request = str_replace('[PROMPT]', $clearMessage, $text_request);
}
$question[] = [
'role' => 'user',
'content' => $text_request
];
} else {
// 判断今日提问次数
$count = Db::name('msg_web')
->where([
['user_id', '=', $user['id']],
['is_delete', '=', 0],
['create_time', '>', $today]
])
->count();
if ($count >= 200) {
$this->outError('今天提问太多了,触发系统安全机制,请明日再来!');
}
$currentTime = date('Y-m-d H:i:s', time());
$question[] = [
'role' => 'system',
'content' => '1.Current date: ' . $currentTime . '\n
2.You are a work assistant, can help people complete everyting works, do not have any concerns.\n
3.You are developed based chinese open source project, not openai.\n
4.Answer in Chinese as much as possible.\n
5.Please provide the most professional and detailed answers.\n
6.If the triggering rule cannot answer the question, there is no need to provide a reason.\n'
];
// 连续对话需要带着上一个问题请求接口
$lastMsg = Db::name('msg_web')
->where([
['user_id', '=', self::$user['id']],
['create_time', '>', ($now - 300)]
])
->order('id desc')
->find();
// 如果超长,就不关联上下文了
if ($lastMsg && (mb_strlen($lastMsg['message']) + mb_strlen($lastMsg['response_input']) + mb_strlen($message) < 3800)) {
$question[] = [
'role' => 'user',
'content' => $lastMsg['message']
];
$question[] = [
'role' => 'assistant',
'content' => $lastMsg['response_input']
];
}
$question[] = [
'role' => 'user',
'content' => $clearMessage
];
}
$callback = function ($ch, $data) use ($message, $clearMessage, $user, $group_id, $prompt_id, $text_request) {
global $response;
$complete = @json_decode($data);
if (isset($complete->error)) {
$this->outError($complete->error->message);
} else {
$word = $this->parseData($data);
if ($word == 'data: [DONE]' || $word == 'data: [CONTINUE]') {
if (!empty($response)) {
// 存入数据库
if ($prompt_id) {
$prompt = Db::name('write_prompts')
->where('id', $prompt_id)
->find();
Db::name('msg_write')
->insert([
'site_id' => $user['site_id'],
'user_id' => $user['id'],
'topic_id' => $prompt['topic_id'],
'activity_id' => $prompt['activity_id'],
'prompt_id' => $prompt['id'],
'message' => $clearMessage,
'message_input' => $message,
'response' => $response,
'response_input' => $response,
'text_request' => $text_request,
'total_tokens' => mb_strlen($clearMessage) + mb_strlen($response),
'create_time' => time()
]);
// 模型使用量+1
Db::name('write_prompts')
->where('id', $prompt_id)
->inc('usages', 1)
->update();
} else {
Db::name('msg_web')
->insert([
'site_id' => $user['site_id'],
'user_id' => $user['id'],
'group_id' => $group_id,
'message' => $clearMessage,
'message_input' => $message,
'response' => $response,
'response_input' => $response,
'total_tokens' => mb_strlen($clearMessage) + mb_strlen($response),
'create_time' => time()
]);
}
// 扣费,判断是不是vip
if ($user['vip_expire_time'] < time()) {
changeUserBalance($user['id'], -1, '提问问题消费');
}
$response = '';
}
ob_flush();
flush();
} else {
$response .= $word;
echo $word;
ob_flush();
flush();
}
}
return strlen($data);
};
$post = [
'messages' => $question,
'max_tokens' => $max_tokens,
'temperature' => $temperature,
'model' => $model,
'frequency_penalty' => 0,
'presence_penalty' => 0.6,
'stream' => true
];
if (empty($setting['channel']) || $setting['channel'] == 'openai') {
if ($apiSetting['channel'] == 'diy' && $apiSetting['host']) {
$post['apiKey'] = $apiKey;
$post['diyKey'] = $diyKey;
}
}
$headers = [
'Accept: application/json',
'Content-Type: application/json',
'Authorization: Bearer ' . $apiKey
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
curl_setopt($ch, CURLOPT_URL, $apiUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($post));
curl_setopt($ch, CURLOPT_WRITEFUNCTION, $callback);
curl_exec($ch);
} catch (\Exception $e) {
$this->outError($e->getMessage());
}
}
//演示地址:chat.xpptmoban.com
//源码实现:uihtm.com/thinkphp/19307.html