From 5a624d468c8f4eddd89c8cf9b99eb6466fa21481 Mon Sep 17 00:00:00 2001
From: Administrator <15274802129@163.com>
Date: Wed, 06 Aug 2025 17:55:18 +0800
Subject: [PATCH] feat(ai): 增加 AI 陪练报告数据解析功能 - 新增 Report、RadarData 和 Evaluation 类用于解析报告数据 - 在 AiService 接口中添加 extractReportData 方法 - 在 AiServiceImpl 中实现报告数据的提取和解析 - 更新 ApiMemberTalkVo,增加 report 字段用于存储解析后的报告数据 - 修改前端相关的回答格式和类型

---
 src/main/java/cc/mrbird/febs/ai/service/impl/AiServiceImpl.java           |  144 +++++++++++++++++++++++++---
 src/main/java/cc/mrbird/febs/ai/res/memberTalk/ApiMemberTalkVo.java       |    6 +
 src/main/java/cc/mrbird/febs/ai/res/ai/RadarData.java                     |   33 ++++++
 src/main/java/cc/mrbird/febs/ai/req/ai/AiRequest.java                     |   16 +++
 src/main/java/cc/mrbird/febs/ai/res/ai/AiResponse.java                    |    7 +
 src/main/java/cc/mrbird/febs/ai/res/ai/Report.java                        |   24 ++++
 src/main/java/cc/mrbird/febs/ai/service/impl/AiMemberTalkServiceImpl.java |   22 ++--
 src/main/java/cc/mrbird/febs/ai/res/ai/Evaluation.java                    |   32 ++++++
 src/main/java/cc/mrbird/febs/ai/service/AiService.java                    |   11 ++
 9 files changed, 267 insertions(+), 28 deletions(-)

