|
|
@@ -2,6 +2,7 @@ package com.njuzr.eaibackend.service;
|
|
|
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
|
|
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
|
|
+import com.njuzr.eaibackend.constant.PromptConstant;
|
|
|
import com.njuzr.eaibackend.exception.MyException;
|
|
|
import com.njuzr.eaibackend.mapper.*;
|
|
|
import com.njuzr.eaibackend.po.*;
|
|
|
@@ -29,76 +30,6 @@ import java.util.concurrent.TimeoutException;
|
|
|
public class AIEvaluationService {
|
|
|
|
|
|
private static final int RETRY_COUNT = 3;
|
|
|
- // 整体评价
|
|
|
- // 两个输入 1.作文描述 2.作文内容
|
|
|
- private static final String REWRITE_PROMPT_TEMPLATE = "假设你现在是一位英文专业的教师,正在批阅一位同学的写作作业,作业描述是:%s"
|
|
|
- + "请做如下工作:\n"
|
|
|
- + "1. 满分100分,请综合英文用词、句式等方面,给出评分和评分原因\n"
|
|
|
- + "2. 请对以下英语作文进行详细分析,从词汇、句法、内容结构、逻辑连贯性、语言风格以及整体写作水平等方面展开。\n" +
|
|
|
- " 词汇层面,评估词汇的丰富度,是否存在重复词汇,是否运用了适合文章主题和难度级别的词汇,有无高级词汇或专业术语的恰当使用。检查词汇拼写与词性搭配是否准确,以及词汇的感情色彩是否契合文章基调。\n" +
|
|
|
- " 句法层面,剖析句子结构的多样性,是否合理运用了简单句、并列句、复合句等。判断句子语法是否正确,包括主谓一致、时态一致、语态使用是否恰当等。关注句子的长度和复杂度是否适中,是否存在句式过于冗长或过于简单的问题。\n" +
|
|
|
- " 内容结构方面,分析文章是否紧扣主题,开头是否能够有效引入主题,中间论述是否条理清晰、层次分明,结尾是否有力且能升华主题。检查段落之间的过渡是否自然,每个段落是否有明确的中心思想,段落内部的内容是否紧密围绕该中心思想展开。\n" +
|
|
|
- " 逻辑连贯性上,判断文章整体逻辑是否清晰,观点之间的衔接是否自然流畅,是否使用了合适的连接词、过渡语来增强逻辑关系。\n" +
|
|
|
- " 语言风格上,考量文章是正式、半正式还是口语化风格,风格是否与文章类型和受众相匹配。\n" +
|
|
|
- " 整体写作水平方面,综合上述各项因素,给出一个整体评价,指出文章的优点与不足,并提出具体的改进建议。\n"
|
|
|
- + "格式举例:\n"
|
|
|
- + "评分:95分\n" +
|
|
|
- "\n" +
|
|
|
- "评分原因:\n" +
|
|
|
- "\n" +
|
|
|
- "1. 词汇层面:文章词汇丰富,运用了多种词汇,如\"palpable sense of community\"、\"isolationist world\"、\"raw creativity\"、\"immense patience\"、\"academic rigor\"等,体现了作者对词汇的恰当运用。文章中未出现重复词汇,高级词汇和术语的使用也恰到好处,如\"algorithm design\"。\n" +
|
|
|
- "\n" +
|
|
|
- "2. 句法层面:文章句式多样,合理运用了简单句、并列句和复合句。句子结构清晰,语法正确,如主谓一致和时态一致等。句子的长度和复杂度适中,既不过于冗长也不过于简单。\n" +
|
|
|
- "\n" +
|
|
|
- "3. 内容结构方面:文章紧扣主题,开头通过激活时光机器的方式引人入胜。中间部分分别从校园氛围、学术氛围、社交氛围等方面展开,条理清晰,层次分明。结尾升华主题,表达对那段时光的感激之情,使文章更具感染力。段落之间的过渡自然,每个段落都有明确的中心思想。\n" +
|
|
|
- "\n" +
|
|
|
- "4. 逻辑连贯性方面:文章整体逻辑清晰,观点之间的衔接自然流畅,使用了合适的连接词和过渡语来增强逻辑关系。\n" +
|
|
|
- "\n" +
|
|
|
- "5. 语言风格方面:文章采用正式风格,适合文章类型和受众,既庄重又富有感染力。\n" +
|
|
|
- "\n" +
|
|
|
- "6. 整体写作水平方面:文章结构完整,内容丰富,语言流畅,表达清晰,具有很高的写作水平。\n" +
|
|
|
- "\n" +
|
|
|
- "改进建议:\n" +
|
|
|
- "1. 可以适当增加一些形容词和副词,使文章更加生动形象。\n" +
|
|
|
- "2. 在描述校园氛围和学术氛围时,可以加入一些具体的事例来增强说服力。\n" +
|
|
|
- "3. 结尾部分可以进一步强调作者对那段时光的感激之情,使文章更具感染力。"
|
|
|
- + "其余的任何描述都不需要,也不需要任何交互!\n"
|
|
|
- + "以下是学生作业内容:\n\n"
|
|
|
- + "%s";
|
|
|
- // 逐句评价
|
|
|
- private static final String SENTENCE_PROMPT_TEMPLATE = "假设你是一位英文专业的教师,正在批改一份写作作业,以下是作业相关信息:\n" +
|
|
|
- "- 作业描述:%s\n" +
|
|
|
- "- 学生作业内容:%s\n" +
|
|
|
- "- 待分析的句子:%s\n" +
|
|
|
- "\n" + "请从以下三个角度对该句子进行评价,并选择最需要修改的一个角度给出意见。若句子合理,则回复“合理”;若句子不合理,则按照以下格式回复:\n" +
|
|
|
- "{角度小标题}\n" +
|
|
|
- "{具体修改建议}\n" +
|
|
|
- "\n" +
|
|
|
- "例如\n" +
|
|
|
- "\n" +
|
|
|
- "1.合理\n" +
|
|
|
- "\n" +
|
|
|
- "则回复\n" +
|
|
|
- "\n" +
|
|
|
- "修改建议:合理\n" +
|
|
|
- "\n" +
|
|
|
- "2.词汇最不合理\n" +
|
|
|
- "\n" +
|
|
|
- "则回复\n" +
|
|
|
- "\n" +
|
|
|
- "词汇表达意思不够生动\n" +
|
|
|
- "\n" +
|
|
|
- "可以使用“the world outside the time machine swirled into obscurity”或“the world outside the time machine dissolved into a whirl of blurs”来替代,这样既能保持句子的流畅性,又能增加描述的生动性。" +
|
|
|
- "评判标准如下:\n" +
|
|
|
- "- **词汇分析**:\n" +
|
|
|
- " - **词汇丰富度**:查看句子中词汇的多样性,避免词汇重复,确保词汇的难度符合作文要求。如高中英语作文应使用一定比例的高级词汇,大学英语四六级作文则对词汇丰富度和复杂度有更高要求。检查是否有简单词汇可被更精准、更具表现力的词汇替换,例如“good”可改为“excellent”“marvelous”“superb”等。\n" +
|
|
|
- " - **词汇准确性**:确保词汇拼写正确,词性使用恰当,注意词汇搭配的合理性和符合英语表达习惯,例如“affect”(动词,影响)和“effect”(名词,影响)不可混淆,“make a decision”正确,“do a decision”错误。\n" +
|
|
|
- "- **句法分析**:\n" +
|
|
|
- " - **句型结构**:分析使用的基本句型(如主谓宾、主系表等),检查是否运用多种句型以丰富表达,避免单一。例如,应灵活运用简单句、并列句和复合句。确保句子结构完整,无成分缺失或多余,如“Because he is ill, so he didn't come to school.”应去掉“because”或“so”其中一个。\n" +
|
|
|
- " - **语法正确性**:检查时态、语态是否正确且一致,例如描述过去事件用一般过去时,强调动作承受者用被动语态。同时,检查主谓是否一致,名词单复数是否正确,如“The book on the desk are mine.”应改为“The book on the desk is mine.”。\n" +
|
|
|
- "- **内容结构分析**:\n" +
|
|
|
- " - **与主题相关性**:判断句子是否紧密围绕作文主题展开,是否对主题表达有积极贡献。例如,若主题是“环境保护”,句子应涉及环境污染现状、原因、解决措施等相关内容。同时,检查句子在上下文中的连贯性,是否能自然衔接前后句,使文章逻辑清晰,如使用过渡句引导读者理解文章思路。\n" +
|
|
|
- " - **逻辑连贯性**:分析句子内部及句子间的逻辑关系是否清晰,使用恰当连接词(如“and”“but”“however”“therefore”等)表达并列、转折、因果等逻辑关系。检查句子能否有效支持段落主旨,段落能否服务于文章整体结构,例如在论述观点时,句子应提供具体论据或例子支持论点。";
|
|
|
|
|
|
private final AIRequestService aiRequestService;
|
|
|
|
|
|
@@ -136,56 +67,56 @@ public class AIEvaluationService {
|
|
|
}
|
|
|
|
|
|
public CompletableFuture<AIEntry> evaluationAsync(Long assignmentId, Long studentId) {
|
|
|
- return CompletableFuture.supplyAsync(() -> evaluation(assignmentId, studentId), aiTaskExecutor);
|
|
|
+ return CompletableFuture.supplyAsync(() -> evaluation(assignmentId, studentId), aiTaskExecutor)
|
|
|
+ .exceptionally(ex -> { throw new MyException(HttpStatus.INTERNAL_SERVER_ERROR.value(), "AI请求失败:" + ex.getMessage());})
|
|
|
+ .orTimeout(40, TimeUnit.SECONDS);
|
|
|
}
|
|
|
+
|
|
|
/**
|
|
|
* AI作文整体智能批改
|
|
|
* @param assignmentId
|
|
|
+ *
|
|
|
* @param studentId
|
|
|
* @return
|
|
|
*/
|
|
|
public AIEntry evaluation(Long assignmentId, Long studentId) {
|
|
|
StopWatch sw = StopWatch.createStarted();
|
|
|
+ try{
|
|
|
+ Engagement engagement = studentAssignmentMapper.findEngagementByStudentIdAndAssignmentId(studentId, assignmentId);
|
|
|
+ if(engagement == null) throw MyException.create(HttpStatus.BAD_REQUEST, "作业参与不存在");
|
|
|
+ Assignment assignment = assignmentMapper.selectById(assignmentId);
|
|
|
+ if (assignment == null) throw MyException.create(HttpStatus.BAD_REQUEST, "作业不存在");
|
|
|
+ Evaluation evaluationJudge = evaluationMapperServiceImpl.getByVersion(assignmentId, studentId, engagement.getVersion());
|
|
|
+ if(evaluationJudge != null) throw MyException.create(HttpStatus.BAD_REQUEST, "作业已进行过AI分析");
|
|
|
+ initEvaluationRecord(assignmentId, studentId, engagement);
|
|
|
|
|
|
- Engagement engagement = studentAssignmentMapper.findEngagementByStudentIdAndAssignmentId(studentId, assignmentId);
|
|
|
- if(engagement == null) throw MyException.create(HttpStatus.BAD_REQUEST, "作业参与不存在");
|
|
|
- Assignment assignment = assignmentMapper.selectById(assignmentId);
|
|
|
- if (assignment == null) throw MyException.create(HttpStatus.BAD_REQUEST, "作业不存在");
|
|
|
- Evaluation evaluationJudge = evaluationMapperServiceImpl.getByVersion(assignmentId, studentId, engagement.getVersion());
|
|
|
- if(evaluationJudge != null) throw MyException.create(HttpStatus.BAD_REQUEST, "作业已进行过AI分析");
|
|
|
- log.info("evaluation start");
|
|
|
- initEvaluationRecord(assignmentId, studentId, engagement);
|
|
|
- log.info("evaluation end");
|
|
|
- String prompt = String.format(REWRITE_PROMPT_TEMPLATE, assignment.getDescription(), engagement.getTextContent());
|
|
|
- AIRequestService.AIResponse response = requestAI(prompt, 0);
|
|
|
- String retContent;
|
|
|
- String role;
|
|
|
- try {
|
|
|
- retContent = response.getChoices().get(0).getMessage().getContent();
|
|
|
- role = response.getChoices().get(0).getMessage().getRole();
|
|
|
- log.info("evaluation requestAI Response:"+retContent);
|
|
|
+ // 发起LLM请求
|
|
|
+ String prompt = String.format(PromptConstant.REWRITE_PROMPT_TEMPLATE, assignment.getDescription(), engagement.getTextContent());
|
|
|
+ AIRequestService.AIResponse response = requestAIWithRetry(prompt, 0);
|
|
|
+ String retContent = response.getChoices().get(0).getMessage().getContent();
|
|
|
+ String role = response.getChoices().get(0).getMessage().getRole();
|
|
|
+
|
|
|
+ OverallEvaluation overallEvaluation = new OverallEvaluation()
|
|
|
+ .setStudentId(studentId)
|
|
|
+ .setAssignmentId(assignmentId)
|
|
|
+ .setContent(engagement.getTextContent())
|
|
|
+ .setEvaluation(retContent)
|
|
|
+ .setVersion(engagement.getVersion());
|
|
|
+ overallEvaluationMapper.insert(overallEvaluation);
|
|
|
+
|
|
|
+ evaluationMapperServiceImpl.update(new UpdateWrapper<Evaluation>()
|
|
|
+ .set("overall", 1)
|
|
|
+ .eq("student_id", studentId)
|
|
|
+ .eq("assignment_id", assignmentId)
|
|
|
+ .eq("version", engagement.getVersion()));
|
|
|
+
|
|
|
+ log.info("evaluation cost: {}", sw.formatTime());
|
|
|
+ return new AIEntry(role, retContent);
|
|
|
} catch (Exception e) {
|
|
|
- log.error("evaluation requestAI error: {}", response);
|
|
|
+ log.error("evaluation requestAI error: {}", e.getMessage());
|
|
|
throw new MyException(HttpStatus.INTERNAL_SERVER_ERROR.value(), HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase()+":"+"请求失败");
|
|
|
}
|
|
|
|
|
|
- OverallEvaluation overallEvaluation = new OverallEvaluation()
|
|
|
- .setStudentId(studentId)
|
|
|
- .setAssignmentId(assignmentId)
|
|
|
- .setContent(engagement.getTextContent())
|
|
|
- .setEvaluation(retContent)
|
|
|
- .setVersion(engagement.getVersion());
|
|
|
- overallEvaluationMapper.insert(overallEvaluation);
|
|
|
-
|
|
|
- evaluationMapperServiceImpl.update(new UpdateWrapper<Evaluation>()
|
|
|
- .set("overall", 1)
|
|
|
- .eq("student_id", studentId)
|
|
|
- .eq("assignment_id", assignmentId)
|
|
|
- .eq("version", engagement.getVersion()));
|
|
|
-
|
|
|
- log.info("evaluation cost: {}", sw.formatTime());
|
|
|
- return new AIEntry(role, retContent);
|
|
|
-
|
|
|
}
|
|
|
|
|
|
private void initEvaluationRecord(Long assignmentId, Long studentId, Engagement engagement) {
|
|
|
@@ -201,13 +132,13 @@ public class AIEvaluationService {
|
|
|
log.info("evaluation record: {}", evaluation);
|
|
|
}
|
|
|
|
|
|
- private AIRequestService.AIResponse requestAI(String prompt, int retryTime) {
|
|
|
+ private AIRequestService.AIResponse requestAIWithRetry(String prompt, int retryTime) {
|
|
|
List<AIEntry> entry = new ArrayList<>();
|
|
|
entry.add(new AIEntry("user", prompt));
|
|
|
AIRequestService.AIResponse response = aiRequestService.requestChatGLM4(entry);
|
|
|
if(response == null && retryTime < 3) {
|
|
|
log.info("requestAI failed, retryTime: {}", retryTime + 1);
|
|
|
- return requestAI(prompt, retryTime + 1);
|
|
|
+ return requestAIWithRetry(prompt, retryTime + 1);
|
|
|
}
|
|
|
return response;
|
|
|
}
|
|
|
@@ -254,8 +185,7 @@ public class AIEvaluationService {
|
|
|
}
|
|
|
|
|
|
String content = engagement.getTextContent();
|
|
|
- // TODO: 句子分割需要详细考虑和判断
|
|
|
- String[] sentences = content.split("\\.");
|
|
|
+ String[] sentences = getSentenceList(content);
|
|
|
List<SentenceEvaluation> sentenceEvaluationsList = new ArrayList<>();
|
|
|
CompletableFuture<?>[] futures = new CompletableFuture[sentences.length];
|
|
|
|
|
|
@@ -298,6 +228,11 @@ public class AIEvaluationService {
|
|
|
return sentenceEvaluationsList;
|
|
|
}
|
|
|
|
|
|
+ private static String[] getSentenceList(String content) {
|
|
|
+ String[] sentences = content.split("\\.");
|
|
|
+ return sentences;
|
|
|
+ }
|
|
|
+
|
|
|
private SentenceEvaluation processSingleSentenceWithRetry(Long assignmentId, Long studentId, String content, String description, String sentence, int sentenceNo, int version) {
|
|
|
int retryTime = 0;
|
|
|
while (retryTime < RETRY_COUNT) {
|
|
|
@@ -311,6 +246,18 @@ public class AIEvaluationService {
|
|
|
return new SentenceEvaluation().setVersion(version).setNo(sentenceNo + 1).setType("error").setCategory("error").setEvaluation("error").setStudentId(studentId).setAssignmentId(assignmentId);
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 向Dify发起请求智评单个句子
|
|
|
+ * @param assignmentId
|
|
|
+ * @param studentId
|
|
|
+ * @param content
|
|
|
+ * @param assignmentDesc
|
|
|
+ * @param sentence
|
|
|
+ * @param sentenceNo
|
|
|
+ * @param version
|
|
|
+ * @param retryCount
|
|
|
+ * @return
|
|
|
+ */
|
|
|
private SentenceEvaluation processSingleSentence(
|
|
|
Long assignmentId, Long studentId,
|
|
|
String content, String assignmentDesc,
|
|
|
@@ -389,4 +336,82 @@ public class AIEvaluationService {
|
|
|
return evaluationMapper.selectList(queryWrapper);
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 对某一个句子进行智评
|
|
|
+ * @param assignmentId
|
|
|
+ * @param studentId
|
|
|
+ * @param version
|
|
|
+ * @return
|
|
|
+ */
|
|
|
+ public SentenceEvaluation evaluateSentenceByOne(Long assignmentId, Long studentId, Integer version, Integer sentenceNo) {
|
|
|
+ Engagement engagement = studentAssignmentMapper.findEngagementByStudentIdAndAssignmentId(studentId, assignmentId);
|
|
|
+ if(engagement == null) throw MyException.create(HttpStatus.BAD_REQUEST, "作业参与不存在");
|
|
|
+ Assignment assignment = assignmentMapper.selectById(assignmentId);
|
|
|
+ if (assignment == null) throw MyException.create(HttpStatus.BAD_REQUEST, "作业不存在");
|
|
|
+ Evaluation evaluationJudge = evaluationMapperServiceImpl.getByVersion(assignmentId, studentId, engagement.getVersion());
|
|
|
+ if(evaluationJudge == null) throw MyException.create(HttpStatus.BAD_REQUEST, "没有该版本的智评记录");
|
|
|
+
|
|
|
+ String content = engagement.getTextContent();
|
|
|
+ String[] sentences = getSentenceList(content);
|
|
|
+ SentenceEvaluation result = processSingleSentenceWithRetry(assignmentId, studentId, content, assignment.getDescription(), sentences[sentenceNo], sentenceNo, engagement.getVersion());
|
|
|
+ if(result == null || result.getType().equals("error")) {
|
|
|
+ throw MyException.create(HttpStatus.BAD_REQUEST, "智评失败,请重试");
|
|
|
+ }
|
|
|
+
|
|
|
+ sentenceEvaluationMapperServiceImpl.saveOrUpdateByCondition(result);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 双智评:同时触发整体智评和逐句智评
|
|
|
+ * 1. 创建智评记录
|
|
|
+ * 2. 同步执行整体智评(等待结果)
|
|
|
+ * 3. 异步执行逐句智评(不等待结果)
|
|
|
+ *
|
|
|
+ * @param assignmentId 作业ID
|
|
|
+ * @param studentId 学生ID
|
|
|
+ * @return 整体智评结果的CompletableFuture
|
|
|
+ */
|
|
|
+ @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
|
|
|
+ public CompletableFuture<AIEntry> doubleEvaluationAsync(Long assignmentId, Long studentId) {
|
|
|
+ // 1. 基础校验和记录初始化
|
|
|
+ Engagement engagement = studentAssignmentMapper.findEngagementByStudentIdAndAssignmentId(studentId, assignmentId);
|
|
|
+ if (engagement == null) throw MyException.create(HttpStatus.BAD_REQUEST, "作业参与不存在");
|
|
|
+ Assignment assignment = assignmentMapper.selectById(assignmentId);
|
|
|
+ if (assignment == null) throw MyException.create(HttpStatus.BAD_REQUEST, "作业不存在");
|
|
|
+ if (evaluationMapperServiceImpl.getByVersion(assignmentId, studentId, engagement.getVersion()) != null) {
|
|
|
+ throw MyException.create(HttpStatus.BAD_REQUEST, "该版本作业已进行过AI分析");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 创建智评记录(原子操作)
|
|
|
+ initEvaluationRecord(assignmentId, studentId, engagement);
|
|
|
+ log.info("双智评记录已创建,开始并行处理");
|
|
|
+
|
|
|
+ // 3. 同步执行整体智评(阻塞等待结果)
|
|
|
+ CompletableFuture<AIEntry> overallFuture = CompletableFuture.supplyAsync(() -> {
|
|
|
+ try {
|
|
|
+ return evaluation(assignmentId, studentId);
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw MyException.create(HttpStatus.BAD_REQUEST, "整体智评失败");
|
|
|
+ }
|
|
|
+ }, aiTaskExecutor);
|
|
|
+
|
|
|
+ // 4. 异步执行逐句智评(不阻塞主线程)
|
|
|
+ CompletableFuture.runAsync(() -> {
|
|
|
+ try {
|
|
|
+ rewritePerSentence(assignmentId, studentId);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("逐句智评异步执行失败: assignmentId={}, studentId={}, error={}",
|
|
|
+ assignmentId, studentId, e.getMessage(), e);
|
|
|
+ }
|
|
|
+ }, aiTaskExecutor);
|
|
|
+
|
|
|
+ return overallFuture
|
|
|
+ .exceptionally(ex -> {
|
|
|
+ log.error("双智评整体处理失败", ex);
|
|
|
+ throw new MyException(HttpStatus.INTERNAL_SERVER_ERROR.value(), "双智评执行异常");
|
|
|
+ })
|
|
|
+ .orTimeout(120, TimeUnit.SECONDS); // 整体智评超时控制
|
|
|
+ }
|
|
|
+
|
|
|
}
|