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.ObjectMapper;
import com.guo.learningprogresstracker.utils.CompareResult;
import jakarta.annotation.PostConstruct;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
@@ -129,4 +130,40 @@ public class AiServiceClient {
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();
}
}
}