feat(复习):回忆对比优先走 AI 语义对比,失败时降级为内置算法

- CompareResult.java:新增 evaluation 字段(AI 对比时写入评价文本)
- AiServiceClient.java:新增 compareRecall(),调用 lpt-ai /ai/compare-recall
- StandardMindMapServiceImpl.java:recallCompare() 中注入 AiServiceClient,
  优先 try AI 语义对比,含层级上下文和同义表达识别;
  AI 不可用或失败时自动 fallback 到内置标题匹配算法
- StandardMindMapServiceImplTest.java:适配新增的 AiServiceClient 依赖
This commit is contained in:
2026-07-04 15:47:24 +08:00
parent cd5ee0b404
commit 7ac7868511
4 changed files with 66 additions and 3 deletions
@@ -2,6 +2,7 @@ package com.guo.learningprogresstracker.service.impl;
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.guo.learningprogresstracker.utils.CompareResult;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import lombok.Setter; import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -129,4 +130,40 @@ public class AiServiceClient {
return Optional.empty(); return Optional.empty();
} }
} }
/**
* 调用 lpt-ai 进行语义化回忆对比。
*
* @return 带 MATCHED/MISSED 标注和评价文本的对比结果;服务未配置或调用失败时返回 empty
*/
public Optional<CompareResult> compareRecall(String taskName, String standardOutline, String recallOutline) {
if (!isConfigured()) {
return Optional.empty();
}
try {
Map<String, String> body = Map.of(
"taskName", taskName,
"standardOutline", standardOutline,
"recallOutline", recallOutline
);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url + "/ai/compare-recall"))
.timeout(Duration.ofSeconds(timeoutSeconds))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body)))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
log.warn("lpt-ai 回忆对比失败: status={}, body={}", response.statusCode(),
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));
} catch (Exception e) {
log.warn("lpt-ai 回忆对比调用异常,将降级为内置算法: {}", e.getMessage());
return Optional.empty();
}
}
} }
@@ -39,6 +39,7 @@ public class StandardMindMapServiceImpl implements StandardMindMapService {
private final List<MindMapAiClient> aiClients; private final List<MindMapAiClient> aiClients;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final AiServiceClient aiServiceClient;
private static final String GENERATOR_BUILTIN = "BUILTIN"; private static final String GENERATOR_BUILTIN = "BUILTIN";
private static final String GENERATOR_USER = "USER"; private static final String GENERATOR_USER = "USER";
@@ -105,8 +106,28 @@ public class StandardMindMapServiceImpl implements StandardMindMapService {
MindMapNode standardRoot = MindMapTreeTool.fromJson(standard.getContent(), objectMapper); MindMapNode standardRoot = MindMapTreeTool.fromJson(standard.getContent(), objectMapper);
MindMapNode recallRoot = MindMapTreeTool.parseOutline(recallOutline); MindMapNode recallRoot = MindMapTreeTool.parseOutline(recallOutline);
// 3. 执行对比 // 3. 执行对比(优先 AI 语义对比,失败时降级为内置算法)
CompareResult result = compareTrees(standardRoot, recallRoot); CompareResult result;
if (aiServiceClient.isConfigured()) {
String taskName = tasksMapper.selectOne(
Wrappers.<TaskEntity>lambdaQuery().eq(TaskEntity::getTaskNum, taskNum).last("LIMIT 1"))
.getTaskName();
var optResult = aiServiceClient.compareRecall(
taskName,
MindMapTreeTool.toFullOutline(standardRoot),
recallOutline
);
if (optResult.isPresent()) {
result = optResult.get();
log.info("AI 回忆对比: taskNum={}, recallRatio={}, evaluation={}",
taskNum, result.getRecallRatio(),
result.getEvaluation() != null ? result.getEvaluation().substring(0, Math.min(50, result.getEvaluation().length())) : "");
} else {
result = compareTrees(standardRoot, recallRoot);
}
} else {
result = compareTrees(standardRoot, recallRoot);
}
// 4. 序列化对比结果 // 4. 序列化对比结果
String resultJson; String resultJson;
@@ -32,6 +32,9 @@ public class CompareResult {
/** 额外节点数 */ /** 额外节点数 */
private int extraCount; private int extraCount;
/** AI 评价文本(仅 AI 对比时有值,内置对比为 null) */
private String evaluation;
@Data @Data
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@@ -45,6 +45,8 @@ class StandardMindMapServiceImplTest {
private List<MindMapAiClient> aiClients; private List<MindMapAiClient> aiClients;
@Mock @Mock
private MindMapAiClient mockAiClient; private MindMapAiClient mockAiClient;
@Mock
private AiServiceClient aiServiceClient;
private final ObjectMapper objectMapper = new ObjectMapper(); private final ObjectMapper objectMapper = new ObjectMapper();
@@ -189,7 +191,7 @@ class StandardMindMapServiceImplTest {
standardMindMapMapper, recallRecordMapper, standardMindMapMapper, recallRecordMapper,
tasksMapper, studyReportsMapper, studyReportFragmentsMapper, tasksMapper, studyReportsMapper, studyReportFragmentsMapper,
taskApplicationMapper, studySessionsMapper, taskApplicationMapper, studySessionsMapper,
List.of(mockAiClient), objectMapper); List.of(mockAiClient), objectMapper, aiServiceClient);
return s; return s;
} }
} }