diff --git a/refine-domain/src/main/java/com/achobeta/domain/rag/service/impl/UserMistakeEventListener.java b/refine-domain/src/main/java/com/achobeta/domain/rag/service/impl/UserMistakeEventListener.java new file mode 100644 index 0000000..601adca --- /dev/null +++ b/refine-domain/src/main/java/com/achobeta/domain/rag/service/impl/UserMistakeEventListener.java @@ -0,0 +1,142 @@ +package com.achobeta.domain.rag.service.impl; + +import com.achobeta.domain.rag.service.IVectorService; +import com.achobeta.types.event.UserMistakeEvent; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.Map; + +/** + * 用户错误行为事件监听器 + * 负责将用户的错误行为异步写入向量库,用于后续的学习动态追踪 + */ +@Slf4j +@Service +public class UserMistakeEventListener { + + @Autowired + @Qualifier("weaviateVectorRepository") + private IVectorService vectorService; + + /** + * 监听用户错误行为事件,异步处理 + */ + @Async + @EventListener + public void handleUserMistakeEvent(UserMistakeEvent event) { + try { + log.info("处理用户错误行为事件,userId: {}, mistakeType: {}, sessionId: {}", + event.getUserId(), event.getMistakeType().getCode(), event.getSessionId()); + + // 构建向量数据 + Map vectorData = buildVectorData(event); + + // 生成唯一的向量ID + String vectorId = generateVectorId(event); + + // 异步写入向量库,使用统一的行为类型 + boolean success = vectorService.storeLearningVector( + event.getUserId(), + vectorId, // 使用vectorId作为questionId + event.getQuestionContent() != null ? event.getQuestionContent() : "AI求助", + "qa", // 统一使用qa作为行为类型 + event.getSubject(), + null // knowledgePointId暂时为null + ); + + if (success) { + log.info("用户错误行为已成功写入向量库,userId: {}, vectorId: {}", event.getUserId(), vectorId); + } else { + log.warn("向量库写入失败,userId: {}, vectorId: {}", event.getUserId(), vectorId); + } + + log.info("用户错误行为已写入向量库,userId: {}, vectorId: {}", event.getUserId(), vectorId); + + } catch (Exception e) { + log.error("处理用户错误行为事件失败,userId: {}, sessionId: {}", + event.getUserId(), event.getSessionId(), e); + } + } + + /** + * 构建向量数据 + */ + private Map buildVectorData(UserMistakeEvent event) { + Map data = new HashMap<>(); + + // 基本信息 + data.put("userId", event.getUserId()); + data.put("mistakeType", event.getMistakeType().getCode()); + data.put("mistakeDescription", event.getMistakeType().getDescription()); + data.put("sessionId", event.getSessionId()); + + // 时间信息 + data.put("eventTime", event.getEventTime().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); + data.put("timestamp", System.currentTimeMillis()); + + // 学习内容信息 + if (event.getQuestionContent() != null) { + data.put("questionContent", event.getQuestionContent()); + data.put("questionLength", event.getQuestionContent().length()); + } + + if (event.getSubject() != null) { + data.put("subject", event.getSubject()); + } + + if (event.getKnowledgePoint() != null) { + data.put("knowledgePoint", event.getKnowledgePoint()); + } + + // 错误详情 + if (event.getMistakeDescription() != null) { + data.put("detailedDescription", event.getMistakeDescription()); + } + + // 上下文信息 + if (event.getContextInfo() != null) { + data.put("contextInfo", event.getContextInfo()); + } + + // 行为分类 + data.put("behaviorCategory", "mistake"); + data.put("learningPhase", determineLearningPhase(event)); + + return data; + } + + /** + * 生成向量ID + */ + private String generateVectorId(UserMistakeEvent event) { + return String.format("mistake_%s_%s_%d", + event.getUserId(), + event.getSessionId(), + System.currentTimeMillis()); + } + + /** + * 判断学习阶段 + */ + private String determineLearningPhase(UserMistakeEvent event) { + switch (event.getMistakeType()) { + case AI_HELP_REQUEST: + return "seeking_help"; + case CONCEPT_MISUNDERSTANDING: + return "concept_learning"; + case CALCULATION_ERROR: + return "practice"; + case METHOD_ERROR: + return "problem_solving"; + default: + return "unknown"; + } + } +} \ No newline at end of file diff --git a/refine-trigger/src/main/java/com/achobeta/trigger/http/AiSolveController.java b/refine-trigger/src/main/java/com/achobeta/trigger/http/AiSolveController.java index 49c30b8..084d7e0 100644 --- a/refine-trigger/src/main/java/com/achobeta/trigger/http/AiSolveController.java +++ b/refine-trigger/src/main/java/com/achobeta/trigger/http/AiSolveController.java @@ -2,14 +2,20 @@ import com.achobeta.api.dto.AiSolveRequestDTO; import com.achobeta.domain.ai.service.IAiService; +import com.achobeta.types.annotation.GlobalInterception; +import com.achobeta.types.common.UserContext; +import com.achobeta.types.event.UserMistakeEvent; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.io.IOException; +import java.time.LocalDateTime; +import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -27,10 +33,18 @@ public class AiSolveController { private final IAiService aiService; + private final ApplicationEventPublisher eventPublisher; + @GlobalInterception @PostMapping("stream") public SseEmitter stream(@Valid @RequestBody AiSolveRequestDTO requestDTO) { String question = requestDTO.getQuestion(); + String userId = UserContext.getUserId(); + String sessionId = UUID.randomUUID().toString(); + + // 记录用户求助AI的行为 + publishMistakeEvent(userId, question, sessionId); + // 设置超时时间为5分钟 SseEmitter emitter = new SseEmitter(300000L); @@ -121,5 +135,28 @@ public SseEmitter stream(@Valid @RequestBody AiSolveRequestDTO requestDTO) { return emitter; } + /** + * 发布用户错误行为事件 + */ + private void publishMistakeEvent(String userId, String question, String sessionId) { + try { + UserMistakeEvent mistakeEvent = UserMistakeEvent.builder() + .userId(userId) + .mistakeType(UserMistakeEvent.MistakeType.AI_HELP_REQUEST) + .questionContent(question) + .mistakeDescription("用户向AI求助解题,表明遇到了不会的问题") + .subject("AI答疑") // 设置科目 + .eventTime(LocalDateTime.now()) + .sessionId(sessionId) + .contextInfo("AI答疑接口调用") + .build(); + + log.info("发布用户错误行为事件,userId: {}, sessionId: {}", userId, sessionId); + eventPublisher.publishEvent(mistakeEvent); + + } catch (Exception e) { + log.error("发布用户错误行为事件失败,userId: {}", userId, e); + } + } } diff --git a/refine-types/src/main/java/com/achobeta/types/event/UserMistakeEvent.java b/refine-types/src/main/java/com/achobeta/types/event/UserMistakeEvent.java new file mode 100644 index 0000000..7946915 --- /dev/null +++ b/refine-types/src/main/java/com/achobeta/types/event/UserMistakeEvent.java @@ -0,0 +1,110 @@ +package com.achobeta.types.event; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 用户错误行为事件 + * 用于追踪用户在学习过程中遇到的问题和错误 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserMistakeEvent { + + /** + * 用户ID + */ + private String userId; + + /** + * 错误类型 + */ + private MistakeType mistakeType; + + /** + * 问题内容 + */ + private String questionContent; + + /** + * 错误描述 + */ + private String mistakeDescription; + + /** + * 相关科目 + */ + private String subject; + + /** + * 知识点 + */ + private String knowledgePoint; + + /** + * 事件发生时间 + */ + private LocalDateTime eventTime; + + /** + * 会话ID(用于关联同一次学习会话) + */ + private String sessionId; + + /** + * 额外的上下文信息 + */ + private String contextInfo; + + /** + * 错误类型枚举 + */ + public enum MistakeType { + /** + * 求助AI - 表示用户遇到不会的题目需要AI帮助 + */ + AI_HELP_REQUEST("ai_help", "AI求助"), + + /** + * 概念理解错误 + */ + CONCEPT_MISUNDERSTANDING("concept_error", "概念理解错误"), + + /** + * 计算错误 + */ + CALCULATION_ERROR("calc_error", "计算错误"), + + /** + * 方法选择错误 + */ + METHOD_ERROR("method_error", "方法选择错误"), + + /** + * 其他错误 + */ + OTHER("other", "其他错误"); + + private final String code; + private final String description; + + MistakeType(String code, String description) { + this.code = code; + this.description = description; + } + + public String getCode() { + return code; + } + + public String getDescription() { + return description; + } + } +} \ No newline at end of file