diff --git a/src/main/java/cc/mrbird/febs/ai/req/ai/AiRequest.java b/src/main/java/cc/mrbird/febs/ai/req/ai/AiRequest.java
new file mode 100644
index 0000000..8978209
--- /dev/null
+++ b/src/main/java/cc/mrbird/febs/ai/req/ai/AiRequest.java
@@ -0,0 +1,16 @@
+package cc.mrbird.febs.ai.req.ai;
+
+import io.swagger.annotations.ApiModel;
+import lombok.Data;
+
+/**
+ * @author Administrator
+ */
+@Data
+@ApiModel(value = "AiRequest", description = "参数")
+public class AiRequest {
+
+    private String promptTemplate;
+    private String linkId;
+    private String content;
+}
diff --git a/src/main/java/cc/mrbird/febs/ai/res/ai/AiResponse.java b/src/main/java/cc/mrbird/febs/ai/res/ai/AiResponse.java
index 14cf671..ae91a68 100644
--- a/src/main/java/cc/mrbird/febs/ai/res/ai/AiResponse.java
+++ b/src/main/java/cc/mrbird/febs/ai/res/ai/AiResponse.java
@@ -1,6 +1,7 @@
 package cc.mrbird.febs.ai.res.ai;
 
 import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
 
 /**
@@ -10,7 +11,13 @@
 @ApiModel(value = "AiResponse", description = "参数")
 public class AiResponse {
 
+    @ApiModelProperty(value = "响应")
     private String code;
+    @ApiModelProperty(value = "描述")
     private String description;
+    @ApiModelProperty(value = "建议")
     private String resContext;
+
+    @ApiModelProperty(value = "亮点、建议、参考答案、核心知识点雷达图表数据")
+    private Report report;
 }
diff --git a/src/main/java/cc/mrbird/febs/ai/res/ai/Evaluation.java b/src/main/java/cc/mrbird/febs/ai/res/ai/Evaluation.java
new file mode 100644
index 0000000..6b1e472
--- /dev/null
+++ b/src/main/java/cc/mrbird/febs/ai/res/ai/Evaluation.java
@@ -0,0 +1,32 @@
+package cc.mrbird.febs.ai.res.ai;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+/**
+ * @author Administrator
+ */
+@Data
+@ApiModel(value = "Evaluation", description = "参数")
+public class Evaluation {
+
+    @ApiModelProperty(value = "亮点")
+    @JsonProperty("highlight")
+    private String highlight;
+
+
+    @ApiModelProperty(value = "建议")
+    @JsonProperty("suggestion")
+    private String suggestion;
+
+
+    @ApiModelProperty(value = "参考答案")
+    @JsonProperty("reference_answer")
+    private String referenceAnswer;
+
+
+    @ApiModelProperty(value = "核心知识点")
+    @JsonProperty("key_knowledge")
+    private String keyKnowledge;
+}
diff --git a/src/main/java/cc/mrbird/febs/ai/res/ai/RadarData.java b/src/main/java/cc/mrbird/febs/ai/res/ai/RadarData.java
new file mode 100644
index 0000000..34b8a79
--- /dev/null
+++ b/src/main/java/cc/mrbird/febs/ai/res/ai/RadarData.java
@@ -0,0 +1,33 @@
+package cc.mrbird.febs.ai.res.ai;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+/**
+ * @author Administrator
+ */
+@Data
+@ApiModel(value = "radar_data", description = "参数")
+public class RadarData {
+    @ApiModelProperty(value = "问题理解")
+    @JsonProperty("problem_understanding")
+    private double problemUnderstanding;
+
+    @ApiModelProperty(value = "表达流畅度")
+    @JsonProperty("fluency")
+    private double fluency;
+
+    @ApiModelProperty(value = "原则体现")
+    @JsonProperty("principle_adherence")
+    private double principleAdherence;
+
+    @ApiModelProperty(value = "语言逻辑性")
+    @JsonProperty("logicality")
+    private double logicality;
+
+    @ApiModelProperty(value = "知识点掌握")
+    @JsonProperty("knowledge_mastery")
+    private double knowledgeMastery;
+
+}
diff --git a/src/main/java/cc/mrbird/febs/ai/res/ai/Report.java b/src/main/java/cc/mrbird/febs/ai/res/ai/Report.java
new file mode 100644
index 0000000..85a869a
--- /dev/null
+++ b/src/main/java/cc/mrbird/febs/ai/res/ai/Report.java
@@ -0,0 +1,24 @@
+package cc.mrbird.febs.ai.res.ai;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * @author Administrator
+ */
+@Data
+@ApiModel(value = "Report", description = "参数")
+public class Report {
+
+
+
+    @ApiModelProperty(value = "雷达图表数据")
+    @JsonProperty("radar_data")
+    private RadarData radarData;
+
+    @ApiModelProperty(value = "亮点、建议、参考答案、核心知识点")
+    @JsonProperty("evaluation")
+    private Evaluation evaluation;
+}
diff --git a/src/main/java/cc/mrbird/febs/ai/res/memberTalk/ApiMemberTalkVo.java b/src/main/java/cc/mrbird/febs/ai/res/memberTalk/ApiMemberTalkVo.java
index a93dd7d..7966519 100644
--- a/src/main/java/cc/mrbird/febs/ai/res/memberTalk/ApiMemberTalkVo.java
+++ b/src/main/java/cc/mrbird/febs/ai/res/memberTalk/ApiMemberTalkVo.java
@@ -1,5 +1,6 @@
 package cc.mrbird.febs.ai.res.memberTalk;
 
+import cc.mrbird.febs.ai.res.ai.Report;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
@@ -29,7 +30,10 @@
      * 内容
      */
 
-    @ApiModelProperty(value = "内容")
+    @ApiModelProperty(value = "内容(文本格式)")
     private String context;
 
+    @ApiModelProperty(value = "内容亮点、建议、参考答案、核心知识点雷达图表数据(数据对象)")
+    private Report report;
+
 }
diff --git a/src/main/java/cc/mrbird/febs/ai/service/AiService.java b/src/main/java/cc/mrbird/febs/ai/service/AiService.java
index 116094d..04a87e9 100644
--- a/src/main/java/cc/mrbird/febs/ai/service/AiService.java
+++ b/src/main/java/cc/mrbird/febs/ai/service/AiService.java
@@ -1,6 +1,8 @@
 package cc.mrbird.febs.ai.service;
 
+import cc.mrbird.febs.ai.req.ai.AiRequest;
 import cc.mrbird.febs.ai.res.ai.AiResponse;
+import cc.mrbird.febs.ai.res.ai.Report;
 
 /**
  * @author Administrator
@@ -10,5 +12,12 @@
 
     AiResponse start(String productRoleId, String content);
 
-    AiResponse question(String promptTemplate,String linkId,String content);
+    AiResponse question(AiRequest aiRequest);
+
+    /**
+     * 从模型输出中提取并解析报告数据
+     * @param modelOutput 模型原始输出
+     * @return 解析后的报告对象
+     */
+    Report extractReportData(String modelOutput);
 }
