优化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评分结果(仅简答题)
|
// AI评分结果(仅简答题)
|
||||||
var aiGrading *models.AIGrading = nil
|
var aiGrading *models.AIGrading = nil
|
||||||
|
|
||||||
// 对简答题使用AI评分
|
// 对简答题使用AI评分(必须成功,失败重试最多5次)
|
||||||
if question.Type == "short-answer" {
|
if question.Type == "short-answer" {
|
||||||
// 获取用户答案字符串
|
// 获取用户答案字符串
|
||||||
userAnswerStr, ok := submit.Answer.(string)
|
userAnswerStr, ok := submit.Answer.(string)
|
||||||
if ok {
|
if !ok {
|
||||||
// 获取标准答案字符串
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
standardAnswerStr, ok := correctAnswer.(string)
|
"success": false,
|
||||||
if ok {
|
"message": "简答题答案格式错误",
|
||||||
// 调用AI评分服务
|
})
|
||||||
aiService := services.NewAIGradingService()
|
return
|
||||||
aiResult, err := aiService.GradeShortAnswer(question.Question, standardAnswerStr, userAnswerStr)
|
}
|
||||||
if err != nil {
|
|
||||||
// AI评分失败时记录日志,但不影响主流程
|
// 获取标准答案字符串
|
||||||
log.Printf("AI评分失败: %v", err)
|
standardAnswerStr, ok := correctAnswer.(string)
|
||||||
} else {
|
if !ok {
|
||||||
// 使用AI的评分结果
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
correct = aiResult.IsCorrect
|
"success": false,
|
||||||
aiGrading = &models.AIGrading{
|
"message": "题目答案格式错误",
|
||||||
Score: aiResult.Score,
|
})
|
||||||
Feedback: aiResult.Feedback,
|
return
|
||||||
Suggestion: aiResult.Suggestion,
|
}
|
||||||
}
|
|
||||||
}
|
// 调用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: 标准答案
|
// standardAnswer: 标准答案
|
||||||
// userAnswer: 用户答案
|
// userAnswer: 用户答案
|
||||||
func (s *AIGradingService) GradeShortAnswer(question, standardAnswer, userAnswer string) (*AIGradingResult, error) {
|
func (s *AIGradingService) GradeShortAnswer(question, standardAnswer, userAnswer string) (*AIGradingResult, error) {
|
||||||
// 构建评分提示词
|
// 构建评分提示词(严格评分模式)
|
||||||
prompt := fmt.Sprintf(`你是一位专业的阅卷老师,请对以下简答题进行评分。
|
prompt := fmt.Sprintf(`你是一位严格的阅卷老师,请严格按照标准答案对以下简答题进行评分。
|
||||||
|
|
||||||
题目:%s
|
题目:%s
|
||||||
|
|
||||||
@ -56,21 +56,29 @@ func (s *AIGradingService) GradeShortAnswer(question, standardAnswer, userAnswer
|
|||||||
|
|
||||||
学生答案:%s
|
学生答案:%s
|
||||||
|
|
||||||
请按照以下要求进行评分:
|
评分标准(请严格遵守):
|
||||||
1. 给出一个0-100的分数
|
1. 必须与标准答案进行逐项对比
|
||||||
2. 判断答案是否正确(60分及以上为正确)
|
2. 答案要点完全覆盖标准答案且表述准确的,给85-100分
|
||||||
3. 给出简短的评语(不超过50字)
|
3. 答案要点基本覆盖但有缺漏或表述不够准确的,给60-84分
|
||||||
4. 如果答案不完善,给出改进建议(不超过50字,如果答案很好可以为空)
|
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格式返回结果:
|
请按照以下JSON格式返回结果:
|
||||||
{
|
{
|
||||||
"score": 85,
|
"score": 85,
|
||||||
"is_correct": true,
|
"is_correct": true,
|
||||||
"feedback": "答案基本正确,要点全面",
|
"feedback": "答案覆盖了主要要点,但XXX部分描述不够准确",
|
||||||
"suggestion": "可以补充一些具体的例子"
|
"suggestion": "建议补充XXX内容,并完善XXX的描述"
|
||||||
}
|
}
|
||||||
|
|
||||||
注意:只返回JSON格式的结果,不要有其他内容。`, question, standardAnswer, userAnswer)
|
注意:只返回JSON格式的结果,不要有其他内容。必须严格对照标准答案评分,不要过于宽松。`, question, standardAnswer, userAnswer)
|
||||||
|
|
||||||
// 调用AI API
|
// 调用AI API
|
||||||
resp, err := s.client.CreateChatCompletion(
|
resp, err := s.client.CreateChatCompletion(
|
||||||
@ -80,14 +88,14 @@ func (s *AIGradingService) GradeShortAnswer(question, standardAnswer, userAnswer
|
|||||||
Messages: []openai.ChatCompletionMessage{
|
Messages: []openai.ChatCompletionMessage{
|
||||||
{
|
{
|
||||||
Role: openai.ChatMessageRoleSystem,
|
Role: openai.ChatMessageRoleSystem,
|
||||||
Content: "你是一位专业的阅卷老师,擅长对简答题进行公正、客观的评分。",
|
Content: "你是一位严格的阅卷老师,必须严格按照标准答案进行评分,不能过于宽松。你的评分标准是客观的、一致的、可预测的。",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Role: openai.ChatMessageRoleUser,
|
Role: openai.ChatMessageRoleUser,
|
||||||
Content: prompt,
|
Content: prompt,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Temperature: 0.3, // 较低的温度以获得更稳定的评分结果
|
Temperature: 0, // 温度为0,获得最确定、最一致的评分结果
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { useSearchParams, useNavigate } from "react-router-dom";
|
import { useSearchParams, useNavigate } from "react-router-dom";
|
||||||
import { Button, message, Typography } from "antd";
|
import { Button, message, Typography, Switch, InputNumber, Popover, Space } from "antd";
|
||||||
import { ArrowLeftOutlined } from "@ant-design/icons";
|
import { ArrowLeftOutlined, SettingOutlined } from "@ant-design/icons";
|
||||||
import type { Question, AnswerResult } from "../types/question";
|
import type { Question, AnswerResult } from "../types/question";
|
||||||
import * as questionApi from "../api/question";
|
import * as questionApi from "../api/question";
|
||||||
import QuestionProgress from "../components/QuestionProgress";
|
import QuestionProgress from "../components/QuestionProgress";
|
||||||
@ -28,6 +28,33 @@ const QuestionPage: React.FC = () => {
|
|||||||
const [wrongCount, setWrongCount] = useState(0);
|
const [wrongCount, setWrongCount] = useState(0);
|
||||||
const [showSummary, setShowSummary] = useState(false);
|
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恢复答题进度
|
// 从localStorage恢复答题进度
|
||||||
const getStorageKey = () => {
|
const getStorageKey = () => {
|
||||||
const type = searchParams.get("type");
|
const type = searchParams.get("type");
|
||||||
@ -180,13 +207,13 @@ const QuestionPage: React.FC = () => {
|
|||||||
saveProgress(currentIndex, correctCount, newWrong);
|
saveProgress(currentIndex, correctCount, newWrong);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果答案正确,1秒后自动进入下一题
|
// 如果答案正确且开启自动跳转,根据设置的延迟时间后自动进入下一题
|
||||||
if (res.data.correct) {
|
if (res.data.correct && autoNext) {
|
||||||
setAutoNextLoading(true);
|
setAutoNextLoading(true);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setAutoNextLoading(false);
|
setAutoNextLoading(false);
|
||||||
handleNext();
|
handleNext();
|
||||||
}, 1000);
|
}, autoNextDelay * 1000); // 将秒转换为毫秒
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -279,6 +306,53 @@ const QuestionPage: React.FC = () => {
|
|||||||
<span className={styles.statLabel}>错误</span>
|
<span className={styles.statLabel}>错误</span>
|
||||||
<span className={styles.statValue} style={{ color: '#ff4d4f' }}>{wrongCount}</span>
|
<span className={styles.statValue} style={{ color: '#ff4d4f' }}>{wrongCount}</span>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user