diff --git a/src/main/java/com/guo/learningprogresstracker/service/impl/AiServiceClient.java b/src/main/java/com/guo/learningprogresstracker/service/impl/AiServiceClient.java index 729694f..d3ebcaf 100644 --- a/src/main/java/com/guo/learningprogresstracker/service/impl/AiServiceClient.java +++ b/src/main/java/com/guo/learningprogresstracker/service/impl/AiServiceClient.java @@ -2,7 +2,6 @@ package com.guo.learningprogresstracker.service.impl; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.guo.learningprogresstracker.utils.CompareResult; import jakarta.annotation.PostConstruct; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @@ -133,10 +132,12 @@ public class AiServiceClient { /** * 调用 lpt-ai 进行语义化回忆对比。 + * 注意:AI 返回平铺的匹配列表,不返回完整树结构。 + * 树标注(MATCHED/MISSED)由 StandardMindMapServiceImpl 基于本方法返回的 JSON 完成。 * - * @return 带 MATCHED/MISSED 标注和评价文本的对比结果;服务未配置或调用失败时返回 empty + * @return AI 返回的原始 JSON;服务未配置或调用失败时返回 empty */ - public Optional compareRecall(String taskName, String standardOutline, String recallOutline) { + public Optional compareRecall(String taskName, String standardOutline, String recallOutline) { if (!isConfigured()) { return Optional.empty(); } @@ -160,7 +161,7 @@ public class AiServiceClient { response.body() != null ? response.body().substring(0, Math.min(200, response.body().length())) : ""); return Optional.empty(); } - return Optional.of(objectMapper.readValue(response.body(), CompareResult.class)); + return Optional.of(objectMapper.readTree(response.body())); } catch (Exception e) { log.warn("lpt-ai 回忆对比调用异常,将降级为内置算法: {}", e.getMessage()); return Optional.empty(); diff --git a/src/main/java/com/guo/learningprogresstracker/service/impl/StandardMindMapServiceImpl.java b/src/main/java/com/guo/learningprogresstracker/service/impl/StandardMindMapServiceImpl.java index 01669c8..5877af4 100644 --- a/src/main/java/com/guo/learningprogresstracker/service/impl/StandardMindMapServiceImpl.java +++ b/src/main/java/com/guo/learningprogresstracker/service/impl/StandardMindMapServiceImpl.java @@ -1,6 +1,7 @@ package com.guo.learningprogresstracker.service.impl; import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.guo.learningprogresstracker.entity.*; import com.guo.learningprogresstracker.exception.NotFindEntitiesException; @@ -112,13 +113,13 @@ public class StandardMindMapServiceImpl implements StandardMindMapService { String taskName = tasksMapper.selectOne( Wrappers.lambdaQuery().eq(TaskEntity::getTaskNum, taskNum).last("LIMIT 1")) .getTaskName(); - var optResult = aiServiceClient.compareRecall( + var optJson = aiServiceClient.compareRecall( taskName, MindMapTreeTool.toFullOutline(standardRoot), recallOutline ); - if (optResult.isPresent()) { - result = optResult.get(); + if (optJson.isPresent()) { + result = buildCompareResultFromAI(optJson.get(), standardRoot); log.info("AI 回忆对比: taskNum={}, recallRatio={}, evaluation={}", taskNum, result.getRecallRatio(), result.getEvaluation() != null ? result.getEvaluation().substring(0, Math.min(50, result.getEvaluation().length())) : ""); @@ -225,6 +226,74 @@ public class StandardMindMapServiceImpl implements StandardMindMapService { return result; } + /** + * 从 AI 返回的平铺匹配列表构建 CompareResult,并将 MATCHED/MISSED 标注回标准树。 + *

AI 只输出哪些节点匹配/遗漏,树结构标注由本方法确定性完成,避免 LLM 输出不可靠的嵌套 JSON。

+ */ + private CompareResult buildCompareResultFromAI(JsonNode aiJson, MindMapNode standardRoot) { + CompareResult result = new CompareResult(); + + // 1. 读取 AI 返回的匹配对 → 构建 standardNorm → matchFlag + Set matchedStandardTitles = new HashSet<>(); + JsonNode matchesArr = aiJson.path("matches"); + if (matchesArr.isArray()) { + for (JsonNode m : matchesArr) { + String stdTitle = m.path("standardTitle").asText(null); + if (stdTitle != null) { + matchedStandardTitles.add(normalize(stdTitle)); + } + } + } + + // 2. 读取遗漏列表 + Set missedTitles = new HashSet<>(); + JsonNode missedArr = aiJson.path("missedTitles"); + if (missedArr.isArray()) { + for (JsonNode t : missedArr) { + String title = t.asText(null); + if (title != null) missedTitles.add(normalize(title)); + } + } + + // 3. 标注标准树的每个非根节点 + List standardFlat = MindMapTreeTool.flatten(standardRoot); + int matched = 0, missed = 0; + for (MindMapNode node : standardFlat) { + if (node == standardRoot) continue; + String key = normalize(node.getTitle()); + if (matchedStandardTitles.contains(key)) { + node.setNotes(MATCH_STATUS_MATCHED + "|" + (node.getNotes() != null ? node.getNotes() : "")); + matched++; + } else { + node.setNotes(MATCH_STATUS_MISSED + "|" + (node.getNotes() != null ? node.getNotes() : "")); + missed++; + } + } + + // 4. 读取 extraNodes + List extras = new ArrayList<>(); + JsonNode extrasArr = aiJson.path("extraNodes"); + if (extrasArr.isArray()) { + for (JsonNode e : extrasArr) { + CompareResult.FlatNode fn = new CompareResult.FlatNode(); + fn.setTitle(e.path("title").asText("")); + fn.setPath(e.path("path").asText("")); + extras.add(fn); + } + } + + // 5. 组装结果 + result.setMatchedTree(standardRoot); + result.setExtraNodes(extras); + result.setMatchedCount(matched); + result.setMissedCount(missed); + result.setExtraCount(extras.size()); + int total = matched + missed; + result.setRecallRatio(total > 0 ? (double) matched / total : 0); + result.setEvaluation(aiJson.path("evaluation").asText(null)); + return result; + } + // ============ 回忆记录查询 ============ @Override