diff --git a/src/main/java/cc/mrbird/febs/ai/service/impl/AiMemberTalkServiceImpl.java b/src/main/java/cc/mrbird/febs/ai/service/impl/AiMemberTalkServiceImpl.java
index ddec2bb..6c88c98 100644
--- a/src/main/java/cc/mrbird/febs/ai/service/impl/AiMemberTalkServiceImpl.java
+++ b/src/main/java/cc/mrbird/febs/ai/service/impl/AiMemberTalkServiceImpl.java
@@ -14,6 +14,7 @@
 import cc.mrbird.febs.ai.service.AiService;
 import cc.mrbird.febs.ai.utils.UUID;
 import cc.mrbird.febs.common.entity.FebsResponse;
+import cc.mrbird.febs.common.exception.FebsException;
 import cc.mrbird.febs.common.utils.LoginUserUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
@@ -59,7 +60,7 @@
         productLinkQuery.last("limit 1");
         AiProductRoleLink aiProductRoleLink = aiProductRoleLinkService.getByQuery(productLinkQuery);
         if(ObjectUtil.isNull(aiProductRoleLink)){
-            throw new RuntimeException("产品AI陪练不存在");
+            throw new FebsException("产品AI陪练不存在");
         }
 
         Date nowTime = new Date();
@@ -72,17 +73,18 @@
             aiMemberTalk = this.add(memberUuid,productId,nowTime);
         }
 
-        AiResponse aiResponse = aiService.start(aiProductRoleLink.getProductRoleId(),"开始出题");
+        AiResponse aiResponse = aiService.start(aiProductRoleLink.getProductRoleId(),"<strong>\"生成题目\"</strong>");
         if(aiResponse.getCode().equals("200")){
             aiMemberTalkItemService.add(memberUuid,aiMemberTalk.getId(),1,aiResponse.getResContext(),nowTime);
             this.updateTimeUpdate(nowTime,aiMemberTalk.getId());
         }else{
-            throw new RuntimeException(aiResponse.getDescription());
+            throw new FebsException(aiResponse.getDescription());
         }
         ApiMemberTalkVo apiMemberTalkVo = new ApiMemberTalkVo();
         apiMemberTalkVo.setMemberTalkId(aiMemberTalk.getId());
         apiMemberTalkVo.setType(1);
         apiMemberTalkVo.setContext(aiResponse.getResContext());
+        apiMemberTalkVo.setReport(aiResponse.getReport());
 
         return new FebsResponse().success().data(apiMemberTalkVo);
     }
@@ -102,14 +104,14 @@
     }
 
 
-    public static final String ANSWER_FORMAT = "{}/n回答:{}/n请根据回答给出以下四个方面的总结,这个四个方面分别是亮点、建议、参考答案和核心知识点回顾。重点:四个方面的总结都是必须要有内容。";
+    public static final String ANSWER_FORMAT = "{}/n[回答]{}/n";
     @Override
     public FebsResponse answer(ApiMemberTalkAnswerDto dto) {
         String memberUuid = LoginUserUtil.getLoginUser().getMemberUuid();
         String memberTalkId = dto.getId();
         AiMemberTalk aiMemberTalk = this.getById(memberTalkId);
         if (ObjectUtil.isNull(aiMemberTalk)){
-            throw new RuntimeException("产品AI陪练对话不存在");
+            throw new FebsException("产品AI陪练对话不存在");
         }
 
         LambdaQueryWrapper<AiProductRoleLink> productLinkQuery = Wrappers.lambdaQuery(AiProductRoleLink.class);
@@ -117,7 +119,7 @@
         productLinkQuery.last("limit 1");
         AiProductRoleLink aiProductRoleLink = aiProductRoleLinkService.getByQuery(productLinkQuery);
         if(ObjectUtil.isNull(aiProductRoleLink)){
-            throw new RuntimeException("产品AI陪练不存在");
+            throw new FebsException("产品AI陪练不存在");
         }
 
         String reqContext = dto.getReqContext();
@@ -134,21 +136,21 @@
         String format = StrUtil.format(ANSWER_FORMAT, aiMemberTalkItem.getContext(), reqContext);
         log.info("format:{}",format);
         AiResponse aiResponse = aiService.start(aiProductRoleLink.getProductRoleId(), format);
+//        AiResponse aiResponse = aiService.start(aiProductRoleLink.getProductRoleId(), reqContext);
         if(aiResponse.getCode().equals("200")){
             Date nowTime = new Date();
             aiMemberTalkItemService.add(memberUuid,aiMemberTalk.getId(),3,aiResponse.getResContext(),nowTime);
             this.updateTimeUpdate(nowTime,aiMemberTalk.getId());
         }else{
-            throw new RuntimeException(aiResponse.getDescription());
+            throw new FebsException(aiResponse.getDescription());
         }
         ApiMemberTalkVo apiMemberTalkVo = new ApiMemberTalkVo();
         apiMemberTalkVo.setMemberTalkId(aiMemberTalk.getId());
-        apiMemberTalkVo.setType(1);
+        apiMemberTalkVo.setType(3);
         apiMemberTalkVo.setContext(aiResponse.getResContext());
-
+        apiMemberTalkVo.setReport(aiResponse.getReport());
         return new FebsResponse().success().data(apiMemberTalkVo);
     }
