You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实时返回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中调用会导致脚本提前结束,无法处理后续的流式chunk
  • CURLOPT_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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 11:22:52