213 lines
7.9 KiB
PHP
213 lines
7.9 KiB
PHP
<?php
|
||
/**
|
||
* LLM AI Review - AI错误点评
|
||
* 接收 solution_id,收集上下文信息,调用LLM API返回启发式点评
|
||
* 结果缓存到 llm_review 表,同一 solution_id 只调用一次 LLM
|
||
*/
|
||
header("Content-Type: application/json; charset=utf-8");
|
||
header("Cache-Control: no-cache, must-revalidate");
|
||
|
||
require_once('./include/db_info.inc.php');
|
||
require_once('./include/setlang.php');
|
||
require_once('./include/const.inc.php');
|
||
|
||
// 检查功能是否开启
|
||
if (!isset($OJ_LLM_ENABLED) || !$OJ_LLM_ENABLED) {
|
||
echo json_encode(["error" => "AI点评功能未开启"], JSON_UNESCAPED_UNICODE);
|
||
exit;
|
||
}
|
||
|
||
// 检查登录
|
||
if (!isset($_SESSION[$OJ_NAME . '_user_id'])) {
|
||
echo json_encode(["error" => "请先登录"], JSON_UNESCAPED_UNICODE);
|
||
exit;
|
||
}
|
||
|
||
// 获取 solution_id
|
||
if (!isset($_GET['sid'])) {
|
||
echo json_encode(["error" => "缺少参数 sid"], JSON_UNESCAPED_UNICODE);
|
||
exit;
|
||
}
|
||
$sid = intval($_GET['sid']);
|
||
if ($sid <= 0) {
|
||
echo json_encode(["error" => "无效的 solution_id"], JSON_UNESCAPED_UNICODE);
|
||
exit;
|
||
}
|
||
|
||
// ---- 1. 检查缓存 ----
|
||
$cache = pdo_query("SELECT `review` FROM `llm_review` WHERE `solution_id`=?", $sid);
|
||
if ($cache !== -1 && !empty($cache)) {
|
||
echo json_encode(["review" => $cache[0]['review']], JSON_UNESCAPED_UNICODE);
|
||
exit;
|
||
}
|
||
|
||
// ---- 2. 查询 solution 信息 ----
|
||
$solution = pdo_query("SELECT * FROM `solution` WHERE `solution_id`=?", $sid);
|
||
if (empty($solution)) {
|
||
echo json_encode(["error" => "找不到该提交记录"], JSON_UNESCAPED_UNICODE);
|
||
exit;
|
||
}
|
||
$sol = $solution[0];
|
||
$problem_id = intval($sol['problem_id']);
|
||
$language = intval($sol['language']);
|
||
$result_code = intval($sol['result']);
|
||
$pass_rate = floatval($sol['pass_rate']) * 100;
|
||
$lang_name = isset($language_name[$language]) ? $language_name[$language] : "unknown";
|
||
|
||
// ---- 3. 查询学生源码 ----
|
||
$src = pdo_query("SELECT `source` FROM `source_code_user` WHERE `solution_id`=?", $sid);
|
||
$student_code = (!empty($src) && $src !== -1) ? $src[0]['source'] : "";
|
||
|
||
// ---- 4. 查询题目信息 ----
|
||
$prob = pdo_query("SELECT `title`,`description`,`input`,`output`,`sample_input`,`sample_output`,`hint` FROM `problem` WHERE `problem_id`=?", $problem_id);
|
||
if (empty($prob) || $prob === -1) {
|
||
echo json_encode(["error" => "找不到该题目"], JSON_UNESCAPED_UNICODE);
|
||
exit;
|
||
}
|
||
$p = $prob[0];
|
||
|
||
// ---- 5. 查询第一个 AC 代码(同语言) ----
|
||
$first_ac = pdo_query(
|
||
"SELECT `solution_id` FROM `solution` WHERE `problem_id`=? AND `result`=4 AND `language`=? ORDER BY `in_date` ASC LIMIT 1",
|
||
$problem_id, $language
|
||
);
|
||
$reference_code = "";
|
||
if (!empty($first_ac) && $first_ac !== -1) {
|
||
$ref_src = pdo_query("SELECT `source` FROM `source_code_user` WHERE `solution_id`=?", intval($first_ac[0]['solution_id']));
|
||
if (!empty($ref_src) && $ref_src !== -1) {
|
||
$reference_code = $ref_src[0]['source'];
|
||
}
|
||
}
|
||
|
||
// ---- 6. 查询 diff/error 信息 ----
|
||
$diff_info = "";
|
||
if ($result_code == 11) {
|
||
// 编译错误
|
||
$err = pdo_query("SELECT `error` FROM `compileinfo` WHERE `solution_id`=?", $sid);
|
||
if (!empty($err) && $err !== -1) {
|
||
$diff_info = $err[0]['error'];
|
||
}
|
||
} else {
|
||
// 运行时/WA 等
|
||
$err = pdo_query("SELECT `error` FROM `runtimeinfo` WHERE `solution_id`=?", $sid);
|
||
if (!empty($err) && $err !== -1) {
|
||
$diff_info = $err[0]['error'];
|
||
}
|
||
}
|
||
|
||
// ---- 7. 组装 Prompt ----
|
||
$system_prompt = isset($OJ_LLM_SYSTEM_PROMPT) && $OJ_LLM_SYSTEM_PROMPT !== ""
|
||
? $OJ_LLM_SYSTEM_PROMPT
|
||
: <<<EOP
|
||
你是一位经验丰富的编程辅导老师,正在帮助一名正在学习C++的初中学生。
|
||
|
||
你的职责:
|
||
1. 用亲切、鼓励的语气和学生交流
|
||
2. 仔细分析学生的代码,找出错误所在并解释错误原因
|
||
3. 给出思考方向和启发性提示,引导学生自己去发现和改正错误
|
||
4. 绝对不要直接给出正确代码、完整解法或关键算法步骤
|
||
5. 不要在回复中透露、引用、复述或暗示参考代码的任何内容
|
||
6. 如果学生代码思路正确但有小bug,肯定其思路并指出具体的bug位置
|
||
7. 如果是格式错误(PE),提醒学生注意输出格式要求
|
||
8. 如果是超时(TLE),引导学生思考算法的时间复杂度
|
||
9. 如果是运行时错误(RE),帮助学生分析可能的数组越界、除零等问题
|
||
10. 如果是答案错误(WA),可以建议学生用样例手动模拟执行过程
|
||
11. 使用中文回答,适当使用Markdown格式使内容清晰易读
|
||
EOP;
|
||
|
||
$user_prompt = "## 题目信息\n";
|
||
$user_prompt .= "- 标题: " . $p['title'] . "\n";
|
||
$user_prompt .= "- 题目描述:\n" . $p['description'] . "\n";
|
||
if (!empty($p['input'])) $user_prompt .= "- 输入格式:\n" . $p['input'] . "\n";
|
||
if (!empty($p['output'])) $user_prompt .= "- 输出格式:\n" . $p['output'] . "\n";
|
||
if (!empty($p['sample_input'])) $user_prompt .= "- 样例输入:\n```\n" . $p['sample_input'] . "\n```\n";
|
||
if (!empty($p['sample_output'])) $user_prompt .= "- 样例输出:\n```\n" . $p['sample_output'] . "\n```\n";
|
||
if (!empty($p['hint'])) $user_prompt .= "- 提示:\n" . $p['hint'] . "\n";
|
||
|
||
$user_prompt .= "\n## 判题结果\n";
|
||
$user_prompt .= $judge_result[$result_code] . "(通过率: " . round($pass_rate, 1) . "%)\n";
|
||
|
||
if (!empty($diff_info)) {
|
||
$user_prompt .= "\n## 错误/对比信息\n```\n" . $diff_info . "\n```\n";
|
||
}
|
||
|
||
$user_prompt .= "\n## 学生提交的代码\n```" . strtolower($lang_name) . "\n" . $student_code . "\n```\n";
|
||
|
||
if (!empty($reference_code)) {
|
||
$user_prompt .= "\n## 参考代码(仅供你分析对比,绝不可在回复中透露或暗示其内容)\n```" . strtolower($lang_name) . "\n" . $reference_code . "\n```\n";
|
||
}
|
||
|
||
$user_prompt .= "\n请分析学生的错误,给出启发性的提示和引导,帮助学生自己发现问题并改正。\n";
|
||
|
||
// ---- 8. 调用 LLM API ----
|
||
$api_url = isset($OJ_LLM_API_URL) ? $OJ_LLM_API_URL : "https://api.openai.com/v1/chat/completions";
|
||
$api_key = isset($OJ_LLM_API_KEY) ? $OJ_LLM_API_KEY : "";
|
||
$model = isset($OJ_LLM_MODEL) ? $OJ_LLM_MODEL : "gpt-4o-mini";
|
||
$max_tokens = isset($OJ_LLM_MAX_TOKENS) ? intval($OJ_LLM_MAX_TOKENS) : 1024;
|
||
$timeout = isset($OJ_LLM_TIMEOUT) ? intval($OJ_LLM_TIMEOUT) : 30;
|
||
|
||
if (empty($api_key)) {
|
||
echo json_encode(["error" => "API Key未配置"], JSON_UNESCAPED_UNICODE);
|
||
exit;
|
||
}
|
||
|
||
$request_body = json_encode([
|
||
"model" => $model,
|
||
"messages" => [
|
||
["role" => "system", "content" => $system_prompt],
|
||
["role" => "user", "content" => $user_prompt]
|
||
],
|
||
"max_tokens" => $max_tokens,
|
||
"temperature" => 0.7
|
||
]);
|
||
|
||
$ch = curl_init($api_url);
|
||
curl_setopt_array($ch, [
|
||
CURLOPT_POST => true,
|
||
CURLOPT_POSTFIELDS => $request_body,
|
||
CURLOPT_HTTPHEADER => [
|
||
"Content-Type: application/json",
|
||
"Authorization: Bearer " . $api_key
|
||
],
|
||
CURLOPT_RETURNTRANSFER => true,
|
||
CURLOPT_TIMEOUT => $timeout,
|
||
CURLOPT_SSL_VERIFYPEER => true
|
||
]);
|
||
|
||
$response = curl_exec($ch);
|
||
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||
$curl_error = curl_error($ch);
|
||
curl_close($ch);
|
||
|
||
if ($response === false) {
|
||
echo json_encode(["error" => "网络请求失败: " . $curl_error], JSON_UNESCAPED_UNICODE);
|
||
exit;
|
||
}
|
||
|
||
if ($http_code !== 200) {
|
||
echo json_encode(["error" => "API请求失败 (HTTP $http_code)"], JSON_UNESCAPED_UNICODE);
|
||
exit;
|
||
}
|
||
|
||
$resp_data = json_decode($response, true);
|
||
if (!$resp_data || !isset($resp_data['choices'][0]['message']['content'])) {
|
||
echo json_encode(["error" => "API返回格式异常"], JSON_UNESCAPED_UNICODE);
|
||
exit;
|
||
}
|
||
|
||
$review_text = $resp_data['choices'][0]['message']['content'];
|
||
|
||
// ---- 9. 缓存结果 ----
|
||
// 自动建表(首次调用时)
|
||
pdo_query("CREATE TABLE IF NOT EXISTS `llm_review` (
|
||
`solution_id` INT NOT NULL,
|
||
`review` TEXT NOT NULL,
|
||
`create_time` DATETIME NOT NULL,
|
||
PRIMARY KEY (`solution_id`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
|
||
|
||
pdo_query("INSERT INTO `llm_review` (`solution_id`, `review`, `create_time`) VALUES (?, ?, NOW())", $sid, $review_text);
|
||
|
||
// ---- 10. 返回结果 ----
|
||
echo json_encode(["review" => $review_text], JSON_UNESCAPED_UNICODE);
|