fix(复习):AI 对比结果由 Java 端标注标准树,不再依赖 LLM 输出树结构

根因:LLM 无法可靠输出 CompareResult 所需的完整嵌套树 JSON,
实际返回格式(coverage/matched/missed)与预期不符。

修复:
- AiServiceClient.compareRecall() 返回 Optional<JsonNode>(原始 JSON),
  不再尝试直接反序列化为 CompareResult
- StandardMindMapServiceImpl 新增 buildCompareResultFromAI():
  解析 AI 返回的平铺 matches/missedTitles/extraNodes 列表,
  将 MATCHED|/MISSED| 标注写入标准树,确定性构建 CompareResult
- AI 只负责语义匹配判断,树结构操作由 Java 端保证可靠性
This commit is contained in:
2026-07-04 16:09:07 +08:00
parent 7ac7868511
commit af4f536980
2 changed files with 77 additions and 7 deletions
@@ -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.<TaskEntity>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 标注回标准树。
* <p>AI 只输出哪些节点匹配/遗漏,树结构标注由本方法确定性完成,避免 LLM 输出不可靠的嵌套 JSON。</p>
*/
private CompareResult buildCompareResultFromAI(JsonNode aiJson, MindMapNode standardRoot) {
CompareResult result = new CompareResult();
// 1. 读取 AI 返回的匹配对 → 构建 standardNorm → matchFlag
Set<String> 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<String> 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<MindMapNode> 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<CompareResult.FlatNode> 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