第2251篇:在线教育的智能批改——主观题和代码题的AI评分系统
2026/4/30大约 6 分钟
第2251篇:在线教育的智能批改——主观题和代码题的AI评分系统
适读人群:教育科技工程师、Java开发者、在线教育平台技术团队 | 阅读时长:约15分钟 | 核心价值:深度讲解主观题和代码题AI评分的工程挑战与完整实现方案
在线教育平台规模化之后,最头疼的问题就是批改。
客观题(选择、填空)计算机秒出结果,但主观题(简答、论述、代码编程)还得靠人工批改。一个有几十万学员的编程课,每周布置的编程作业,哪怕只有10%的学员提交,也是几万份代码需要批改。按每份5分钟算,那是几千人时的工作量。
这个规模下,纯人工批改根本撑不住,大部分平台的做法是"抽批"——抽样批改一部分,其他的靠自动评分,或者干脆不改。但这样做学习体验很差,学员不知道自己错在哪。
AI批改的价值就在这里:提供及时、有针对性的反馈,哪怕不是完美的,也比"等三天才知道对不对"强得多。
评分系统的架构设计
主观题评分:关键点检测+语义评估
@Service
public class SubjectiveAnswerScoringService {
@Autowired
private LLMClient llmClient;
@Autowired
private EmbeddingService embeddingService;
/**
* 主观题综合评分
* 融合关键点检测和语义质量评估
*/
public ScoringResult score(SubjectiveQuestion question, String studentAnswer) {
if (studentAnswer == null || studentAnswer.trim().length() < 10) {
return ScoringResult.emptyAnswer();
}
// 1. 关键点检测:检查是否涵盖了必须回答的要点
KeyPointCheckResult keyPointResult = checkKeyPoints(question, studentAnswer);
// 2. LLM综合评分
LLMScoringResult llmResult = scoringWithLLM(question, studentAnswer, keyPointResult);
// 3. 融合评分
double finalScore = 0.4 * keyPointResult.getCoverageScore() +
0.6 * llmResult.getQualityScore();
finalScore = Math.min(question.getMaxScore(),
Math.round(finalScore * question.getMaxScore()));
return ScoringResult.builder()
.score(finalScore)
.maxScore(question.getMaxScore())
.keyPointsCovered(keyPointResult.getCoveredPoints())
.keyPointsMissed(keyPointResult.getMissedPoints())
.feedback(llmResult.getFeedback())
.suggestedImprovements(llmResult.getSuggestedImprovements())
.build();
}
/**
* 关键点检测
* 用向量相似度判断学生答案是否涵盖了参考答案的关键要点
*/
private KeyPointCheckResult checkKeyPoints(SubjectiveQuestion question,
String studentAnswer) {
List<String> keyPoints = question.getKeyPoints();
float[] answerEmbedding = embeddingService.encode(studentAnswer);
List<String> coveredPoints = new ArrayList<>();
List<String> missedPoints = new ArrayList<>();
for (String keyPoint : keyPoints) {
float[] keyPointEmbedding = embeddingService.encode(keyPoint);
double similarity = cosineSimilarity(answerEmbedding, keyPointEmbedding);
if (similarity > 0.75) { // 相似度阈值
coveredPoints.add(keyPoint);
} else {
missedPoints.add(keyPoint);
}
}
double coverageScore = keyPoints.isEmpty() ? 1.0 :
(double) coveredPoints.size() / keyPoints.size();
return new KeyPointCheckResult(coveredPoints, missedPoints, coverageScore);
}
private LLMScoringResult scoringWithLLM(SubjectiveQuestion question,
String studentAnswer,
KeyPointCheckResult keyPointResult) {
String missedPointsDesc = keyPointResult.getMissedPoints().isEmpty() ?
"无" : String.join("、", keyPointResult.getMissedPoints());
String prompt = String.format("""
你是一位严谨的教学评估专家,请对学生的作答进行评分和点评。
题目:%s
满分:%d分
参考答案要点:%s
学生答案:
%s
已检测到以下要点未在答案中体现:%s
请从以下维度评估(0-100分的质量评分):
1. 核心概念理解的准确性
2. 论述的逻辑性和条理性
3. 表达的清晰程度
4. 是否有创新性理解或额外补充
输出JSON格式:
{
"quality_score": 0-100的整数,
"feedback": "100字以内的总体评价",
"strengths": ["答得好的地方"],
"improvements": ["需要改进的具体建议"]
}
注意:feedback要具体,有指导价值,不要泛泛而谈。
""",
question.getTitle(),
question.getMaxScore(),
String.join(";", question.getKeyPoints()),
studentAnswer,
missedPointsDesc
);
LLMResponse response = llmClient.complete(
"你是经验丰富的学科教师,擅长提供有建设性的学习反馈。",
prompt,
LLMConfig.builder().temperature(0.2).responseFormat(ResponseFormat.JSON).build()
);
return parseLLMScoringResult(response.getContent());
}
}代码题评测:功能+质量双轨评分
代码题的评测分两个维度:功能正确性(测试用例)和代码质量(风格、效率、安全):
@Service
public class CodeScoringService {
@Autowired
private CodeExecutionSandbox sandbox;
@Autowired
private CodeQualityAnalyzer qualityAnalyzer;
@Autowired
private LLMClient llmClient;
public CodeScoringResult scoreCode(CodeQuestion question, String studentCode,
String language) {
// 1. 安全检查(防止恶意代码)
SecurityCheckResult securityCheck = sandbox.securityCheck(studentCode, language);
if (!securityCheck.isSafe()) {
return CodeScoringResult.securityViolation(securityCheck.getReason());
}
// 2. 执行测试用例
TestExecutionResult testResult = runTestCases(question, studentCode, language);
// 3. 代码质量分析
CodeQualityResult qualityResult = qualityAnalyzer.analyze(studentCode, language);
// 4. LLM生成反馈
String feedback = generateCodeFeedback(question, studentCode, testResult, qualityResult);
// 综合评分
double funcScore = testResult.getPassRate() * question.getFuncMaxScore();
double qualScore = qualityResult.getScore() * question.getQualMaxScore();
return CodeScoringResult.builder()
.functionScore(funcScore)
.qualityScore(qualScore)
.totalScore(funcScore + qualScore)
.testResults(testResult.getDetailedResults())
.qualityIssues(qualityResult.getIssues())
.feedback(feedback)
.build();
}
/**
* 在沙箱环境中执行代码并跑测试用例
*/
private TestExecutionResult runTestCases(CodeQuestion question,
String studentCode, String language) {
List<TestCaseResult> results = new ArrayList<>();
for (TestCase testCase : question.getTestCases()) {
try {
ExecutionResult exec = sandbox.execute(
studentCode,
language,
testCase.getInput(),
ExecutionConfig.builder()
.timeoutMs(testCase.getTimeLimitMs())
.memoryLimitMB(256)
.build()
);
boolean passed = compareOutput(exec.getOutput(), testCase.getExpectedOutput());
results.add(TestCaseResult.builder()
.testCaseId(testCase.getId())
.input(testCase.isVisible() ? testCase.getInput() : "[隐藏]")
.expectedOutput(testCase.isVisible() ? testCase.getExpectedOutput() : "[隐藏]")
.actualOutput(exec.getOutput())
.passed(passed)
.executionTimeMs(exec.getExecutionTimeMs())
.errorMessage(exec.getErrorMessage())
.build());
} catch (TimeoutException e) {
results.add(TestCaseResult.timeout(testCase.getId()));
} catch (Exception e) {
results.add(TestCaseResult.runtimeError(testCase.getId(), e.getMessage()));
}
}
long passCount = results.stream().filter(TestCaseResult::isPassed).count();
return new TestExecutionResult(results, (double) passCount / results.size());
}
private String generateCodeFeedback(CodeQuestion question, String studentCode,
TestExecutionResult testResult,
CodeQualityResult qualityResult) {
// 只有失败的测试用例才给出详细分析
List<TestCaseResult> failedCases = testResult.getDetailedResults().stream()
.filter(r -> !r.isPassed() && r.isVisible()) // 只显示公开测试用例
.limit(3)
.collect(Collectors.toList());
if (testResult.getPassRate() == 1.0 && qualityResult.getIssues().isEmpty()) {
return "代码功能正确,实现简洁。";
}
String prompt = String.format("""
请对以下编程题的解答给出简洁的反馈(150字以内):
题目:%s
学生代码:
```%s
%s
```
测试通过率:%.0f%%
失败的测试用例:%s
代码质量问题:%s
请给出:
1. 主要问题是什么(如果有)
2. 改进建议(具体,可操作)
3. 如果代码正确,给出简单的代码评价
语气友好,鼓励为主。
""",
question.getTitle(),
language,
studentCode.length() > 500 ? studentCode.substring(0, 500) + "..." : studentCode,
testResult.getPassRate() * 100,
formatFailedCases(failedCases),
formatQualityIssues(qualityResult.getIssues())
);
return llmClient.complete(
"你是耐心的编程导师,擅长给出有建设性的代码反馈。",
prompt,
LLMConfig.builder().temperature(0.3).maxTokens(300).build()
).getContent();
}
}评分一致性和质量保障
AI评分不可能100%准确,需要质量保障机制:
@Service
public class ScoringQualityAssurance {
@Autowired
private ScoringResultRepository scoringRepo;
@Autowired
private HumanReviewService humanReviewService;
/**
* 抽样人工审核:确保AI评分质量
*/
@Scheduled(fixedDelay = 3600000) // 每小时执行
public void sampleAndReview() {
// 1. 随机抽取5%的评分结果
List<ScoringResult> sample = scoringRepo.randomSample(0.05);
// 2. 优先选择边界分数(高置信度区间外的)
List<ScoringResult> borderlineCases = scoringRepo.findBorderlineCases(
0.4, 0.6 // 得分率在40%-60%之间的
);
// 3. 选择学生投诉的评分
List<ScoringResult> disputedCases = scoringRepo.findDisputedCases();
// 4. 提交人工审核
List<ScoringResult> reviewQueue = new ArrayList<>();
reviewQueue.addAll(sample);
reviewQueue.addAll(borderlineCases);
reviewQueue.addAll(disputedCases);
humanReviewService.submitForReview(dedup(reviewQueue));
}
/**
* 根据人工审核结果,持续改进评分规则
*/
public void processHumanReviewResults(List<HumanReviewResult> reviewResults) {
long agreementCount = reviewResults.stream()
.filter(HumanReviewResult::isAIScoreAccepted)
.count();
double agreementRate = (double) agreementCount / reviewResults.size();
log.info("AI评分与人工评分一致率: {:.1f}%", agreementRate * 100);
// 一致率低于80%时触发报警
if (agreementRate < 0.8) {
alertService.sendAlert("AI评分质量下降,一致率: " + agreementRate);
}
// 收集不一致案例,用于模型改进
List<HumanReviewResult> disagreements = reviewResults.stream()
.filter(r -> !r.isAIScoreAccepted())
.collect(Collectors.toList());
modelImprovement.addTrainingExamples(disagreements);
}
}工程实践的核心洞察
做了两年教育AI批改系统,最重要的一个工程认知是:
AI批改的价值不在于打分,而在于反馈。
学生拿到分数,他最关心的不是"得了几分",而是"哪里错了,怎么改"。如果AI只能给出一个数字分数,那跟没有反馈差不多。
但如果AI能说:"你的第二点论述逻辑不够清晰,建议先说结论,再举例论证",或者"这段代码逻辑正确,但没有处理边界情况,当输入为空时会抛出NullPointerException"——这才是真正有学习价值的反馈。
所以评分系统的技术重心不是"让评分更精准",而是"让反馈更有用"。这两件事,后者更难,也更重要。
