如何实时返回GPT-3.5-turbo部分响应至Ajax?PHP代码排查
你的API调用与实时响应实现的核心问题
1. OpenAI API端点错误
gpt-3.5-turbo属于聊天模型,必须使用https://api.openai.com/v1/chat/completions端点,你当前用的/v1/completions是针对文本补全模型(如text-davinci-003)的,两者接口格式完全不兼容。
2. 流式响应解析逻辑错误
OpenAI的流式响应格式是每行以data: 开头,包含单条增量数据,最后以data: [DONE]结束,你的代码存在两处关键错误:
- 直接将整个chunk解析为JSON,忽略了流式响应的行结构,会导致解析失败
- 流式返回的是增量内容,字段为
choices[0].delta.content,而非完整的choices->messages
3. PHP端流式输出的实现错误
wp_send_json()会直接输出JSON并终止脚本,在CURLOPT_WRITEFUNCTION中调用会导致脚本提前结束,无法处理后续的流式chunkCURLOPT_RETURNTRANSFER设为1时,curl会将响应内容返回给变量,但流式场景下应该关闭这个选项,让响应直接输出到客户端- 流式模式下,
curl_exec()不会返回完整的JSON结果,后续的json_decode($result)完全无效
4. 信用扣除逻辑缺失
当前信用更新代码放在非流式的分支中,流式模式下永远不会执行,导致用户提问后信用不会被扣除。
5. 前端AJAX不支持流式接收
你使用的jQuery.ajax默认会等待整个响应完成才触发success回调,无法实时接收流式返回的部分内容,需要改用原生XHR的onprogress事件或Fetch API的ReadableStream来处理实时数据。
关键部分修正示例
PHP端核心修正
function ask_question() { $query = $_POST['query']; $query_credit = $_POST['queryCredit']; $user_id = get_current_user_id(); $meta_key = 'ai_anna_credit'; $user_credit = get_user_meta($user_id, $meta_key, true); if ($user_credit <= 0) { header('Content-Type: text/event-stream'); header('Cache-Control: no-cache'); header('Connection: keep-alive'); echo "data: " . json_encode(array('type' => 'error', 'message' => 'You do not have enough credit for this aiAnna question.')) . "\n\n"; ob_flush(); flush(); exit; } $ch = curl_init(); // 修正API端点 $url = 'https://api.openai.com/v1/chat/completions'; $api_key = 'sk-***********************'; $post_fields = array( 'model' => 'gpt-3.5-turbo', 'messages' => array( array('role' => 'system', 'content' => 'You are aiAnna, CSUKs helpful virtual teacher\'s assistant!'), array('role' => 'user', 'content' => $query) ), 'stream' => true ); $header = array( 'Content-Type: application/json', 'Authorization: Bearer ' . $api_key ); curl_setopt($ch, CURLOPT_URL, $url); // 关闭RETURNTRANSFER,让响应直接输出 curl_setopt($ch, CURLOPT_RETURNTRANSFER, 0); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($post_fields)); curl_setopt($ch, CURLOPT_HTTPHEADER, $header); // 设置SSE响应头 header('Content-Type: text/event-stream'); header('Cache-Control: no-cache'); header('Connection: keep-alive'); // 处理流式响应的回调 curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $chunk) use ($user_id, $meta_key, $user_credit, $query_credit) { static $credit_deducted = false; // 按行分割chunk $lines = explode("\n", $chunk); foreach ($lines as $line) { $line = trim($line); if (empty($line) || $line === 'data: [DONE]') { continue; } // 去掉data:前缀 if (str_starts_with($line, 'data: ')) { $json_data = substr($line, 6); $response_data = json_decode($json_data); if (isset($response_data->choices[0]->delta->content)) { $content = $response_data->choices[0]->delta->content; // 以SSE格式输出内容 echo "data: " . json_encode(array('type' => 'content', 'content' => $content)) . "\n\n"; // 刷新输出缓冲区 ob_flush(); flush(); } } } // 首次处理时扣除信用(避免重复扣除) if (!$credit_deducted) { $new_credit = $user_credit - $query_credit; update_user_meta($user_id, $meta_key, $new_credit); $credit_deducted = true; // 输出信用更新 echo "data: " . json_encode(array('type' => 'credit', 'credit' => $new_credit)) . "\n\n"; ob_flush(); flush(); } return strlen($chunk); }); curl_exec($ch); if (curl_errno($ch)) { echo "data: " . json_encode(array('type' => 'error', 'message' => 'Error: ' . curl_error($ch))) . "\n\n"; ob_flush(); flush(); } curl_close($ch); // 终止脚本,避免后续输出 exit; }
前端核心修正(使用原生XHR处理流式)
function askQuestion(query, queryCredit) { const answerOutput = document.getElementById("answerBoxDiv"); const creditOutput = document.getElementById("creditMessage"); const aiThinkElement = document.getElementById("ai_think_id"); aiThinkElement.style.display = "block"; answerOutput.innerText = ""; const xhr = new XMLHttpRequest(); xhr.open("POST", "'.$adminAjaxUrl.'", true); xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); // 处理流式响应 xhr.onprogress = function(event) { if (event.lengthComputable) return; // 获取最新的响应文本片段 const responseChunk = xhr.responseText.substring(xhr.lastResponseLength || 0); xhr.lastResponseLength = xhr.responseText.length; // 按SSE格式分割 const messages = responseChunk.split("\n\n"); messages.forEach(msg => { if (!msg.startsWith("data: ")) return; try { const data = JSON.parse(msg.slice(6)); if (data.type === 'content') { answerOutput.innerText += data.content; // 处理代码块格式 const pattern = /```([\s\S]*?)```/g; answerOutput.innerHTML = answerOutput.innerHTML.replace(pattern, "<code>$1</code>"); } else if (data.type === 'credit') { creditOutput.innerHTML = "Current aiAnna Credit: " + data.credit; } else if (data.type === 'error') { answerOutput.innerText = data.message; aiThinkElement.style.display = "none"; } } catch(e) { // 忽略解析错误的片段 } }); }; xhr.onload = function() { aiThinkElement.style.display = "none"; }; xhr.onerror = function() { answerOutput.innerText = "Oh no, aiAnna was not able to fetch a response. Do not worry though as your credit has not been reduced! Please try again!"; aiThinkElement.style.display = "none"; }; // 发送数据 const formData = new URLSearchParams(); formData.append("action", "ask_question"); formData.append("query", query); formData.append("queryCredit", queryCredit); xhr.send(formData); }
内容的提问来源于stack exchange,提问作者sw123456
相关产品推荐
相关产品推荐