-
     @Override
     public AiMemberTalk add(String memberUuid, String productId, Date nowTime) {
         AiMemberTalk aiMemberTalk = new AiMemberTalk();
diff --git a/src/main/java/cc/mrbird/febs/ai/service/impl/AiServiceImpl.java b/src/main/java/cc/mrbird/febs/ai/service/impl/AiServiceImpl.java
index 0ef2ae9..3ab9f2c 100644
--- a/src/main/java/cc/mrbird/febs/ai/service/impl/AiServiceImpl.java
+++ b/src/main/java/cc/mrbird/febs/ai/service/impl/AiServiceImpl.java
@@ -1,13 +1,15 @@
 package cc.mrbird.febs.ai.service.impl;
 
 import cc.mrbird.febs.ai.entity.AiProductRole;
+import cc.mrbird.febs.ai.req.ai.AiRequest;
 import cc.mrbird.febs.ai.res.ai.AiResponse;
+import cc.mrbird.febs.ai.res.ai.Report;
 import cc.mrbird.febs.ai.service.AiProductRoleService;
 import cc.mrbird.febs.ai.service.AiService;
-import com.volcengine.ark.runtime.model.completion.chat.ChatCompletionChoice;
-import com.volcengine.ark.runtime.model.completion.chat.ChatCompletionRequest;
-import com.volcengine.ark.runtime.model.completion.chat.ChatMessage;
-import com.volcengine.ark.runtime.model.completion.chat.ChatMessageRole;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.volcengine.ark.runtime.model.completion.chat.*;
 import com.volcengine.ark.runtime.service.ArkService;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -22,6 +24,8 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
 /**
@@ -37,6 +41,7 @@
     private static final String CODE_ERROR = "500";
 
     private final AiProductRoleService aiProductRoleService;
+    private final ObjectMapper objectMapper;
 
     @Value("${ai.service.ak}")
     private String ak;
@@ -90,11 +95,19 @@
             return buildErrorResponse(CODE_ERROR, "角色配置不完整");
         }
 
-        return question(promptTemplate, linkId, content);
+        AiRequest aiRequest = new AiRequest();
+        aiRequest.setPromptTemplate(promptTemplate);
+        aiRequest.setLinkId(linkId);
+        aiRequest.setContent(content);
+
+        return question(aiRequest);
     }
 
     @Override
-    public AiResponse question(String promptTemplate, String linkId, String content) {
+    public AiResponse question(AiRequest aiRequest) {
+        String promptTemplate = aiRequest.getPromptTemplate();
+        String linkId = aiRequest.getLinkId();
+        String content = aiRequest.getContent();
         if (!StringUtils.hasText(promptTemplate) || !StringUtils.hasText(linkId) || !StringUtils.hasText(content)) {
             log.warn("请求参数不完整,promptTemplate: {}, linkId: {}, content: {}", promptTemplate, linkId, content);
             return buildErrorResponse(CODE_ERROR, "请求参数不完整");
@@ -106,16 +119,44 @@
         messages.add(systemMessage);
         messages.add(userMessage);
 
-        ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder()
-                .model(linkId)
-                .messages(messages)
-                .temperature(1.0)
-                .topP(0.7)
-                .maxTokens(4096)
-                .frequencyPenalty(0.0)
-                .build();
-
+        // 生成 JSON Schema
+        String schemaJson = "{\n" +
+                "     \"radar_data\": {\n" +
+                "       \"problem_understanding\": \"object\",\n" +
+                "       \"fluency\": \"object\",\n" +
+                "       \"principle_adherence\": \"object\",\n" +
+                "       \"logicality\": \"object\",\n" +
+                "       \"knowledge_mastery\": \"object\"\n" +
+                "     },\n" +
+                "     \"evaluation\": {\n" +
+                "       \"highlight\": \"object\",\n" +
+                "       \"suggestion\": \"object\",\n" +
+                "       \"reference_answer\": \"object\",\n" +
+                "       \"key_knowledge\": \"object\"\n" +
+                "     }\n" +
+                "   }";
         try {
+            JsonNode schemaNode = objectMapper.readTree(schemaJson);
+            // 配置响应格式
+            ChatCompletionRequest.ChatCompletionRequestResponseFormat responseFormat = new ChatCompletionRequest.ChatCompletionRequestResponseFormat(
+                    "json_schema",
+                    new ResponseFormatJSONSchemaJSONSchemaParam(
+                            "ai_response",
+                            "json数据响应",
+                            schemaNode,
+                            true
+                    )
+            );
+            ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder()
+                    .model(linkId)
+                    .messages(messages)
+                    .stream(false)
+                    .responseFormat(responseFormat)
+                    .temperature(1.0)
+                    .topP(0.7)
+                    .maxTokens(4096)
+                    .frequencyPenalty(0.0)
+                    .build();
             List<ChatCompletionChoice> choices = service.createChatCompletion(chatCompletionRequest).getChoices();
             String result = choices.stream()
                     .map(choice -> choice.getMessage().getContent())
@@ -123,11 +164,73 @@
                     .map(Object::toString)
                     .collect(Collectors.joining());
 
-            return buildSuccessResponse(result);
+            Report report = this.extractReportData(result);
+            return buildSuccessResponse(report, result);
+        } catch (JsonProcessingException e) {
+            log.error("初始化AI服务失败,JSON格式化输出初始化失败", e);
+            return buildErrorResponse(CODE_ERROR, "AI服务调用失败");
         } catch (Exception e) {
             log.error("调用AI服务失败,modelId: {}, content: {}", linkId, content, e);
             return buildErrorResponse(CODE_ERROR, "AI服务调用失败");
         }
+    }
+
+    private static final Pattern JSON_PATTERN = Pattern.compile(
+            "<\\|FunctionCallBegin\\|>(.*?)<\\|FunctionCallEnd\\|>",
+            Pattern.DOTALL
+    );
+
+    @Override
+    public Report extractReportData(String modelOutput) {
+        // 提取JSON部分
+        Matcher matcher = JSON_PATTERN.matcher(modelOutput);
+        if (!matcher.find()) {
+            log.warn("未匹配到FunctionCall内容,原始输出: {}", modelOutput);
+            return null;
+        }
+
+        String jsonContent = matcher.group(1);
+        log.debug("提取到的JSON内容: {}", jsonContent);
+
+        // 解析JSON到Report对象
+        try {
+            return objectMapper.readValue(jsonContent, Report.class);
+        } catch (JsonProcessingException e) {
+            log.error("JSON解析失败,原始内容: {}", jsonContent, e);
+            // 尝试修复截断的JSON(可选)
+            Report repairedReport = tryRepairTruncatedJson(jsonContent);
+            if (repairedReport != null) {
+                log.info("成功修复截断的JSON");
+                return repairedReport;
+            }
+            return null;
+        }
+    }
+
+    /**
+     * 尝试修复截断的JSON字符串
+     * @param truncatedJson 可能被截断的JSON字符串
+     * @return 修复后的Report对象,如果无法修复则返回null
+     */
+    private Report tryRepairTruncatedJson(String truncatedJson) {
+        // 简单的修复策略:尝试添加缺失的结束括号
+        String[] repairAttempts = {
+                truncatedJson + "\"}}}",
+                truncatedJson + "}}}",
+                truncatedJson + "}}"
+        };
+
+        for (String attempt : repairAttempts) {
+            try {
+                return objectMapper.readValue(attempt, Report.class);
+            } catch (JsonProcessingException e) {
+                log.debug("修复尝试失败: {}", attempt);
+                continue;
+            }
+        }
+
+        log.warn("无法修复截断的JSON: {}", truncatedJson);
+        return null;
     }
 
     private AiResponse buildErrorResponse(String code, String description) {
@@ -144,4 +247,13 @@
         response.setResContext(result);
         return response;
     }
+
+    private AiResponse buildSuccessResponse(Report report, String result) {
+        AiResponse response = new AiResponse();
+        response.setCode(CODE_SUCCESS);
+        response.setDescription("成功");
+        response.setResContext(result);
+        response.setReport(report);
+        return response;
+    }
 }

--
Gitblit v1.9.1