优化AI评分系统和答题体验
后端改进: - 简答题AI评分改为必须成功,失败则返回错误提示 - 实现AI评分重试机制,最多重试5次,采用指数退避策略 - AI评分温度设置为0,确保评分结果更加一致和可预测 - 优化AI评分提示词,要求严格按照标准答案评分 - 添加详细的评分标准(85-100分/60-84分/40-59分/0-39分) - 强化系统消息,要求评分客观、一致、可预测 前端改进: - 添加自动下一题功能,答对后自动跳转(默认开启) - 支持配置自动跳转延迟时间(1-10秒,默认2秒) - 使用Popover组件优化设置UI,保持界面简洁 - 设置保存到localStorage,支持跨会话持久化 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
24d098ae92
commit
2e526425a0
@ -175,30 +175,62 @@ func SubmitPracticeAnswer(c *gin.Context) {
|
||||
// AI评分结果(仅简答题)
|
||||
var aiGrading *models.AIGrading = nil
|
||||
|
||||
// 对简答题使用AI评分
|
||||
// 对简答题使用AI评分(必须成功,失败重试最多5次)
|
||||
if question.Type == "short-answer" {
|
||||
// 获取用户答案字符串
|
||||
userAnswerStr, ok := submit.Answer.(string)
|
||||
if ok {
|
||||
// 获取标准答案字符串
|
||||
standardAnswerStr, ok := correctAnswer.(string)
|
||||
if ok {
|
||||
// 调用AI评分服务
|
||||
aiService := services.NewAIGradingService()
|
||||
aiResult, err := aiService.GradeShortAnswer(question.Question, standardAnswerStr, userAnswerStr)
|
||||
if err != nil {
|
||||
// AI评分失败时记录日志,但不影响主流程
|
||||
log.Printf("AI评分失败: %v", err)
|
||||
} else {
|
||||
// 使用AI的评分结果
|
||||
correct = aiResult.IsCorrect
|
||||
aiGrading = &models.AIGrading{
|
||||
Score: aiResult.Score,
|
||||
Feedback: aiResult.Feedback,
|
||||
Suggestion: aiResult.Suggestion,
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "简答题答案格式错误",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取标准答案字符串
|
||||
standardAnswerStr, ok := correctAnswer.(string)
|
||||
if !ok {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"success": false,
|
||||
"message": "题目答案格式错误",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 调用AI评分服务(带重试机制)
|
||||
aiService := services.NewAIGradingService()
|
||||
var aiResult *services.AIGradingResult
|
||||
var err error
|
||||
maxRetries := 5
|
||||
|
||||
for attempt := 1; attempt <= maxRetries; attempt++ {
|
||||
log.Printf("AI评分尝试第 %d 次 (题目ID: %d)", attempt, question.ID)
|
||||
aiResult, err = aiService.GradeShortAnswer(question.Question, standardAnswerStr, userAnswerStr)
|
||||
if err == nil {
|
||||
log.Printf("AI评分成功 (题目ID: %d, 得分: %.1f)", question.ID, aiResult.Score)
|
||||
break
|
||||
}
|
||||
log.Printf("AI评分失败 (第 %d 次尝试): %v", attempt, err)
|
||||
if attempt < maxRetries {
|
||||
// 等待一小段时间后重试(指数退避)
|
||||
time.Sleep(time.Second * time.Duration(attempt))
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"success": false,
|
||||
"message": fmt.Sprintf("AI评分服务暂时不可用,已重试%d次,请稍后再试", maxRetries),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 使用AI的评分结果
|
||||
correct = aiResult.IsCorrect
|
||||
aiGrading = &models.AIGrading{
|
||||
Score: aiResult.Score,
|
||||
Feedback: aiResult.Feedback,
|
||||
Suggestion: aiResult.Suggestion,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -47,8 +47,8 @@ type AIGradingResult struct {
|
||||
// standardAnswer: 标准答案
|
||||
// userAnswer: 用户答案
|
||||
func (s *AIGradingService) GradeShortAnswer(question, standardAnswer, userAnswer string) (*AIGradingResult, error) {
|
||||
// 构建评分提示词
|
||||
prompt := fmt.Sprintf(`你是一位专业的阅卷老师,请对以下简答题进行评分。
|
||||
// 构建评分提示词(严格评分模式)
|
||||
prompt := fmt.Sprintf(`你是一位严格的阅卷老师,请严格按照标准答案对以下简答题进行评分。
|
||||
|
||||
题目:%s
|
||||
|
||||
@ -56,21 +56,29 @@ func (s *AIGradingService) GradeShortAnswer(question, standardAnswer, userAnswer
|
||||
|
||||
学生答案:%s
|
||||
|
||||
请按照以下要求进行评分:
|
||||
1. 给出一个0-100的分数
|
||||
2. 判断答案是否正确(60分及以上为正确)
|
||||
3. 给出简短的评语(不超过50字)
|
||||
4. 如果答案不完善,给出改进建议(不超过50字,如果答案很好可以为空)
|
||||
评分标准(请严格遵守):
|
||||
1. 必须与标准答案进行逐项对比
|
||||
2. 答案要点完全覆盖标准答案且表述准确的,给85-100分
|
||||
3. 答案要点基本覆盖但有缺漏或表述不够准确的,给60-84分
|
||||
4. 答案要点缺失较多或有明显错误的,给40-59分
|
||||
5. 答案完全错误或离题的,给0-39分
|
||||
6. 判断标准:60分及以上为正确(is_correct: true),否则为错误(is_correct: false)
|
||||
|
||||
评分要求:
|
||||
1. 给出一个0-100的精确分数
|
||||
2. 判断答案是否正确(is_correct: 60分及以上为true,否则为false)
|
||||
3. 给出简短的评语(说明得分和失分原因,不超过50字)
|
||||
4. 给出具体的改进建议(如果答案满分可以为空,否则必须指出具体改进方向,不超过50字)
|
||||
|
||||
请按照以下JSON格式返回结果:
|
||||
{
|
||||
"score": 85,
|
||||
"is_correct": true,
|
||||
"feedback": "答案基本正确,要点全面",
|
||||
"suggestion": "可以补充一些具体的例子"
|
||||
"feedback": "答案覆盖了主要要点,但XXX部分描述不够准确",
|
||||
"suggestion": "建议补充XXX内容,并完善XXX的描述"
|
||||
}
|
||||
|
||||
注意:只返回JSON格式的结果,不要有其他内容。`, question, standardAnswer, userAnswer)
|
||||
注意:只返回JSON格式的结果,不要有其他内容。必须严格对照标准答案评分,不要过于宽松。`, question, standardAnswer, userAnswer)
|
||||
|
||||
// 调用AI API
|
||||
resp, err := s.client.CreateChatCompletion(
|
||||
@ -80,14 +88,14 @@ func (s *AIGradingService) GradeShortAnswer(question, standardAnswer, userAnswer
|
||||
Messages: []openai.ChatCompletionMessage{
|
||||
{
|
||||
Role: openai.ChatMessageRoleSystem,
|
||||
Content: "你是一位专业的阅卷老师,擅长对简答题进行公正、客观的评分。",
|
||||
Content: "你是一位严格的阅卷老师,必须严格按照标准答案进行评分,不能过于宽松。你的评分标准是客观的、一致的、可预测的。",
|
||||
},
|
||||
{
|
||||
Role: openai.ChatMessageRoleUser,
|
||||
Content: prompt,
|
||||
},
|
||||
},
|
||||
Temperature: 0.3, // 较低的温度以获得更稳定的评分结果
|
||||
Temperature: 0, // 温度为0,获得最确定、最一致的评分结果
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useSearchParams, useNavigate } from "react-router-dom";
|
||||
import { Button, message, Typography } from "antd";
|
||||
import { ArrowLeftOutlined } from "@ant-design/icons";
|
||||
import { Button, message, Typography, Switch, InputNumber, Popover, Space } from "antd";
|
||||
import { ArrowLeftOutlined, SettingOutlined } from "@ant-design/icons";
|
||||
import type { Question, AnswerResult } from "../types/question";
|
||||
import * as questionApi from "../api/question";
|
||||
import QuestionProgress from "../components/QuestionProgress";
|
||||
@ -28,6 +28,33 @@ const QuestionPage: React.FC = () => {
|
||||
const [wrongCount, setWrongCount] = useState(0);
|
||||
const [showSummary, setShowSummary] = useState(false);
|
||||
|
||||
// 自动跳转开关(默认开启)
|
||||
const [autoNext, setAutoNext] = useState(() => {
|
||||
const saved = localStorage.getItem('autoNextEnabled');
|
||||
return saved !== null ? saved === 'true' : true;
|
||||
});
|
||||
|
||||
// 自动跳转延迟时间(秒,默认2秒)
|
||||
const [autoNextDelay, setAutoNextDelay] = useState(() => {
|
||||
const saved = localStorage.getItem('autoNextDelay');
|
||||
return saved !== null ? parseInt(saved, 10) : 2;
|
||||
});
|
||||
|
||||
// 切换自动跳转开关
|
||||
const toggleAutoNext = () => {
|
||||
const newValue = !autoNext;
|
||||
setAutoNext(newValue);
|
||||
localStorage.setItem('autoNextEnabled', String(newValue));
|
||||
};
|
||||
|
||||
// 修改自动跳转延迟时间
|
||||
const handleDelayChange = (value: number | null) => {
|
||||
if (value !== null && value >= 1 && value <= 10) {
|
||||
setAutoNextDelay(value);
|
||||
localStorage.setItem('autoNextDelay', String(value));
|
||||
}
|
||||
};
|
||||
|
||||
// 从localStorage恢复答题进度
|
||||
const getStorageKey = () => {
|
||||
const type = searchParams.get("type");
|
||||
@ -180,13 +207,13 @@ const QuestionPage: React.FC = () => {
|
||||
saveProgress(currentIndex, correctCount, newWrong);
|
||||
}
|
||||
|
||||
// 如果答案正确,1秒后自动进入下一题
|
||||
if (res.data.correct) {
|
||||
// 如果答案正确且开启自动跳转,根据设置的延迟时间后自动进入下一题
|
||||
if (res.data.correct && autoNext) {
|
||||
setAutoNextLoading(true);
|
||||
setTimeout(() => {
|
||||
setAutoNextLoading(false);
|
||||
handleNext();
|
||||
}, 1000);
|
||||
}, autoNextDelay * 1000); // 将秒转换为毫秒
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@ -279,6 +306,53 @@ const QuestionPage: React.FC = () => {
|
||||
<span className={styles.statLabel}>错误</span>
|
||||
<span className={styles.statValue} style={{ color: '#ff4d4f' }}>{wrongCount}</span>
|
||||
</div>
|
||||
{/* 设置按钮 */}
|
||||
<Popover
|
||||
content={
|
||||
<div style={{ width: 200 }}>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<div>
|
||||
<div style={{ marginBottom: 8, fontWeight: 500 }}>自动下一题</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={autoNext}
|
||||
onChange={toggleAutoNext}
|
||||
size="small"
|
||||
/>
|
||||
<span style={{ fontSize: 14 }}>
|
||||
{autoNext ? '已开启' : '已关闭'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{autoNext && (
|
||||
<div>
|
||||
<div style={{ marginBottom: 8, fontWeight: 500 }}>延迟时间</div>
|
||||
<Space>
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={10}
|
||||
value={autoNextDelay}
|
||||
onChange={handleDelayChange}
|
||||
size="small"
|
||||
style={{ width: 60 }}
|
||||
/>
|
||||
<span style={{ fontSize: 14 }}>秒后跳转</span>
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
}
|
||||
title="答题设置"
|
||||
trigger="click"
|
||||
placement="bottomRight"
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<SettingOutlined />}
|
||||
style={{ marginLeft: 8 }}
|
||||
/>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user