From f0d0222c339d8a61f0076763ea05e67907f60169 Mon Sep 17 00:00:00 2001
From: malog <2153315236@qq.com>
Date: Mon, 1 Dec 2025 15:11:53 +0800
Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=E8=BF=81=E7=A7=BB=E7=94=A8?=
=?UTF-8?q?=E6=88=B7=E6=95=B0=E6=8D=AE=E8=B7=9F=E8=B8=AA=E5=88=86=E6=9E=90?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
pom.xml | 18 +-
refine-app/pom.xml | 6 +-
.../java/com/achobeta/config/RagConfig.java | 161 +----
.../achobeta/config/RagConfigProperties.java | 18 +-
.../com/achobeta/config/WeaviateConfig.java | 86 +++
.../ocr/service/impl/DefaultOcrService.java | 30 +-
.../service/impl/LearningAnalysisService.java | 12 +-
.../impl/LearningDynamicsServiceImpl.java | 179 +++++-
.../domain/user/event/UserBehaviorEvent.java | 32 +
.../domain/user/event/UserMistakeEvent.java | 14 +
.../domain/user/event/UserReviewEvent.java | 14 +
.../domain/user/event/UserUploadEvent.java | 14 +
refine-infrastructure/pom.xml | 4 +
.../repository/LearningDataRepository.java | 338 +++++++++--
.../repository/WeaviateSchemaInitializer.java | 156 +++++
.../repository/WeaviateVectorRepository.java | 573 ++++++++++++++++++
.../event/UserBehaviorEventListener.java | 162 +++++
.../http/LearningAnalysisController.java | 2 +
.../achobeta/trigger/http/OcrController.java | 2 -
19 files changed, 1573 insertions(+), 248 deletions(-)
create mode 100644 refine-app/src/main/java/com/achobeta/config/WeaviateConfig.java
create mode 100644 refine-domain/src/main/java/com/achobeta/domain/user/event/UserBehaviorEvent.java
create mode 100644 refine-domain/src/main/java/com/achobeta/domain/user/event/UserMistakeEvent.java
create mode 100644 refine-domain/src/main/java/com/achobeta/domain/user/event/UserReviewEvent.java
create mode 100644 refine-domain/src/main/java/com/achobeta/domain/user/event/UserUploadEvent.java
create mode 100644 refine-infrastructure/src/main/java/com/achobeta/infrastructure/adapter/repository/WeaviateSchemaInitializer.java
create mode 100644 refine-infrastructure/src/main/java/com/achobeta/infrastructure/adapter/repository/WeaviateVectorRepository.java
create mode 100644 refine-trigger/src/main/java/com/achobeta/trigger/event/UserBehaviorEventListener.java
diff --git a/pom.xml b/pom.xml
index bdebeaf..29ce182 100644
--- a/pom.xml
+++ b/pom.xml
@@ -76,11 +76,11 @@
postgresql
42.7.1
-
+
- com.pgvector
- pgvector
- 0.1.4
+ io.weaviate
+ client
+ 4.8.0
@@ -181,7 +181,7 @@
org.apache.commons
commons-lang3
- 3.9
+ 3.12.0
com.google.guava
@@ -235,14 +235,6 @@
langchain4j-reactor
${langchain4j.version}
-
-
- dev.langchain4j
- langchain4j-pgvector
- ${langchain4j.version}
-
-
-
com.achobeta
diff --git a/refine-app/pom.xml b/refine-app/pom.xml
index 94e2d03..02bf62e 100644
--- a/refine-app/pom.xml
+++ b/refine-app/pom.xml
@@ -113,10 +113,10 @@
dev.langchain4j
langchain4j
-
+
- dev.langchain4j
- langchain4j-pgvector
+ io.weaviate
+ client
diff --git a/refine-app/src/main/java/com/achobeta/config/RagConfig.java b/refine-app/src/main/java/com/achobeta/config/RagConfig.java
index 9f7d305..6cd9ebf 100644
--- a/refine-app/src/main/java/com/achobeta/config/RagConfig.java
+++ b/refine-app/src/main/java/com/achobeta/config/RagConfig.java
@@ -15,7 +15,6 @@
import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
import dev.langchain4j.store.embedding.filter.Filter;
import dev.langchain4j.store.embedding.filter.MetadataFilterBuilder;
-import dev.langchain4j.store.embedding.pgvector.PgVectorEmbeddingStore;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
@@ -24,10 +23,6 @@
import org.springframework.context.annotation.Configuration;
import java.io.File;
-import java.sql.Connection;
-import java.sql.DriverManager;
-import java.sql.PreparedStatement;
-import java.sql.SQLException;
import java.util.List;
import java.util.stream.Collectors;
@@ -46,21 +41,22 @@ public class RagConfig {
@PostConstruct
public void initialize() {
- this.embeddingStore = PgVectorEmbeddingStore.builder()
- .host(ragConfigProperties.getHost())
- .port(ragConfigProperties.getPort())
- .database(ragConfigProperties.getDatabase())
- .user(ragConfigProperties.getUser())
- .password(ragConfigProperties.getPassword())
- .table("knowledge_embeddings")
- .dimension(qwenEmbeddingModel.dimension())
- .build();
- log.info("EmbeddingStore初始化完成");
+ try {
+ log.info("开始初始化Weaviate EmbeddingStore");
+
+ // 暂时跳过EmbeddingStore初始化,因为我们使用自定义的向量存储
+ log.info("RagConfig初始化完成,使用自定义向量存储服务");
+
+ } catch (Exception e) {
+ log.error("RagConfig初始化失败", e);
+ }
}
@Bean
public EmbeddingStore embeddingStore() {
- return this.embeddingStore;
+ // 返回null,因为我们使用自定义的向量存储服务
+ log.info("使用自定义向量存储服务,不创建EmbeddingStore Bean");
+ return null;
}
@Bean
@@ -75,29 +71,8 @@ public ContentRetriever contentRetriever() {
// 获取目录下的文件(排除子目录,只考虑文件)
File[] files = docDir.listFiles(File::isFile);
if (files != null && files.length > 0) {
- // 先清空向量表的所有旧数据
- truncateEmbeddingTable(); // 全删逻辑(替换原来的按文件名删)
-
- // 加载文档并重新入库
- List documents = FileSystemDocumentLoader.loadDocuments(docPath);
- log.info("待录入文档总数:{} 个,开始分段并生成向量", documents.size());
-
-
- // 文档分段
- DocumentByParagraphSplitter documentSplitter = new DocumentByParagraphSplitter(800, 350);
- // 自定义文档加载器,把文档转换成向量并存储到向量数据库中
- EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
- .documentSplitter(documentSplitter)
- .textSegmentTransformer(textSegment -> TextSegment.from(
- textSegment.metadata().getString("file_name") + "\n" + textSegment.text(),
- textSegment.metadata()
- ))
- .embeddingModel(qwenEmbeddingModel)
- .embeddingStore(embeddingStore)
- .build();
- // 加载文档
- ingestor.ingest(documents);
- log.info("文档全量入库完成! 共加载 {} 个文件,已清空旧数据,当前表中为最新全量数据", files.length);
+ // TODO: 使用Weaviate实现文档向量化和存储
+ log.warn("RagConfig暂未实现Weaviate文档向量化,跳过文档加载");
} else {
log.info("文档目录 {} 下无可用文件,跳过文档加载(表中数据保持不变)", docPath);
}
@@ -109,100 +84,30 @@ public ContentRetriever contentRetriever() {
return new ContentRetriever() {
@Override
public List retrieve(Query query) {
- // 1. 从查询元数据中动态提取业务传入的学科(target)
- Object obj = query.metadata().invocationContext().chatMemoryId();
- if (null == obj) {
- log.warn("RAG检索 - 未传递 file_name 筛选条件,返回空结果");
+ try {
+ log.info("ContentRetriever收到查询请求: {}", query.text());
+
+ // 暂时返回空结果,因为我们主要关注学习行为记录
+ // 后续可以集成Weaviate进行内容检索
+ log.info("ContentRetriever暂时返回空结果,专注于学习行为记录功能");
+ return List.of();
+
+ } catch (Exception e) {
+ log.error("ContentRetriever查询失败", e);
return List.of();
}
- String target = String.valueOf(obj);
- log.info("RAG检索 - 动态过滤 file_name(模糊匹配): {}", target);
-
- // 2. 生成查询向量
- Embedding embeddedQuery = qwenEmbeddingModel.embed(query.text()).content();
-
- // 3. 用自定义的Filter构建动态过滤条件
- //Filter filter = MetadataFilterBuilder.metadataKey("subject").isEqualTo(subject);
-
- //模糊匹配,自定义的Filter构建动态过滤条件
- Filter filter = MetadataFilterBuilder.metadataKey("file_name").containsString(target);
-
- // 4. 执行带动态过滤的检索
- EmbeddingSearchRequest searchRequest = EmbeddingSearchRequest.builder()
- .queryEmbedding(embeddedQuery)
- .maxResults(5) // 最多返回5条
- .minScore(0.75) // 相关度阈值
- .filter(filter) // 动态过滤条件
- .build();
-
- EmbeddingSearchResult searchResult = embeddingStore.search(searchRequest);
-
- // 5. 转换结果为Content列表
- return searchResult.matches().stream()
- .map(match -> Content.from(match.embedded()))
- .collect(Collectors.toList());
}
};
}
- private void truncateEmbeddingTable() {
- // 方案1:TRUNCATE(推荐)- 快速清空表,不记录单行删除日志,效率极高
- String sql = String.format("TRUNCATE TABLE %s CASCADE", "knowledge_embeddings");
- // CASCADE:若表有外键关联,同时清空关联表(无外键可去掉CASCADE)
-
- // 方案2:DELETE(备选,无TRUNCATE权限时使用)
- // String sql = String.format("DELETE FROM %s", VECTOR_TABLE);
-
- try (
- // 复用配置类的连接信息,避免硬编码
- Connection conn = DriverManager.getConnection(
- String.format("jdbc:postgresql://%s:%d/%s",
- ragConfigProperties.getHost(),
- ragConfigProperties.getPort(),
- ragConfigProperties.getDatabase()),
- ragConfigProperties.getUser(),
- ragConfigProperties.getPassword()
- );
- PreparedStatement pstmt = conn.prepareStatement(sql)
- ) {
- log.info("开始清空向量表:{}", "knowledge_embeddings");
- pstmt.executeUpdate();
- log.info("向量表 {} 清空成功!", "knowledge_embeddings");
-
- } catch (SQLException e) {
- // 若TRUNCATE失败(如无权限),自动降级为DELETE重试(可选逻辑)
- if (e.getMessage().contains("permission denied for table") || e.getMessage().contains("TRUNCATE")) {
- log.warn("TRUNCATE权限不足,尝试用DELETE清空表", e);
- deleteAllEmbeddingData(); // 调用DELETE全删方法
- return;
- }
-
- log.error("清空向量表 {} 失败", "knowledge_embeddings", e);
- throw new RuntimeException("清空旧数据异常,终止文档入库", e);
- }
- }
+ // 注释掉PgVector相关的表操作方法
+ // private void truncateEmbeddingTable() {
+ // // TODO: 使用Weaviate实现数据清理
+ // log.warn("RagConfig暂未实现Weaviate数据清理");
+ // }
- /**
- * 备选方法:用DELETE清空全表(无TRUNCATE权限时触发)
- */
- private void deleteAllEmbeddingData() {
- String sql = String.format("DELETE FROM %s", "knowledge_embeddings");
- try (
- Connection conn = DriverManager.getConnection(
- String.format("jdbc:postgresql://%s:%d/%s",
- ragConfigProperties.getHost(),
- ragConfigProperties.getPort(),
- ragConfigProperties.getDatabase()),
- ragConfigProperties.getUser(),
- ragConfigProperties.getPassword()
- );
- PreparedStatement pstmt = conn.prepareStatement(sql)
- ) {
- int deletedRows = pstmt.executeUpdate();
- log.info("用DELETE清空表 {} 成功,共删除 {} 条记录", "knowledge_embeddings", deletedRows);
- } catch (SQLException e) {
- log.error("DELETE清空表 {} 失败", "knowledge_embeddings", e);
- throw new RuntimeException("删除旧数据异常,终止文档入库", e);
- }
- }
+ // private void deleteAllEmbeddingData() {
+ // // TODO: 使用Weaviate实现数据删除
+ // log.warn("RagConfig暂未实现Weaviate数据删除");
+ // }
}
\ No newline at end of file
diff --git a/refine-app/src/main/java/com/achobeta/config/RagConfigProperties.java b/refine-app/src/main/java/com/achobeta/config/RagConfigProperties.java
index 6c84c24..1701596 100644
--- a/refine-app/src/main/java/com/achobeta/config/RagConfigProperties.java
+++ b/refine-app/src/main/java/com/achobeta/config/RagConfigProperties.java
@@ -4,20 +4,20 @@
import org.springframework.boot.context.properties.ConfigurationProperties;
@Data
-@ConfigurationProperties(prefix = "pgvector.config", ignoreInvalidFields = true)
+@ConfigurationProperties(prefix = "weaviate.config", ignoreInvalidFields = true)
public class RagConfigProperties {
/** host:ip */
private String host;
/** 端口 */
private int port;
- /** 数据库名 */
- private String database;
- /** 用户名 */
- private String user;
- /** 账密 */
- private String password;
- /** 向量维度 */
- //private String vectorDimension;
+ /** scheme */
+ private String scheme;
+ /** api-key */
+ private String apiKey;
+ /** timeout */
+ private int timeout;
+ /** class-name */
+ private String className;
}
diff --git a/refine-app/src/main/java/com/achobeta/config/WeaviateConfig.java b/refine-app/src/main/java/com/achobeta/config/WeaviateConfig.java
new file mode 100644
index 0000000..a46d72c
--- /dev/null
+++ b/refine-app/src/main/java/com/achobeta/config/WeaviateConfig.java
@@ -0,0 +1,86 @@
+package com.achobeta.config;
+
+import io.weaviate.client.Config;
+import io.weaviate.client.WeaviateAuthClient;
+import io.weaviate.client.WeaviateClient;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @Auth : Malog
+ * @Desc : Weaviate客户端配置
+ * @Time : 2025/11/28
+ */
+@Slf4j
+@Configuration
+public class WeaviateConfig {
+
+ @Value("${weaviate.config.host:localhost}")
+ private String host;
+
+ @Value("${weaviate.config.port:8080}")
+ private int port;
+
+ @Value("${weaviate.config.scheme:http}")
+ private String scheme;
+
+ @Value("${weaviate.config.api-key:}")
+ private String apiKey;
+
+ @Value("${weaviate.config.timeout:30000}")
+ private int timeout;
+
+ @Value("${weaviate.config.class-name:LearningVector}")
+ private String className;
+
+ /**
+ * 创建Weaviate客户端Bean
+ */
+ @Bean
+ public WeaviateClient weaviateClient() {
+ try {
+ // 构建连接URL - 对于云服务,不需要端口号
+ String url;
+ Config config;
+
+ if (port == 443 && "https".equals(scheme)) {
+ // Weaviate Cloud服务,不需要端口号
+ url = scheme + "://" + host;
+ config = new Config(scheme, host);
+ } else {
+ // 本地服务,需要端口号
+ url = scheme + "://" + host + ":" + port;
+ config = new Config(scheme, host + ":" + port);
+ }
+
+ WeaviateClient client;
+
+ // 如果配置了API Key,使用认证客户端
+ if (apiKey != null && !apiKey.trim().isEmpty()) {
+ log.info("使用API Key认证连接Weaviate");
+ client = WeaviateAuthClient.apiKey(config, apiKey);
+ } else {
+ // 无认证连接
+ log.info("使用无认证方式连接Weaviate");
+ client = new WeaviateClient(config);
+ }
+
+ log.info("Weaviate客户端初始化成功,连接地址: {}", url);
+ return client;
+
+ } catch (Exception e) {
+ log.error("Weaviate客户端初始化失败", e);
+ throw new RuntimeException("Failed to initialize Weaviate client", e);
+ }
+ }
+
+ /**
+ * 获取Weaviate类名配置
+ */
+ @Bean
+ public String weaviateClassName() {
+ return className;
+ }
+}
\ No newline at end of file
diff --git a/refine-domain/src/main/java/com/achobeta/domain/ocr/service/impl/DefaultOcrService.java b/refine-domain/src/main/java/com/achobeta/domain/ocr/service/impl/DefaultOcrService.java
index 08f4a33..069cfdd 100644
--- a/refine-domain/src/main/java/com/achobeta/domain/ocr/service/impl/DefaultOcrService.java
+++ b/refine-domain/src/main/java/com/achobeta/domain/ocr/service/impl/DefaultOcrService.java
@@ -7,10 +7,12 @@
import com.achobeta.domain.ocr.model.entity.QuestionEntity;
import com.achobeta.domain.ocr.service.IOcrService;
import com.achobeta.domain.rag.service.IVectorService;
-import com.achobeta.types.enums.ActionType;
+import com.achobeta.domain.user.event.UserUploadEvent;
import com.achobeta.types.exception.AppException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import java.io.IOException;
@@ -29,6 +31,9 @@ public class DefaultOcrService implements IOcrService {
private final IFilePreprocessPort filePreprocessPort;
private final IOcrPort ocrPort;
private final IAiService aiService;
+ private final ApplicationEventPublisher eventPublisher;
+
+ @Qualifier("weaviateVectorRepository")
private final IVectorService vectorService;
/**
@@ -122,26 +127,21 @@ else if (lowerType.endsWith(".txt") || "txt".equals(lowerType)) {
questionEntity.setQuestionId(uuid);
questionEntity.setUserId(userId);
- // 存储学习行为向量到向量数据库
+ // 发布用户上传事件,由事件监听器处理向量存储
try {
- // 调用向量服务存储学习向量,行为类型为"upload"表示用户上传题目
- boolean vectorStored = vectorService.storeLearningVector(
+ UserUploadEvent uploadEvent = new UserUploadEvent(
+ this,
userId,
uuid,
recognizedText,
- ActionType.UPLOAD.getActionType(),
- null,
- null
+ null, // subject 暂时为空,可以从AI分析中获取
+ null // knowledgePointId 暂时为空,可以从AI分析中获取
);
-
- if (vectorStored) {
- log.info("成功存储题目向量到向量数据库,userId:{} questionId:{}", userId, uuid);
- } else {
- log.warn("存储题目向量到向量数据库失败,但不影响OCR主流程,userId:{} questionId:{}", userId, uuid);
- }
+ eventPublisher.publishEvent(uploadEvent);
+ log.info("用户上传题目事件已发布,userId:{}, questionId:{}", userId, uuid);
} catch (Exception e) {
- // 向量存储失败不应该影响OCR主流程,只记录日志
- log.error("存储题目向量到向量数据库时发生异常,userId:{} questionId:{}", userId, uuid, e);
+ // 事件发布失败不应该影响OCR主流程,只记录日志
+ log.error("发布用户上传题目事件时发生异常,userId:{} questionId:{}", userId, uuid, e);
}
// TODO 将题干存储到Redis中,通过uuid可以查询,设置24小时过期时间
diff --git a/refine-domain/src/main/java/com/achobeta/domain/rag/service/impl/LearningAnalysisService.java b/refine-domain/src/main/java/com/achobeta/domain/rag/service/impl/LearningAnalysisService.java
index 544db96..b697c3e 100644
--- a/refine-domain/src/main/java/com/achobeta/domain/rag/service/impl/LearningAnalysisService.java
+++ b/refine-domain/src/main/java/com/achobeta/domain/rag/service/impl/LearningAnalysisService.java
@@ -7,6 +7,7 @@
import com.achobeta.domain.ai.service.IAiService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@@ -20,6 +21,7 @@
public class LearningAnalysisService {
@Autowired
+ @Qualifier("weaviateVectorRepository")
private IVectorService vectorService;
@Autowired
@@ -38,7 +40,15 @@ public void onUserLogin(String userId) {
// 分析用户最近7天的学习动态
List dynamics = learningDynamicsService.analyzeUserLearningDynamics(userId);
- log.info("用户登录学习动态分析完成,userId:{} 动态数量:{}", userId, dynamics.size());
+ if (dynamics != null && !dynamics.isEmpty()) {
+ log.info("用户登录学习动态分析完成,userId:{} 动态数量:{}", userId, dynamics.size());
+
+ // 可以在这里将分析结果存储到缓存或数据库中,供前端查询使用
+ // 例如:存储到Redis中,key为 "user_dynamics:" + userId
+
+ } else {
+ log.info("用户登录学习动态分析完成,但未生成有效动态,userId:{}", userId);
+ }
} catch (Exception e) {
log.error("用户登录学习动态分析失败,userId:{}", userId, e);
diff --git a/refine-domain/src/main/java/com/achobeta/domain/rag/service/impl/LearningDynamicsServiceImpl.java b/refine-domain/src/main/java/com/achobeta/domain/rag/service/impl/LearningDynamicsServiceImpl.java
index c3d9479..44cf5a5 100644
--- a/refine-domain/src/main/java/com/achobeta/domain/rag/service/impl/LearningDynamicsServiceImpl.java
+++ b/refine-domain/src/main/java/com/achobeta/domain/rag/service/impl/LearningDynamicsServiceImpl.java
@@ -15,6 +15,7 @@
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
@Slf4j
@@ -47,15 +48,24 @@ public List analyzeUserLearningDynamics(String userId) {
// 3. 使用AI分析学习动态
List dynamics = new ArrayList<>();
+ StringBuilder responseBuilder = new StringBuilder();
+
// 异步执行AI分析任务,解析题目并生成学习动态
CompletableFuture aiAnalysis = CompletableFuture.runAsync(() -> {
aiService.aiChat(analysisPrompt, response -> {
try {
- // 解析AI返回的JSON格式学习动态
- List parsedDynamics = parseAIResponse(response);
- dynamics.addAll(parsedDynamics);
+ // 收集所有响应片段
+ responseBuilder.append(response);
+
+ // 检查是否收到完整的JSON响应
+ String currentResponse = responseBuilder.toString();
+ if (isCompleteJsonResponse(currentResponse)) {
+ // 解析完整的AI返回的JSON格式学习动态
+ List parsedDynamics = parseAIResponse(currentResponse);
+ dynamics.addAll(parsedDynamics);
+ }
} catch (Exception e) {
- log.error("解析AI分析结果失败", e);
+ log.error("解析AI分析结果失败:{}", response, e);
}
});
});
@@ -64,9 +74,17 @@ public List analyzeUserLearningDynamics(String userId) {
// 等待AI分析完成,最多等待30秒
try {
aiAnalysis.get(30, TimeUnit.SECONDS);
+
+ // 如果AI分析没有产生结果,使用基于规则的分析
+ if (dynamics.isEmpty()) {
+ log.warn("AI分析未产生结果,使用基于规则的分析,userId:{}", userId);
+ return generateRuleBasedDynamics(userId);
+ }
+ } catch (TimeoutException e) {
+ log.error("AI分析超时,userId:{}", userId, e);
+ return generateRuleBasedDynamics(userId);
} catch (Exception e) {
- log.error("AI分析超时或失败", e);
- // 如果AI分析失败,返回基于规则的分析结果
+ log.error("AI分析失败,userId:{}", userId, e);
return generateRuleBasedDynamics(userId);
}
// 4. 限制返回最多3条动态
@@ -163,17 +181,26 @@ private String buildAnalysisPrompt(String learningDataSummary) {
学习数据:
%s
- 请按照以下JSON格式返回分析结果,每条动态都要有明确的类型、标题、描述和建议:
+ **重要要求:请严格按照以下JSON格式返回分析结果,不要添加任何额外的文字说明,只返回纯JSON数组:**
[
{
- "type": "progress|weakness|achievement",
+ "type": "progress",
"title": "动态标题",
"description": "详细描述",
"subject": "相关科目",
- "priority": 1-5,
+ "priority": 3,
"suggestion": "建议行动",
- "relatedQuestionCount": 数量
+ "relatedQuestionCount": 5
+ },
+ {
+ "type": "weakness",
+ "title": "另一个动态标题",
+ "description": "另一个详细描述",
+ "subject": "另一个科目",
+ "priority": 4,
+ "suggestion": "另一个建议行动",
+ "relatedQuestionCount": 2
}
]
@@ -182,6 +209,13 @@ private String buildAnalysisPrompt(String learningDataSummary) {
- weakness: 发现薄弱点,如某科目错误较多、学习频率低等
- achievement: 学习成就,如连续学习天数、完成学习目标等
+ **注意:**
+ 1. 必须返回有效的JSON数组格式
+ 2. priority必须是1-5之间的整数
+ 3. relatedQuestionCount必须是非负整数
+ 4. 所有字符串字段不能为null
+ 5. 不要在JSON前后添加任何解释文字
+
请确保返回的是有效的JSON格式,优先级高的动态排在前面。
""", learningDataSummary);
}
@@ -191,8 +225,16 @@ private String buildAnalysisPrompt(String learningDataSummary) {
*/
private List parseAIResponse(String aiResponse) {
try {
+ log.debug("开始解析AI响应,响应长度: {}", aiResponse != null ? aiResponse.length() : 0);
+
+ if (aiResponse == null || aiResponse.trim().isEmpty()) {
+ log.warn("AI响应为空,返回空列表");
+ return Collections.emptyList();
+ }
+
// 提取JSON部分(AI可能返回额外的文本)
String jsonPart = extractJsonFromResponse(aiResponse);
+ log.debug("提取的JSON部分: {}", jsonPart.length() > 500 ? jsonPart.substring(0, 500) + "..." : jsonPart);
// 解析JSON
List