@ -3,6 +3,7 @@ package com.sztzjy.linkCommerce.service.impl;
import com.fasterxml.jackson.core.JsonProcessingException ;
import com.fasterxml.jackson.core.JsonProcessingException ;
import com.fasterxml.jackson.databind.JsonNode ;
import com.fasterxml.jackson.databind.JsonNode ;
import com.fasterxml.jackson.databind.ObjectMapper ;
import com.fasterxml.jackson.databind.ObjectMapper ;
import com.fasterxml.jackson.databind.node.ArrayNode ;
import com.fasterxml.jackson.databind.node.ObjectNode ;
import com.fasterxml.jackson.databind.node.ObjectNode ;
import com.sztzjy.linkCommerce.ai.QwenChatClient ;
import com.sztzjy.linkCommerce.ai.QwenChatClient ;
import com.sztzjy.linkCommerce.config.exception.handler.ServiceException ;
import com.sztzjy.linkCommerce.config.exception.handler.ServiceException ;
@ -49,6 +50,8 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
private static final int MAX_HELP_QUESTION_LENGTH = 500 ;
private static final int MAX_HELP_QUESTION_LENGTH = 500 ;
private static final int MAX_STEP_CONTEXT_LENGTH = 2000 ;
private static final int MAX_STEP_CONTEXT_LENGTH = 2000 ;
private static final int MAX_ASSESSMENT_RATIONALE_CHARACTERS = 200 ;
private static final int MAX_ASSESSMENT_RATIONALE_CHARACTERS = 200 ;
private static final int MAX_CHOICE_TITLE_LENGTH = 120 ;
private static final int MAX_CHOICE_OPTION_LENGTH = 80 ;
private final ObjectMapper objectMapper = new ObjectMapper ( ) ;
private final ObjectMapper objectMapper = new ObjectMapper ( ) ;
@ -131,6 +134,9 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
String answerSnapshot = answerSnapshot ( context . answer ) ;
String answerSnapshot = answerSnapshot ( context . answer ) ;
boolean reassessment = SUCCEEDED . equals ( evaluation . getAssessmentStatus ( ) ) ;
boolean reassessment = SUCCEEDED . equals ( evaluation . getAssessmentStatus ( ) ) ;
if ( reassessment & & answerSnapshot . equals ( evaluation . getAssessmentAnswerSnapshot ( ) ) ) {
if ( reassessment & & answerSnapshot . equals ( evaluation . getAssessmentAnswerSnapshot ( ) ) ) {
log . info ( "AI assessment cached hit, evaluationId={}, answerId={}, snapshotLength={}" ,
evaluation . getId ( ) , context . answer = = null ? null : context . answer . getId ( ) ,
answerSnapshot . length ( ) ) ;
return toView ( evaluation ) ;
return toView ( evaluation ) ;
}
}
String taskSnapshot = taskSnapshot ( context . effectiveContent ) ;
String taskSnapshot = taskSnapshot ( context . effectiveContent ) ;
@ -145,7 +151,11 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
String rawResponse = null ;
String rawResponse = null ;
try {
try {
String knowledgeContext = knowledgeContext ( context , user , "" ) ;
String knowledgeContext = knowledgeContext ( context , user , "" ) ;
rawResponse = qwenChatClient . completeJson ( assessmentSystemPrompt ( criterionPlan ) , assessmentUserPrompt ( taskSnapshot , answerSnapshot , knowledgeContext ) ) ;
String choiceQuestionContext = choiceQuestionContext ( context . effectiveContent , context . answer ) ;
log . info ( "AI assessment choice question context prepared, evaluationId={}, entries={}" ,
evaluation . getId ( ) , choiceQuestionEntryCount ( choiceQuestionContext ) ) ;
rawResponse = qwenChatClient . completeJson ( assessmentSystemPrompt ( criterionPlan ) ,
assessmentUserPrompt ( taskSnapshot , answerSnapshot , knowledgeContext , choiceQuestionContext ) ) ;
JsonNode assessmentReport = object ( rawResponse ) ;
JsonNode assessmentReport = object ( rawResponse ) ;
int score = validateAssessment ( assessmentReport , criterionPlan ) ;
int score = validateAssessment ( assessmentReport , criterionPlan ) ;
compactAssessmentRationales ( assessmentReport ) ;
compactAssessmentRationales ( assessmentReport ) ;
@ -316,6 +326,59 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
return json ( node ) ;
return json ( node ) ;
}
}
/ * *
* 第 一 步 选 择 题 的 评 分 上 下 文 : 题 干 、 选 项 文 字 、 标 准 答 案 与 学 生 作 答 逐 题 对 照 。
* 只 用 于 AI 助 评 — — 助 学 的 提 示 词 面 向 学 生 , 带 标 准 答 案 会 泄 题 。
* /
private String choiceQuestionContext ( EffectiveTrainingTaskContent content , StudentTrainingAnswer answer ) {
ArrayNode result = objectMapper . createArrayNode ( ) ;
JsonNode questions = parseJsonArray ( content . getStepOneQuestions ( ) ) ;
JsonNode choiceAnswers = parseChoiceAnswers ( answer = = null ? null : answer . getStep1Answer ( ) ) ;
for ( JsonNode question : questions ) {
ObjectNode entry = result . addObject ( ) ;
entry . put ( "title" , StringUtils . abbreviate ( question . path ( "title" ) . asText ( ) , MAX_CHOICE_TITLE_LENGTH ) ) ;
ArrayNode options = entry . putArray ( "options" ) ;
for ( JsonNode option : question . path ( "options" ) ) {
ObjectNode item = options . addObject ( ) ;
item . put ( "key" , option . path ( "key" ) . asText ( ) ) ;
item . put ( "content" , StringUtils . abbreviate ( option . path ( "content" ) . asText ( ) , MAX_CHOICE_OPTION_LENGTH ) ) ;
}
entry . put ( "correctAnswer" , question . path ( "answer" ) . asText ( ) ) ;
entry . put ( "studentAnswer" , choiceAnswers . path ( question . path ( "id" ) . asText ( ) ) . asText ( "" ) ) ;
}
return json ( result ) ;
}
private int choiceQuestionEntryCount ( String choiceQuestionContext ) {
try {
JsonNode node = objectMapper . readTree ( StringUtils . trimToEmpty ( choiceQuestionContext ) ) ;
return node . isArray ( ) ? node . size ( ) : 0 ;
} catch ( JsonProcessingException | RuntimeException e ) {
return 0 ;
}
}
private JsonNode parseJsonArray ( String value ) {
try {
String normalized = StringUtils . trimToEmpty ( value ) ;
if ( ! normalized . startsWith ( "[" ) ) return objectMapper . createArrayNode ( ) ;
JsonNode node = objectMapper . readTree ( normalized ) ;
return node ! = null & & node . isArray ( ) ? node : objectMapper . createArrayNode ( ) ;
} catch ( JsonProcessingException | RuntimeException e ) {
return objectMapper . createArrayNode ( ) ;
}
}
private JsonNode parseChoiceAnswers ( String step1Answer ) {
try {
if ( StringUtils . isBlank ( step1Answer ) ) return objectMapper . createObjectNode ( ) ;
JsonNode choiceAnswers = objectMapper . readTree ( step1Answer ) . path ( "choiceAnswers" ) ;
return choiceAnswers . isObject ( ) ? choiceAnswers : objectMapper . createObjectNode ( ) ;
} catch ( JsonProcessingException | RuntimeException e ) {
return objectMapper . createObjectNode ( ) ;
}
}
private String helpAnswerSnapshot ( String question , String stepContext , StudentTrainingAnswer answer ) {
private String helpAnswerSnapshot ( String question , String stepContext , StudentTrainingAnswer answer ) {
ObjectNode node = objectMapper . createObjectNode ( ) ;
ObjectNode node = objectMapper . createObjectNode ( ) ;
node . put ( "studentQuestion" , question ) ;
node . put ( "studentQuestion" , question ) ;
@ -359,19 +422,22 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
}
}
requireText ( criterion , "name" ) ;
requireText ( criterion , "name" ) ;
requireText ( criterion , "rationale" ) ;
requireText ( criterion , "rationale" ) ;
Integer max = wholeNumber ( criterion . get ( "maxScore" ) ) ;
Integer earned = wholeNumber ( criterion . get ( "score" ) ) ;
Integer earned = wholeNumber ( criterion . get ( "score" ) ) ;
// maxScore 以评分计划为准:模型偶发把 maxScore 抄错导致合计≠100,
// 不再校验模型返回的 maxScore, 只用计划值判定得分范围。
if ( ! normalizeCriterionName ( expected . name ) . equals ( normalizeCriterionName ( criterion . path ( "name" ) . asText ( ) ) )
if ( ! normalizeCriterionName ( expected . name ) . equals ( normalizeCriterionName ( criterion . path ( "name" ) . asText ( ) ) )
| | max = = null | | earned = = null | | max ! = expected . maxScore | | earned < 0 | | earned > max ) {
| | earned = = null | | earned < 0 | | earned > expected . maxScore ) {
throw invalidReport ( ) ;
throw invalidReport ( ) ;
}
}
maxTotal + = max;
maxTotal + = expected. maxScore ;
earnedTotal + = earned ;
earnedTotal + = earned ;
}
}
if ( maxTotal ! = 100 | | earnedTotal ! = score ) {
// 模型偶发把总分写错(与逐项得分之和不一致):以逐项得分为准并回写报告,
throw invalidReport ( ) ;
// 避免仅因算术错误整份作废。
if ( earnedTotal ! = score ) {
( ( ObjectNode ) root ) . put ( "score" , earnedTotal ) ;
}
}
return score ;
return earnedTotal ;
}
}
private List < AssessmentCriterionPlan > assessmentCriterionPlan ( String steps ) {
private List < AssessmentCriterionPlan > assessmentCriterionPlan ( String steps ) {
@ -527,6 +593,8 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
+ "Return exactly " + criterionPlan . size ( ) + " criteria in this exact order. "
+ "Return exactly " + criterionPlan . size ( ) + " criteria in this exact order. "
+ "Each criterion's name must be copied verbatim from the quoted name in this required step plan, without adding any numbering or other characters; maxScore must exactly match: "
+ "Each criterion's name must be copied verbatim from the quoted name in this required step plan, without adding any numbering or other characters; maxScore must exactly match: "
+ criterionPlanPrompt ( criterionPlan ) + ". "
+ criterionPlanPrompt ( criterionPlan ) + ". "
+ "The user prompt may include a choiceQuestions array: grade each pick strictly against its correctAnswer; "
+ "every wrong or unanswered pick must lose points within the first criterion's score, and that criterion's rationale must name the wrong question numbers. "
+ "score, maxScore, and each criterion score must be JSON integer numbers (not strings). "
+ "score, maxScore, and each criterion score must be JSON integer numbers (not strings). "
+ "Write one complete concise Chinese sentence for each rationale and end it with Chinese punctuation. "
+ "Write one complete concise Chinese sentence for each rationale and end it with Chinese punctuation. "
+ "The combined text length of all criteria.rationale values must not exceed 200 Chinese characters; target no more than "
+ "The combined text length of all criteria.rationale values must not exceed 200 Chinese characters; target no more than "
@ -544,9 +612,12 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ
return result . append ( "]" ) . toString ( ) ;
return result . append ( "]" ) . toString ( ) ;
}
}
private String assessmentUserPrompt ( String taskSnapshot , String answerSnapshot , String knowledgeContext ) {
private String assessmentUserPrompt ( String taskSnapshot , String answerSnapshot , String knowledgeContext ,
String choiceQuestionContext ) {
return "Task snapshot: " + taskSnapshot + "\nStudent answer snapshot: " + answerSnapshot
return "Task snapshot: " + taskSnapshot + "\nStudent answer snapshot: " + answerSnapshot
+ "\nResolved case material and this student's uploaded-file excerpts: " + knowledgeContext
+ "\nResolved case material and this student's uploaded-file excerpts: " + knowledgeContext
+ "\nchoiceQuestions (step-one single-choice questions): " + choiceQuestionContext
+ " Each entry pairs a question's title and options with its correctAnswer and this student's studentAnswer; an empty studentAnswer means the question was not answered."
+ "\nIf the answer snapshot contains a server-generated calculationCheck object, treat it as verified calculation evidence. "
+ "\nIf the answer snapshot contains a server-generated calculationCheck object, treat it as verified calculation evidence. "
+ "When calculationCorrect is false, deduct points for the reported incorrectItemCount rather than treating the calculation as correct." ;
+ "When calculationCorrect is false, deduct points for the reported incorrectItemCount rather than treating the calculation as correct." ;
}
}