feat: 新增 LLM AI 错误点评功能,支持通过 OpenAI 兼容 API 对提交代码进行智能分析

This commit is contained in:
2026-05-29 16:04:42 +08:00
parent 606e0855f9
commit b95ede457e
3 changed files with 296 additions and 0 deletions

View File

@@ -145,6 +145,15 @@ static $OJ_LOG_TRACE_ENABLED=false;
static $OJ_SaaS_ENABLE=false; static $OJ_SaaS_ENABLE=false;
static $OJ_MENU_NEWS=true; static $OJ_MENU_NEWS=true;
/* LLM AI Review - AI错误点评功能 */
static $OJ_LLM_ENABLED=false; // 总开关设为true开启AI点评
static $OJ_LLM_API_URL="https://api.openai.com/v1/chat/completions"; // API地址兼容OpenAI格式
static $OJ_LLM_API_KEY=""; // 填入你的API Key
static $OJ_LLM_MODEL="gpt-4o-mini"; // 模型名称
static $OJ_LLM_MAX_TOKENS=1024; // 最大输出token数
static $OJ_LLM_TIMEOUT=30; // 请求超时(秒)
static $OJ_LLM_SYSTEM_PROMPT=""; // 自定义系统prompt留空使用内置默认
require_once(dirname(__FILE__) . "/pdo.php"); require_once(dirname(__FILE__) . "/pdo.php");
require_once(dirname(__FILE__) . "/init.php"); require_once(dirname(__FILE__) . "/init.php");

212
web/llm-review.php Normal file
View File

@@ -0,0 +1,212 @@
<?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);

View File

@@ -177,5 +177,80 @@
<?php } ?> <?php } ?>
<?php if(isset($OJ_LLM_ENABLED) && $OJ_LLM_ENABLED){ ?>
<div style="margin-top: 20px; margin-bottom: 14px; ">
<div class="ui styled fluid accordion" id="llm-review-accordion">
<div class="title" style="font-size: 1.1em; font-weight: bold; ">
<i class="dropdown icon"></i> 🤖 AI 点评
</div>
<div class="content">
<div id="llm-review-area">
<button class="ui labeled icon blue button" id="llm-review-btn" onclick="fetchLLMReview()">
<i class="magic icon"></i> 获取 AI 点评
</button>
<div id="llm-review-loading" style="display:none; margin-top: 15px; ">
<div class="ui active centered inline text loader">AI 正在分析你的代码,请稍候...</div>
</div>
<div id="llm-review-result" style="display:none; margin-top: 15px; ">
<div class="ui existing segment" id="llm-review-content"></div>
</div>
<div id="llm-review-error" style="display:none; margin-top: 15px; ">
<div class="ui negative message">
<p id="llm-review-error-msg"></p>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
$(document).ready(function(){
$('#llm-review-accordion').accordion();
});
function fetchLLMReview(){
var btn = $('#llm-review-btn');
btn.prop('disabled', true).addClass('loading');
$('#llm-review-loading').show();
$('#llm-review-error').hide();
$('#llm-review-result').hide();
$.ajax({
type: 'GET',
url: 'llm-review.php?sid=<?php echo intval($id); ?>',
dataType: 'json',
timeout: 60000,
success: function(data){
btn.removeClass('loading');
$('#llm-review-loading').hide();
if(data.error){
$('#llm-review-error-msg').text(data.error);
$('#llm-review-error').show();
btn.prop('disabled', false);
} else if(data.review){
$('#llm-review-result').show();
var container = document.getElementById('llm-review-content');
// 先以纯文本放入,再用 Vditor 渲染为 Markdown HTML
container.textContent = data.review;
if(typeof HustOJVditor !== 'undefined' && HustOJVditor.renderMarkdownBlocks){
HustOJVditor.renderMarkdownBlocks('#llm-review-content', {
useTextContent: true
});
}
btn.hide();
}
},
error: function(xhr, status){
btn.removeClass('loading').prop('disabled', false);
$('#llm-review-loading').hide();
var msg = '请求失败';
if(status === 'timeout') msg = '请求超时,请稍后重试';
$('#llm-review-error-msg').text(msg);
$('#llm-review-error').show();
}
});
}
</script>
<?php } ?>
<?php include("template/$OJ_TEMPLATE/footer.php");?> <?php include("template/$OJ_TEMPLATE/footer.php");?>