From 831e51cb419867912ec086a716ab157bb7e65161 Mon Sep 17 00:00:00 2001 From: Wang Date: Thu, 25 Dec 2025 09:41:45 +0800 Subject: [PATCH 1/6] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=85=AC=E5=85=B1?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/seer-teacher/seer-teacher-service-bootstrap/src/main/resources/application.yml b/seer-teacher/seer-teacher-service-bootstrap/src/main/resources/application.yml index 5200c02..406b99b 100644 --- a/seer-teacher/seer-teacher-service-bootstrap/src/main/resources/application.yml +++ b/seer-teacher/seer-teacher-service-bootstrap/src/main/resources/application.yml @@ -22,6 +22,8 @@ spring: - optional:nacos:${spring.application.name}-${spring.profiles.active}.yaml - optional:nacos:shared-database.yaml - optional:nacos:shared-redis.yaml + - optional:nacos:shared-sa-token.yaml + - optional:nacos:shared-minio.yaml cloud: nacos: discovery: From f50cbe33609ef9f43ffb05f976253ff5685e883e Mon Sep 17 00:00:00 2001 From: Wang Date: Thu, 25 Dec 2025 09:43:50 +0800 Subject: [PATCH 2/6] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=85=AC=E5=85=B1?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../teach/teacher/bootstrap/SeerTeacherApplicationBootStrap.java | 1 - 1 file changed, 1 deletion(-) diff --git a/seer-teacher/seer-teacher-service-bootstrap/src/main/java/com/seer/teach/teacher/bootstrap/SeerTeacherApplicationBootStrap.java b/seer-teacher/seer-teacher-service-bootstrap/src/main/java/com/seer/teach/teacher/bootstrap/SeerTeacherApplicationBootStrap.java index 027de6c..e1d3f4d 100644 --- a/seer-teacher/seer-teacher-service-bootstrap/src/main/java/com/seer/teach/teacher/bootstrap/SeerTeacherApplicationBootStrap.java +++ b/seer-teacher/seer-teacher-service-bootstrap/src/main/java/com/seer/teach/teacher/bootstrap/SeerTeacherApplicationBootStrap.java @@ -25,7 +25,6 @@ public class SeerTeacherApplicationBootStrap { public static void main(String[] args) { SpringApplication app = new SpringApplication(SeerTeacherApplicationBootStrap.class); Environment env = app.run(args).getEnvironment(); - log.info("spring.mvc.pathmatch.matching-strategy={}", env.getProperty("spring.mvc.pathmatch.matching-strategy")); String port = env.getProperty("server.port", "8087"); String contextPath = env.getProperty("server.servlet.context-path", "/"); log.info("----------------------------------------------------------"); From 91914407ec8a3ce4a3316081a7810f63fc8412c3 Mon Sep 17 00:00:00 2001 From: Wang Date: Sat, 27 Dec 2025 18:23:29 +0800 Subject: [PATCH 3/6] =?UTF-8?q?=E5=9F=BA=E4=BA=8Espring=20ai=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0dashscope,openpi=E7=9A=84chat=20client?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../teach/common/enums/ResultCodeEnum.java | 5 +- .../common/constants/CommonConstant.java | 5 + seer-dependencies/pom.xml | 44 +- seer-teacher/pom.xml | 101 +-- seer-teacher/seer-teacher-ai/pom.xml | 55 ++ .../teach/ai/client/AiModelClientFactory.java | 12 + .../ai/client/AiModelClientFactoryImpl.java | 43 + .../client/model/AbstractAiModelClient.java | 43 + .../teach/ai/client/model/AiModelClient.java | 78 ++ .../ai/client/model/config/ModelConfig.java | 13 + .../model/dashscope/DashScopeModelClient.java | 191 ++++ .../client/model/local/LocalModelClient.java | 57 ++ .../model/openai/OpenAIModelClient.java | 165 ++++ .../teach/ai/service/AiChatModelService.java | 101 +++ seer-teacher/seer-teacher-common/pom.xml | 6 + .../seer/teach/admin/util/PromptBuilder.java | 851 +++++++++--------- .../service/IAiScenarioConfigService.java | 4 +- .../impl/AiScenarioConfigServiceImpl.java | 23 + 18 files changed, 1263 insertions(+), 534 deletions(-) create mode 100644 seer-teacher/seer-teacher-ai/pom.xml create mode 100644 seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/AiModelClientFactory.java create mode 100644 seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/AiModelClientFactoryImpl.java create mode 100644 seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/AbstractAiModelClient.java create mode 100644 seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/AiModelClient.java create mode 100644 seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/config/ModelConfig.java create mode 100644 seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/dashscope/DashScopeModelClient.java create mode 100644 seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/local/LocalModelClient.java create mode 100644 seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/openai/OpenAIModelClient.java create mode 100644 seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/service/AiChatModelService.java diff --git a/seer-common/common-enums/src/main/java/com/seer/teach/common/enums/ResultCodeEnum.java b/seer-common/common-enums/src/main/java/com/seer/teach/common/enums/ResultCodeEnum.java index b4e278e..d4444a2 100644 --- a/seer-common/common-enums/src/main/java/com/seer/teach/common/enums/ResultCodeEnum.java +++ b/seer-common/common-enums/src/main/java/com/seer/teach/common/enums/ResultCodeEnum.java @@ -316,7 +316,10 @@ public enum ResultCodeEnum { // 富文本模板相关错误码 RICH_TEXT_TEMPLATE_PREVIEW_FAILED(1100418, "预览失败"), - RICH_TEXT_TEMPLATE_CONVERT_FAILED(1100419, "转换失败"); + RICH_TEXT_TEMPLATE_CONVERT_FAILED(1100419, "转换失败"), + + + AI_MODEL_NOT_FOUND(12000, "未找到模型"); private int code; private String msg; diff --git a/seer-common/common/src/main/java/com/seer/teach/common/constants/CommonConstant.java b/seer-common/common/src/main/java/com/seer/teach/common/constants/CommonConstant.java index 51e8961..951a7a1 100644 --- a/seer-common/common/src/main/java/com/seer/teach/common/constants/CommonConstant.java +++ b/seer-common/common/src/main/java/com/seer/teach/common/constants/CommonConstant.java @@ -70,4 +70,9 @@ public interface CommonConstant { * 任务锁前缀 */ String TASK_CANCEL_PREFIX = "ai_task_cancel:"; + + /** + * 启用 + */ + Integer ENABLE = 1; } \ No newline at end of file diff --git a/seer-dependencies/pom.xml b/seer-dependencies/pom.xml index ededf4c..9577480 100644 --- a/seer-dependencies/pom.xml +++ b/seer-dependencies/pom.xml @@ -15,8 +15,8 @@ 4.3.0 2025.0.0.0 - 2.0.0-SNAPSHOT - 1.0.0-M6.1 + 1.1.2 + 1.1.0.0-M5 @@ -68,7 +68,7 @@ 33.5.0-jre - 1.2.5.RELEASE + 2.0.8 2.8.14 @@ -186,6 +186,13 @@ import + + org.springframework.ai + spring-ai-bom + ${spring-ai.version} + pom + import + @@ -646,6 +653,37 @@ + + + com.alibaba.cloud.ai + spring-ai-alibaba-starter-dashscope + ${spring-ai-alibaba.version} + + + + org.springframework.ai + spring-ai-starter-model-deepseek + ${spring-ai.version} + + + + org.springframework.ai + spring-ai-starter-model-deepseek + ${spring-ai.version} + + + + org.springframework.ai + spring-ai-starter-model-ollama + ${spring-ai.version} + + + + com.alibaba.cloud.ai + spring-ai-alibaba-agent-framework + ${spring-ai-alibaba.version} + + org.springframework.boot diff --git a/seer-teacher/pom.xml b/seer-teacher/pom.xml index 5d0c5f3..b390949 100644 --- a/seer-teacher/pom.xml +++ b/seer-teacher/pom.xml @@ -19,105 +19,6 @@ seer-teacher-service-bootstrap seer-teacher-api seer-teacher-service-admin + seer-teacher-ai - - - - - - - com.github.houbb - opencc4j - 1.6.0 - - - - - - io.minio - minio - 8.5.1 - - - - - com.google.protobuf - protobuf-java - 4.30.2 - - - - - commons-io - commons-io - - - - commons-codec - commons-codec - - - - - com.baomidou - mybatis-plus-spring-boot3-starter - - - - com.baomidou - mybatis-plus-generator - - - - - org.apache.velocity - velocity-engine-core - - - - org.springframework.boot - spring-boot-starter-aop - - - - - com.alibaba.fastjson2 - fastjson2 - - - - org.springframework.boot - spring-boot-starter-validation - - - - javax.validation - validation-api - - - - - cn.dev33 - sa-token-spring-boot3-starter - - - - - org.springframework.retry - spring-retry - - - - - org.lionsoul - ip2region - - - - org.springframework.boot - spring-boot-starter-test - test - - - \ No newline at end of file diff --git a/seer-teacher/seer-teacher-ai/pom.xml b/seer-teacher/seer-teacher-ai/pom.xml new file mode 100644 index 0000000..6af3ff8 --- /dev/null +++ b/seer-teacher/seer-teacher-ai/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + + com.seer.teach + seer-teacher + 1.0.0-SNAPSHOT + + + seer-teacher-ai + + + + + ${project.groupId} + seer-teacher-service + ${project.version} + + + + org.springframework.retry + spring-retry + + + + com.alibaba.cloud.ai + spring-ai-alibaba-starter-dashscope + + + + org.springframework.ai + spring-ai-starter-model-openai + + + + org.springframework.ai + spring-ai-starter-model-deepseek + + + + org.springframework.ai + spring-ai-starter-model-ollama + + + + io.micrometer + micrometer-observation + 1.15.7 + compile + + + + \ No newline at end of file diff --git a/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/AiModelClientFactory.java b/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/AiModelClientFactory.java new file mode 100644 index 0000000..53824cb --- /dev/null +++ b/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/AiModelClientFactory.java @@ -0,0 +1,12 @@ +package com.seer.teach.ai.client; + +import com.seer.teach.ai.client.model.AiModelClient; + +/** + * + */ +public interface AiModelClientFactory { + + AiModelClient getClient(String platform); + +} diff --git a/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/AiModelClientFactoryImpl.java b/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/AiModelClientFactoryImpl.java new file mode 100644 index 0000000..ded4ea8 --- /dev/null +++ b/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/AiModelClientFactoryImpl.java @@ -0,0 +1,43 @@ +package com.seer.teach.ai.client; + +import cn.hutool.core.collection.CollectionUtil; +import com.seer.teach.ai.client.model.AiModelClient; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 平台模型管理器接口 + * 定义了平台特定的模型管理功能 + * + * @since 2025-12-25 + */ +@Slf4j +@Service +public class AiModelClientFactoryImpl implements AiModelClientFactory { + + private final Map serviceMap = new ConcurrentHashMap<>(); + + + public AiModelClientFactoryImpl(List platformServices){ + if(CollectionUtil.isEmpty(platformServices)){ + return; + } + for (AiModelClient platform : platformServices) { + register(platform); + } + } + + public void register(AiModelClient platformManager) { + String platformName = platformManager.getPlatformName(); + serviceMap.put(platformName, platformManager); + } + + @Override + public AiModelClient getClient(String platform) { + return serviceMap.get(platform); + } +} \ No newline at end of file diff --git a/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/AbstractAiModelClient.java b/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/AbstractAiModelClient.java new file mode 100644 index 0000000..f53bd21 --- /dev/null +++ b/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/AbstractAiModelClient.java @@ -0,0 +1,43 @@ +package com.seer.teach.ai.client.model; + +import io.micrometer.observation.ObservationRegistry; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.model.tool.DefaultToolExecutionEligibilityPredicate; +import org.springframework.ai.model.tool.ToolCallingManager; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.retry.support.RetryTemplate; +import org.springframework.web.client.ResponseErrorHandler; +import org.springframework.web.client.RestClient; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * 平台模型管理器抽象基类 + * 提供通用的模型管理功能实现,减少代码重复 + * + * @author Captain + * @since 2025-12-26 + */ +@Slf4j +public abstract class AbstractAiModelClient implements AiModelClient { + + @Autowired(required = false) + protected ResponseErrorHandler responseErrorHandler; + + @Autowired(required = false) + protected RestClient.Builder restClientBuilder; + + @Autowired(required = false) + protected WebClient.Builder webClientBuilder; + + @Autowired + protected RetryTemplate retryTemplate; + + @Autowired + protected ToolCallingManager toolCallingManager; + + @Autowired(required = false) + protected ObservationRegistry observationRegistry; + + protected final DefaultToolExecutionEligibilityPredicate toolExecutionEligibilityPredicate = new DefaultToolExecutionEligibilityPredicate(); + +} \ No newline at end of file diff --git a/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/AiModelClient.java b/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/AiModelClient.java new file mode 100644 index 0000000..0b1a2f0 --- /dev/null +++ b/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/AiModelClient.java @@ -0,0 +1,78 @@ +package com.seer.teach.ai.client.model; + +import com.alibaba.cloud.ai.dashscope.audio.synthesis.SpeechSynthesisModel; +import com.seer.teach.ai.client.model.config.ModelConfig; +import org.springframework.ai.audio.tts.TextToSpeechModel; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.prompt.ChatOptions; +import org.springframework.ai.embedding.EmbeddingModel; +import org.springframework.ai.image.ImageModel; +import org.springframework.stereotype.Service; + +import java.util.Map; +import java.util.Optional; + +/** + * + * @author Captain + * @since 2025-12-25 + */ +@Service +public interface AiModelClient { + + String getPlatformName(); + + /** + * 获取聊天模型 + * + * @param modelConfig 模型配置 + * @return 大语言模型实例的Optional包装 + */ + default Optional createChatModel(ModelConfig modelConfig){ + return Optional.empty(); + } + + default Optional createChatOptions(Map options){ + return Optional.empty(); + } + + /** + * 获取图像模型 + */ + default Optional createImageModel(ModelConfig modelConfig){ + return Optional.empty(); + } + + /** + * 获取语音合成模型(Speech Synthesis Model) + * 用于将文本转换为语音音频,仅dashscope平台支持 + * + * @param modelConfig 模型配置 + * @return 语音合成模型实例的Optional包装 + */ + default Optional createSpeechSynthesisModel(ModelConfig modelConfig) { + return Optional.empty(); + } + + /** + * 获取文本转语音模型(Text To Speech Model) + * 用于将文本转换为语音输出,支持多种平台 + * + * @param modelConfig 模型配置 + * @return 文本转语音模型实例的Optional包装 + */ + default Optional createTextToSpeechModel(ModelConfig modelConfig) { + return Optional.empty(); + } + + /** + * 获取嵌入模型(Embedding Model) + * 用于将文本转换为向量表示,支持多种平台 + * + * @param modelConfig 模型配置 + * @return 嵌入模型实例的Optional包装 + */ + default Optional createEmbeddingModel(ModelConfig modelConfig) { + return Optional.empty(); + } +} \ No newline at end of file diff --git a/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/config/ModelConfig.java b/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/config/ModelConfig.java new file mode 100644 index 0000000..2dc7390 --- /dev/null +++ b/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/config/ModelConfig.java @@ -0,0 +1,13 @@ +package com.seer.teach.ai.client.model.config; + +import lombok.Builder; +import lombok.Data; + +@Builder +@Data +public class ModelConfig { + + private String model; + private String apiKey; + private String url; +} diff --git a/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/dashscope/DashScopeModelClient.java b/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/dashscope/DashScopeModelClient.java new file mode 100644 index 0000000..8ec0fce --- /dev/null +++ b/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/dashscope/DashScopeModelClient.java @@ -0,0 +1,191 @@ +package com.seer.teach.ai.client.model.dashscope; + +import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeChatProperties; +import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeConnectionProperties; +import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeEmbeddingProperties; +import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; +import com.alibaba.cloud.ai.dashscope.api.DashScopeAudioSpeechApi; +import com.alibaba.cloud.ai.dashscope.api.DashScopeImageApi; +import com.alibaba.cloud.ai.dashscope.audio.DashScopeAudioSpeechModel; +import com.alibaba.cloud.ai.dashscope.audio.DashScopeAudioSpeechOptions; +import com.alibaba.cloud.ai.dashscope.audio.synthesis.SpeechSynthesisModel; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; +import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingModel; +import com.alibaba.cloud.ai.dashscope.image.DashScopeImageModel; +import com.alibaba.cloud.ai.dashscope.image.DashScopeImageOptions; +import com.seer.teach.ai.client.model.AbstractAiModelClient; +import com.seer.teach.ai.client.model.config.ModelConfig; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.prompt.ChatOptions; +import org.springframework.ai.document.MetadataMode; +import org.springframework.ai.embedding.EmbeddingModel; +import org.springframework.ai.image.ImageModel; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.Optional; + +/** + * DashScope平台模型管理器 + * 专门负责DashScope平台的AI模型管理 + * 使用配置属性进行更灵活的模型初始化 + * + * @author Captain + * @since 2025-12-25 + */ +@Slf4j +@Component +public class DashScopeModelClient extends AbstractAiModelClient { + + private DashScopeConnectionProperties connectionProperties; + + @Autowired(required = false) + private DashScopeChatProperties chatProperties; + + @Override + public String getPlatformName() { + return "dashscope"; + } + + @Override + public Optional createChatModel(ModelConfig modelConfig) { + DashScopeChatOptions chatOptions = DashScopeChatOptions.builder() + .withModel(modelConfig.getModel()) + .build(); + + DashScopeApi dashScopeApi = createDashScopeApi(modelConfig.getApiKey(), modelConfig.getUrl()); + return Optional.of(DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .retryTemplate(retryTemplate) + .toolCallingManager(toolCallingManager) + .defaultOptions(chatOptions) + .observationRegistry(observationRegistry) + .toolExecutionEligibilityPredicate(toolExecutionEligibilityPredicate) + .build()); + } + + @Override + public Optional createChatOptions(Map options) { + DashScopeChatOptions.DashScopeChatOptionsBuilder builder = DashScopeChatOptions.builder(); + for (Map.Entry entry : options.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + + if (value == null) { + continue; + } + switch (key) { + case "temperature": + builder.withTemperature(((Number) value).doubleValue()); + break; + case "maxTokens": + builder.withMaxToken(((Number) value).intValue()); + break; + case "enableSearch": + builder.withEnableSearch((Boolean) value); + break; + case "seed": + builder.withSeed(((Number) value).intValue()); + break; + case "topP": + builder.withTopP(((Number) value).doubleValue()); + break; + case "topK": + builder.withTopK(((Number) value).intValue()); + break; + case "enableThinking": + builder.withEnableThinking((Boolean) value); + break; + case "repetitionPenalty": + builder.withRepetitionPenalty(((Number) value).doubleValue()); + default: + break; + } + } + DashScopeChatOptions chatOptions = builder.build(); + return Optional.of(chatOptions); + } + + @Override + public Optional createImageModel(ModelConfig modelConfig) { + DashScopeImageOptions imageOptions = DashScopeImageOptions.builder() + .withModel(modelConfig.getModel()) + .build(); + + DashScopeImageApi imageApi = DashScopeImageApi.builder() + .apiKey(modelConfig.getApiKey()) + .baseUrl(modelConfig.getUrl()) + .restClientBuilder(restClientBuilder) + .responseErrorHandler(responseErrorHandler) + .build(); + return Optional.of(new DashScopeImageModel(imageApi, imageOptions, retryTemplate)); + } + + @Override + public Optional createSpeechSynthesisModel(ModelConfig modelConfig) { + try { + DashScopeAudioSpeechApi audioSpeechApi = new DashScopeAudioSpeechApi(modelConfig.getApiKey(), modelConfig.getUrl()); + + DashScopeAudioSpeechOptions options = DashScopeAudioSpeechOptions.builder() + .model(modelConfig.getModel()) + .requestText(DashScopeAudioSpeechApi.RequestTextType.PLAIN_TEXT) + .voice("longyingxiao") + .build(); + DashScopeAudioSpeechModel speechSynthesisModel = new DashScopeAudioSpeechModel(audioSpeechApi, options, retryTemplate); + + return Optional.of(speechSynthesisModel); + } catch (Exception e) { + log.error("创建DashScope语音合成模型失败", e); + return Optional.empty(); + } + } + + + @Override + public Optional createEmbeddingModel(ModelConfig modelConfig) { + try { + DashScopeEmbeddingProperties embeddingProperties = new DashScopeEmbeddingProperties(); + DashScopeApi dashScopeApi = createDashScopeApi(modelConfig.getApiKey(), modelConfig.getUrl()); + + DashScopeEmbeddingModel embeddingModel = new DashScopeEmbeddingModel(dashScopeApi, MetadataMode.EMBED, embeddingProperties.getOptions(), retryTemplate, observationRegistry); + + return Optional.of(embeddingModel); + } catch (Exception e) { + log.error("创建DashScope嵌入模型失败", e); + return Optional.empty(); + } + } + + /** + * 创建DashScope API实例 + */ + private DashScopeApi createDashScopeApi(String apiKey, String baseUrl) { + DashScopeApi.Builder builder = DashScopeApi.builder() + .apiKey(apiKey) + .baseUrl(baseUrl); + + // 如果有可用的客户端构建器,添加它们 + if (webClientBuilder != null) { + builder.webClientBuilder(webClientBuilder); + } + if (restClientBuilder != null) { + builder.restClientBuilder(restClientBuilder); + } + if (responseErrorHandler != null) { + builder.responseErrorHandler(responseErrorHandler); + } + return builder.build(); + } + + private DashScopeImageApi createDashScopeImageApi(String apiKey, String baseUrl) { + return DashScopeImageApi.builder() + .apiKey(apiKey) + .baseUrl(baseUrl) + .restClientBuilder(restClientBuilder) + .responseErrorHandler(responseErrorHandler) + .build(); + } +} \ No newline at end of file diff --git a/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/local/LocalModelClient.java b/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/local/LocalModelClient.java new file mode 100644 index 0000000..eb67aaa --- /dev/null +++ b/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/local/LocalModelClient.java @@ -0,0 +1,57 @@ +package com.seer.teach.ai.client.model.local; + +import com.seer.teach.ai.client.model.AbstractAiModelClient; +import com.seer.teach.ai.client.model.config.ModelConfig; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.prompt.ChatOptions; +import org.springframework.ai.ollama.OllamaChatModel; +import org.springframework.ai.ollama.api.OllamaApi; +import org.springframework.ai.ollama.api.OllamaChatOptions; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.Optional; + +/** + * 本地模型管理器 + * 专门负责Ollama等本地AI模型管理 + * + * @author Captain + * @since 2025-12-26 + */ +@Component +public class LocalModelClient extends AbstractAiModelClient { + + @Override + public String getPlatformName() { + return "local"; + } + + @Override + public Optional createChatModel(ModelConfig modelConfig) { + OllamaChatOptions chatOptions = OllamaChatOptions.builder() + .model(modelConfig.getModel()) + .build(); + + OllamaApi ollamaApi = OllamaApi.builder() + .baseUrl(modelConfig.getUrl()) + .restClientBuilder(restClientBuilder) + .webClientBuilder(webClientBuilder) + .responseErrorHandler(responseErrorHandler) + .build(); + + return Optional.of(OllamaChatModel.builder() + .ollamaApi(ollamaApi) + .defaultOptions(chatOptions) + .retryTemplate(retryTemplate) + .toolCallingManager(toolCallingManager) + .observationRegistry(observationRegistry) + .toolExecutionEligibilityPredicate(toolExecutionEligibilityPredicate) + .build()); + } + + @Override + public Optional createChatOptions(Map options) { + return Optional.empty(); + } +} \ No newline at end of file diff --git a/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/openai/OpenAIModelClient.java b/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/openai/OpenAIModelClient.java new file mode 100644 index 0000000..17c9aa7 --- /dev/null +++ b/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/openai/OpenAIModelClient.java @@ -0,0 +1,165 @@ +package com.seer.teach.ai.client.model.openai; + +import com.seer.teach.ai.client.model.AbstractAiModelClient; +import com.seer.teach.ai.client.model.config.ModelConfig; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.audio.tts.TextToSpeechModel; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.prompt.ChatOptions; +import org.springframework.ai.document.MetadataMode; +import org.springframework.ai.embedding.EmbeddingModel; +import org.springframework.ai.image.ImageModel; +import org.springframework.ai.model.SimpleApiKey; +import org.springframework.ai.model.openai.autoconfigure.OpenAiAudioSpeechProperties; +import org.springframework.ai.model.openai.autoconfigure.OpenAiChatProperties; +import org.springframework.ai.model.openai.autoconfigure.OpenAiEmbeddingProperties; +import org.springframework.ai.openai.OpenAiAudioSpeechModel; +import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.ai.openai.OpenAiChatOptions; +import org.springframework.ai.openai.OpenAiEmbeddingModel; +import org.springframework.ai.openai.OpenAiImageModel; +import org.springframework.ai.openai.OpenAiImageOptions; +import org.springframework.ai.openai.api.OpenAiApi; +import org.springframework.ai.openai.api.OpenAiAudioApi; +import org.springframework.ai.openai.api.OpenAiImageApi; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +/** + * OpenAI平台模型管理器 + * 专门负责OpenAI平台的AI模型管理 + * + * @author Captain + * @since 2025-12-26 + */ +@Slf4j +@Component +public class OpenAIModelClient extends AbstractAiModelClient { + + @Override + public String getPlatformName() { + return "openai"; + } + + @Override + public Optional createChatModel(ModelConfig modelConfig) { + OpenAiChatOptions chatOptions = OpenAiChatOptions.builder() + .model(modelConfig.getModel()) + .build(); + + OpenAiApi openAiApi = createOpenAiApi(modelConfig.getApiKey(), modelConfig.getUrl()); + + return Optional.of(new OpenAiChatModel(openAiApi, chatOptions, toolCallingManager, retryTemplate, observationRegistry)); + } + + @Override + public Optional createChatOptions(Map options) { + OpenAiChatOptions.Builder builder = OpenAiChatOptions.builder(); + builder.streamUsage(true); + for (Map.Entry entry : options.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + if (Objects.isNull(value)) { + continue; + } + switch (key) { + case "temperature": + builder.temperature(((Number) value).doubleValue()); + break; + case "maxTokens": + builder.maxTokens(((Number) value).intValue()); + break; + case "maxCompletionToken": + builder.maxCompletionTokens(((Number) value).intValue()); + break; + case "topLogprobs": + builder.topLogprobs(((Number) value).intValue()); + break; + case "logprobs": + builder.logprobs((Boolean) value); + break; + case "N": + case "n": + builder.N(((Number) value).intValue()); + break; + case "reasoningEffort": + builder.reasoningEffort(value.toString()); + break; + case "topP": + case "top_p": + builder.topP(((Number) value).doubleValue()); + break; + case "frequencyPenalty": + builder.frequencyPenalty(((Number) value).doubleValue()); + break; + case "presencePenalty": + builder.presencePenalty(((Number) value).doubleValue()); + break; + default: + break; + } + } + return Optional.of(builder.build()); + } + + @Override + public Optional createImageModel(ModelConfig modelConfig) { + OpenAiImageOptions imageOptions = OpenAiImageOptions.builder() + .model(modelConfig.getModel()) + .build(); + + OpenAiImageApi imageOpenAiApi = OpenAiImageApi.builder() + .baseUrl(modelConfig.getUrl()) + .apiKey(new SimpleApiKey(modelConfig.getApiKey())) + .restClientBuilder(restClientBuilder) + .responseErrorHandler(responseErrorHandler) + .build(); + + return Optional.of(new OpenAiImageModel(imageOpenAiApi, imageOptions, retryTemplate, observationRegistry)); + } + + @Override + public Optional createTextToSpeechModel(ModelConfig modelConfig) { + try { + OpenAiAudioApi audioApi = OpenAiAudioApi.builder() + .baseUrl(modelConfig.getUrl()) + .apiKey(new SimpleApiKey(modelConfig.getApiKey())) + .restClientBuilder(restClientBuilder) + .responseErrorHandler(responseErrorHandler) + .build(); + + OpenAiAudioSpeechProperties properties = new OpenAiAudioSpeechProperties(); + TextToSpeechModel textToSpeechModel = new OpenAiAudioSpeechModel(audioApi, properties.getOptions(), retryTemplate); + return Optional.of(textToSpeechModel); + } catch (Exception e) { + log.error("创建OpenAI文本转语音模型失败", e); + return Optional.empty(); + } + } + + @Override + public Optional createEmbeddingModel(ModelConfig modelConfig) { + try { + OpenAiApi openAiApi = createOpenAiApi(modelConfig.getApiKey(), modelConfig.getUrl()); + OpenAiEmbeddingProperties properties = new OpenAiEmbeddingProperties(); + EmbeddingModel embeddingModel = new OpenAiEmbeddingModel(openAiApi, MetadataMode.EMBED,properties.getOptions(), retryTemplate); + return Optional.of(embeddingModel); + } catch (Exception e) { + log.error("创建OpenAI嵌入模型失败", e); + return Optional.empty(); + } + } + + private OpenAiApi createOpenAiApi(String apiKey, String baseUrl) { + return OpenAiApi.builder() + .baseUrl(baseUrl) + .apiKey(new SimpleApiKey(apiKey)) + .completionsPath(OpenAiChatProperties.DEFAULT_COMPLETIONS_PATH) + .embeddingsPath(OpenAiEmbeddingProperties.DEFAULT_EMBEDDINGS_PATH) + .restClientBuilder(restClientBuilder) + .webClientBuilder(webClientBuilder).build(); + } +} \ No newline at end of file diff --git a/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/service/AiChatModelService.java b/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/service/AiChatModelService.java new file mode 100644 index 0000000..3024b20 --- /dev/null +++ b/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/service/AiChatModelService.java @@ -0,0 +1,101 @@ +package com.seer.teach.ai.service; + +import cn.hutool.core.bean.BeanUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.seer.teach.ai.client.AiModelClientFactory; +import com.seer.teach.ai.client.model.AiModelClient; +import com.seer.teach.ai.client.model.config.ModelConfig; +import com.seer.teach.common.enums.ResultCodeEnum; +import com.seer.teach.common.exception.CommonException; +import com.seer.teach.common.utils.AssertUtils; +import com.seer.teach.teacher.module.entity.AiApiKeyEntity; +import com.seer.teach.teacher.module.entity.AiModelEntity; +import com.seer.teach.teacher.module.entity.AiScenarioConfigEntity; +import com.seer.teach.teacher.service.IAiApiKeyService; +import com.seer.teach.teacher.service.IAiModelService; +import com.seer.teach.teacher.service.IAiScenarioConfigService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.ChatOptions; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class AiChatModelService { + + private final IAiScenarioConfigService aiScenarioConfigService; + + private final IAiApiKeyService aiApiKeyService; + + private final IAiModelService aiModelService; + + private final AiModelClientFactory aiModelClientFactory; + + public ChatResponse chatMessage(String scenarioCode, Object params){ + AiScenarioConfigEntity scenarioConfig = aiScenarioConfigService.getOneByScenarioCode(scenarioCode); + log.info("scenarioConfig:{}", scenarioConfig); + + AssertUtils.notNull(scenarioConfig, ResultCodeEnum.SCENARIO_NOT_FOUND); + + AiModelClient client = aiModelClientFactory.getClient(scenarioConfig.getPlatform()); + + ModelConfig modelConfig = getModelConfig(scenarioConfig); + + ChatModel chatModel = client.createChatModel(modelConfig).orElseThrow(() -> new CommonException(ResultCodeEnum.AI_MODEL_NOT_FOUND)); + + Optional options = client.createChatOptions(BeanUtil.beanToMap(scenarioConfig)); + Prompt prompt = new Prompt(params.toString(), options.orElse(null)); + return chatModel.call(prompt); + } + + public Flux chatStream(String scenarioCode, Object params){ + AiScenarioConfigEntity scenarioConfig = aiScenarioConfigService.getOneByScenarioCode(scenarioCode); + log.info("chatStream scenarioConfig:{}", scenarioConfig); + + AssertUtils.notNull(scenarioConfig, ResultCodeEnum.SCENARIO_NOT_FOUND); + + AiModelClient client = aiModelClientFactory.getClient(scenarioConfig.getPlatform()); + + ModelConfig modelConfig = getModelConfig(scenarioConfig); + + ChatModel chatModel = client.createChatModel(modelConfig).orElseThrow(() -> new CommonException(ResultCodeEnum.AI_MODEL_NOT_FOUND)); + + Optional options = client.createChatOptions(BeanUtil.beanToMap(scenarioConfig)); + Prompt prompt = new Prompt(params.toString(), options.orElse(null)); + return chatModel.stream(prompt); + } + + + private ModelConfig getModelConfig(AiScenarioConfigEntity scenarioConfig) { + AiModelEntity module = aiModelService.getById(scenarioConfig.getModuleId()); + AiApiKeyEntity apiKey = getApiKey(scenarioConfig); + return ModelConfig.builder() + .url(module.getUrl()).apiKey(apiKey.getApiKey()).model(module.getModelIdentifier()).build(); + } + + private AiApiKeyEntity getApiKey(AiScenarioConfigEntity aiScenarioConfig) { + List aiApiKeyList = aiApiKeyService.list(new LambdaQueryWrapper<>(AiApiKeyEntity.class).eq(AiApiKeyEntity::getPlatform, aiScenarioConfig.getPlatform())); + AiApiKeyEntity apiKey = null; + Optional any = aiApiKeyList.stream().filter(aiApiKey -> Objects.nonNull(aiScenarioConfig.getApiKeyId()) && aiScenarioConfig.getApiKeyId().intValue() == aiApiKey.getId()).findAny(); + if (any.isPresent()) { + apiKey = any.get(); + } + if (Objects.isNull(apiKey)) { + Optional apiKeyOptional = aiApiKeyList.stream().filter(aiApiKey -> Objects.nonNull(aiApiKey.getIsDefault()) && aiApiKey.getIsDefault() == 1).findAny(); + if (apiKeyOptional.isPresent()) { + apiKey = apiKeyOptional.get(); + } + } + return apiKey; + } + +} \ No newline at end of file diff --git a/seer-teacher/seer-teacher-common/pom.xml b/seer-teacher/seer-teacher-common/pom.xml index c32a679..50d116e 100644 --- a/seer-teacher/seer-teacher-common/pom.xml +++ b/seer-teacher/seer-teacher-common/pom.xml @@ -30,5 +30,11 @@ spring-webflux + + org.springframework + spring-webmvc + + + \ No newline at end of file diff --git a/seer-teacher/seer-teacher-service-admin/src/main/java/com/seer/teach/admin/util/PromptBuilder.java b/seer-teacher/seer-teacher-service-admin/src/main/java/com/seer/teach/admin/util/PromptBuilder.java index 01dcee5..85d83f3 100644 --- a/seer-teacher/seer-teacher-service-admin/src/main/java/com/seer/teach/admin/util/PromptBuilder.java +++ b/seer-teacher/seer-teacher-service-admin/src/main/java/com/seer/teach/admin/util/PromptBuilder.java @@ -31,41 +31,38 @@ public class PromptBuilder { public static String buildPromptForQuestionGeneration(ExampleQuestionsEntity exampleQuestion, KnowledgePointEntity knowledgePoint, Integer count) { - StringBuilder prompt = new StringBuilder(); + String optionsPart = ExampleQuestionTypesEnum.CHOICE.getCode().equals(exampleQuestion.getType()) ? + " - options: 选项数组(仅选择题需要,每个选项包含content和sort字段)\n\n" : ""; - prompt.append("你是一个教学题目的专家,请根据以下示例题目生成").append(count).append("道类似的题目。\n\n"); - - prompt.append("知识点:").append(knowledgePoint.getKnowledgePointName()).append("\n"); - prompt.append("知识点总结:").append(knowledgePoint.getSummary()).append("\n\n"); - prompt.append("要求:\n"); - prompt.append("1. 题目必须基于相同的教学知识点\n"); - prompt.append("2. 题目难度应与示例题目相当\n"); - prompt.append("3. 题型必须与示例题目一致\n"); - prompt.append("4. 答案必须明确且唯一\n"); - prompt.append("5. 提供详细的解题过程或解析\n"); - prompt.append("6. 返回严格的JSON数组格式,每项包含以下字段:\n"); - prompt.append(" - question: 题干内容\n"); - prompt.append(" - answer: 答案内容\n"); - prompt.append(" - solution: 解析内容\n"); - if (ExampleQuestionTypesEnum.CHOICE.getCode().equals(exampleQuestion.getType())) { - prompt.append(" - options: 选项数组(仅选择题需要,每个选项包含content和sort字段)\n\n"); - } - prompt.append("输出格式示例:\n"); - prompt.append("[\n"); - prompt.append(" {\n"); - prompt.append(" \"question\": \"题目内容\",\n"); - prompt.append(" \"answer\": \"答案内容\",\n"); - prompt.append(" \"solution\": \"解析内容\",\n"); - if (ExampleQuestionTypesEnum.CHOICE.getCode().equals(exampleQuestion.getType())) { - prompt.append(", \"options\": [\n"); - prompt.append(" {\"content\": \"选项A内容\", \"sort\": 1},\n"); - prompt.append(" {\"content\": \"选项B内容\", \"sort\": 2}\n"); - prompt.append(" ]\n"); - } - prompt.append(" }\n"); - prompt.append("]\n"); - - return prompt.toString(); + return """ + 你是一个教学题目的专家,请根据以下示例题目生成%d道类似的题目。 + + 知识点:%s + 知识点总结:%s + + 要求: + 1. 题目必须基于相同的教学知识点 + 2. 题目难度应与示例题目相当 + 3. 题型必须与示例题目一致 + 4. 答案必须明确且唯一 + 5. 提供详细的解题过程或解析 + 6. 返回严格的JSON数组格式,每项包含以下字段: + - question: 题干内容 + - answer: 答案内容 + - solution: 解析内容 + %s + 输出格式示例: + [ + { + "question": "题目内容", + "answer": "答案内容", + "solution": "解析内容", + %s + } + ] + """.formatted(count, knowledgePoint.getKnowledgePointName(), knowledgePoint.getSummary(), optionsPart, + ExampleQuestionTypesEnum.CHOICE.getCode().equals(exampleQuestion.getType()) ? + "\"options\": [\n {\"content\": \"选项A内容\", \"sort\": 1},\n {\"content\": \"选项B内容\", \"sort\": 2}\n ]" : ""); } /** @@ -77,104 +74,108 @@ public class PromptBuilder { * @return 提示词 */ public static String buildPromptForBankQuestionGeneration(KnowledgePointEntity knowledgePoints, Integer count, GenerateTaskReq req) { - StringBuilder prompt = new StringBuilder(); - prompt.append("你是一位资深的'" + getGradeTypeName(req.getGradeType()) + "'教学专家,请根据以下知识点和示例题目为每一类型(1单选选择题 2多选选择题 4判断题 6填空题)的题目分别生成").append(count).append("道类似的题目。\n\n"); - prompt.append("知识点名称:").append(knowledgePoints.getKnowledgePointName()).append("\n"); - prompt.append("知识点总结:").append(req.getSummary()).append("\n"); - prompt.append("对应的年级:").append(getGradeNameList(req.getGradeIds())).append("\n"); - prompt.append("对应的学科:").append(getSubjectName(req.getSubjectId())).append("\n\n"); - - prompt.append("要求:\n"); - prompt.append(" 1. 题目必须基于\"").append(knowledgePoints.getKnowledgePointName()).append("\"这个知识点,不能超出对应年纪和学生的理解范围\n"); - prompt.append(" 2. 题目难度应在1.00和5.00范围内,小数点后面两位小数,1.00为最简单,5.00为最难,且每道题的难度不能相同\n"); - prompt.append(" 3. 答案必须明确且唯一\n"); - prompt.append(" 4. 提供详细的解析过程,如果是数学,物理,化学,必须列出具体的解题步骤\n"); - prompt.append(" 5. 题干内容不能包含题目类型,如 (对/错),(多选)\n"); - prompt.append(" 6. 如果是选择题(单选选择题或则多选选择题),题干内容不能包含选项,如(题目内容,选项A内容,选项B内容)\n"); - prompt.append(" 7. 如果是判断题,答案(answer)只能是‘对’和‘错’,如果是选择题,答案只能是选项A,B,C,D,E... \n"); - prompt.append(" 8. 每一类型题目的数量必须是" + count + ",不能省略,如要求输出10道题目,不能只输出9道题目\n"); - prompt.append(" 9. 输出的格式必须是json格式,字段如下:\n"); - prompt.append(" - title: 题干内容\n"); - prompt.append(" - answer: 答案内容\n"); - prompt.append(" - solution: 解析内容,提供详细的解析过程,如果是数学,物理或则化学的,必须列出具体的解题步骤。如果是英语,尽量使用中文\n"); - prompt.append(" - type: 题型(1->单选选择题, 2->多选选择题, 4->判断题, 6->填空题)\n"); - prompt.append(" - difficulty: 题目难易度系数(1-5的整数,5为最难,每道题难度不能相同)\n"); - prompt.append(" - isNeedImage: 是否需要图片(0->不需要, 1->需要),如果是以图片的形式展示题目内容或者选项,则为1,否则为0\n"); - prompt.append(" - options: 选择题选项数组(仅题型为单选选择题或则多选选择题需要,每个选项包含content和sort字段,选项内容不能一样)\n"); - // 判断是否是一、二年级的数学题目 boolean isLowerElementaryMath = isLowerElementaryMath(req.getGradeIds(), req.getSubjectId()); // 判断是否是三、四年级的英语题目 boolean isMiddleElementaryEnglish = isMiddleElementaryEnglish(req.getGradeIds(), req.getSubjectId()); - if (isNeedLatex(req.getSubjectId())) { - prompt.append(" 10. 输出的题目内容,答案,解析,其中涉及到公式的,请使用Latex\n\n"); - } + String latexRequirement = isNeedLatex(req.getSubjectId()) ? " 10. 输出的题目内容,答案,解析,其中涉及到公式的,请使用Latex\n\n" : ""; - // 对三四年级英语题目添加特殊要求 - if (isMiddleElementaryEnglish) { - prompt.append(" 10. 这是三四年级的英语题目,请尽量使用中文出题,题目内容和选项中涉及的英语内容应适合该年龄段学生理解\n\n"); - } + String englishRequirement = isMiddleElementaryEnglish ? " 10. 这是三四年级的英语题目,请尽量使用中文出题,题目内容和选项中涉及的英语内容应适合该年龄段学生理解\n\n" : ""; - prompt.append("输出格式示例:\n"); - prompt.append("[\n"); - prompt.append(" {\n"); - prompt.append(" \"title\": \"题目内容\",\n"); - prompt.append(" \"answer\": \"答案内容\",\n"); - prompt.append(" \"solution\": \"解析内容\",\n"); - prompt.append(" \"isNeedImage\": \"0\",\n"); - prompt.append(" \"type\": 1,\n"); - prompt.append(" \"difficulty\": 3.34,\n"); - prompt.append(" \"options\": [\n"); - prompt.append(" {\"content\": \"A. 选项A内容\", \"sort\": 1},\n"); - prompt.append(" {\"content\": \"B. 选项B内容\", \"sort\": 2},\n"); - prompt.append(" {\"content\": \"C. 选项C内容\", \"sort\": 3},\n"); - prompt.append(" {\"content\": \"D. 选项D内容\", \"sort\": 4}\n"); - prompt.append(" ]\n"); - - prompt.append(" }\n"); - prompt.append("]\n"); - - // 特殊要求:对于小学一、二年级的数学题目,强调以图片为主 - if (isLowerElementaryMath) { - prompt.append("\n特别注意:这是一、二年级的题目,请尽量以图文并茂的形式展示题目内容和选项,使题目更适合低年级学生理解。\n"); - } + String lowerElementaryMathNote = isLowerElementaryMath ? "\n特别注意:这是一、二年级的题目,请尽量以图文并茂的形式展示题目内容和选项,使题目更适合低年级学生理解。\n" : ""; - // 特殊要求:对于三、四年级的英语题目,强调使用中文出题 - if (isMiddleElementaryEnglish) { - prompt.append("\n特别注意:这是三、四年级的英语题目,请尽量使用中文出题,让题目更容易被学生理解。\n"); - } + String middleElementaryEnglishNote = isMiddleElementaryEnglish ? "\n特别注意:这是三、四年级的英语题目,请尽量使用中文出题,让题目更容易被学生理解。\n" : ""; - // 数学题目特殊要求:针对不同年级提供不同的解题步骤标准 + String mathRequirements = ""; if (req.getSubjectId() == 2) { // 数学学科 - prompt.append("\n数学题目解析(solution字段)规范要求:\n"); + mathRequirements = "\n数学题目解析(solution字段)规范要求:\n"; if(Objects.nonNull(req.getGradeId())){ // 初中三个年级的要求 if (req.getGradeId() == 7) { // 七年级(初一) - prompt.append("七年级(初一)解析标准:\n"); - prompt.append("- 格式要求:从算术思维向代数思维过渡,强调基本概念、运算规则和解题格式的建立\n"); - prompt.append("- 解析步骤:读与标(阅读题目,标记关键数据) -> 联与析(联想相关数学概念) -> 算或解(执行计算或按步骤解方程) -> 验与答(初步检验答案,并规范作答)\n"); - prompt.append("- 示例题型:代数方程应用题,要求严格按照\"设->列->解->验->答\"的格式进行解答\n"); + mathRequirements += "七年级(初一)解析标准:\n"; + mathRequirements += "- 格式要求:从算术思维向代数思维过渡,强调基本概念、运算规则和解题格式的建立\n"; + mathRequirements += "- 解析步骤:读与标(阅读题目,标记关键数据) -> 联与析(联想相关数学概念) -> 算或解(执行计算或按步骤解方程) -> 验与答(初步检验答案,并规范作答)\n"; + mathRequirements += "- 示例题型:代数方程应用题,要求严格按照\"设->列->解->验->答\"的格式进行解答\n"; } if (req.getGradeId() == 8) { // 八年级(初二) - prompt.append("八年级(初二)解析标准:\n"); - prompt.append("- 格式要求:逻辑推理能力要求提高,几何证明和函数概念引入,解题需展现严密的因果链条\n"); - prompt.append("- 解析步骤:条件梳理(明确已知条件) -> 目标分析(明确待证明结论) -> 思路探寻(寻找定理、公式) -> 逻辑推演(清晰展示推理) -> 结论总结(给出最终结论)\n"); - prompt.append("- 示例题型:几何证明题,要求严格按照\"已知->求证->证明\"的格式进行解答\n"); + mathRequirements += "八年级(初二)解析标准:\n"; + mathRequirements += "- 格式要求:逻辑推理能力要求提高,几何证明和函数概念引入,解题需展现严密的因果链条\n"; + mathRequirements += "- 解析步骤:条件梳理(明确已知条件) -> 目标分析(明确待证明结论) -> 思路探寻(寻找定理、公式) -> 逻辑推演(清晰展示推理) -> 结论总结(给出最终结论)\n"; + mathRequirements += "- 示例题型:几何证明题,要求严格按照\"已知->求证->证明\"的格式进行解答\n"; } if (req.getGradeId() == 9) { // 九年级(初三) - prompt.append("九年级(初三)解析标准:\n"); - prompt.append("- 格式要求:知识高度综合,强调数学思想方法(分类讨论、数形结合、转化与化归、方程思想、模型思想)的应用,解法灵活多样\n"); - prompt.append("- 解析步骤:审题与建模(识别问题本质) -> 思路选择与规划(选择合适解题路径) -> 执行与讨论(计算或推理,必要时分类讨论) -> 检验与反思(检验答案合理性)\n"); - prompt.append("- 示例题型:二次函数综合题,要求体现数学思想方法的灵活应用\n"); + mathRequirements += "九年级(初三)解析标准:\n"; + mathRequirements += "- 格式要求:知识高度综合,强调数学思想方法(分类讨论、数形结合、转化与化归、方程思想、模型思想)的应用,解法灵活多样\n"; + mathRequirements += "- 解析步骤:审题与建模(识别问题本质) -> 思路选择与规划(选择合适解题路径) -> 执行与讨论(计算或推理,必要时分类讨论) -> 检验与反思(检验答案合理性)\n"; + mathRequirements += "- 示例题型:二次函数综合题,要求体现数学思想方法的灵活应用\n"; } } } - - return prompt.toString(); + + return """ + 你是一位资深的'%s'教学专家,请根据以下知识点和示例题目为每一类型(1单选选择题 2多选选择题 4判断题 6填空题)的题目分别生成%d道类似的题目。 + + 知识点名称:%s + 知识点总结:%s + 对应的年级:%s + 对应的学科:%s + + 要求: + 1. 题目必须基于"%s"这个知识点,不能超出对应年纪和学生的理解范围 + 2. 题目难度应在1.00和5.00范围内,小数点后面两位小数,1.00为最简单,5.00为最难,且每道题的难度不能相同 + 3. 答案必须明确且唯一 + 4. 提供详细的解析过程,如果是数学,物理,化学,必须列出具体的解题步骤 + 5. 题干内容不能包含题目类型,如 (对/错),(多选) + 6. 如果是选择题(单选选择题或则多选选择题),题干内容不能包含选项,如(题目内容,选项A内容,选项B内容) + 7. 如果是判断题,答案(answer)只能是‘对’和‘错’,如果是选择题,答案只能是选项A,B,C,D,E... + 8. 每一类型题目的数量必须是%d,不能省略,如要求输出10道题目,不能只输出9道题目 + 9. 输出的格式必须是json格式,字段如下: + - title: 题干内容 + - answer: 答案内容 + - solution: 解析内容,提供详细的解析过程,如果是数学,物理或则化学的,必须列出具体的解题步骤。如果是英语,尽量使用中文 + - type: 题型(1->单选选择题, 2->多选选择题, 4->判断题, 6->填空题) + - difficulty: 题目难易度系数(1-5的整数,5为最难,每道题难度不能相同) + - isNeedImage: 是否需要图片(0->不需要, 1->需要),如果是以图片的形式展示题目内容或者选项,则为1,否则为0 + - options: 选择题选项数组(仅题型为单选选择题或则多选选择题需要,每个选项包含content和sort字段,选项内容不能一样) + %s%s + 输出格式示例: + [ + { + "title": "题目内容", + "answer": "答案内容", + "solution": "解析内容", + "isNeedImage": "0", + "type": 1, + "difficulty": 3.34, + "options": [ + {"content": "A. 选项A内容", "sort": 1}, + {"content": "B. 选项B内容", "sort": 2}, + {"content": "C. 选项C内容", "sort": 3}, + {"content": "D. 选项D内容", "sort": 4} + ] + } + ] + + %s%s%s + """.formatted( + getGradeTypeName(req.getGradeType()), + count, + knowledgePoints.getKnowledgePointName(), + req.getSummary(), + getGradeNameList(req.getGradeIds()), + getSubjectName(req.getSubjectId()), + knowledgePoints.getKnowledgePointName(), + count, + latexRequirement, + englishRequirement, + lowerElementaryMathNote, + middleElementaryEnglishNote, + mathRequirements + ); } /** @@ -184,60 +185,70 @@ public class PromptBuilder { * @return 提示词 */ public static String buildSplitPrompt(KnowledgePointEntity knowledgePoint, KnowSummaryEntity summary, KnowledgePointSplitReq req) { - StringBuilder prompt = new StringBuilder(); - - prompt.append("你是一位资深的教学专家,请根据以下知识点进行拆分,将其拆分为" + req.getCount() + "个更细粒度的知识点。\n\n"); - - prompt.append("原始知识点名称:").append(knowledgePoint.getKnowledgePointName()).append("\n"); - prompt.append("原始知识点总结:").append(summary.getSummary()).append("\n"); - prompt.append("对应的年级:").append(getGradeNameList(req.getGradeIds())).append("\n"); - prompt.append("对应的学科:").append(getSubjectName(knowledgePoint.getSubjectId())).append("\n"); + String specificNames = ""; if (CollectionUtil.isNotEmpty(req.getKnowledgePointNames())) { - prompt.append("要求拆分为以下名称的知识点:").append("\n"); + StringBuilder namesBuilder = new StringBuilder(); for (int i = 0; i < req.getKnowledgePointNames().size(); i++) { - prompt.append(" ").append(i + 1).append(". ").append(req.getKnowledgePointNames().get(i)).append("\n"); + namesBuilder.append(" ").append(i + 1).append(". ").append(req.getKnowledgePointNames().get(i)).append("\n"); } + specificNames = "要求拆分为以下名称的知识点:\n" + namesBuilder.toString(); } - prompt.append("要求:\n"); - if (CollectionUtil.isNotEmpty(req.getKnowledgePointNames())) { - prompt.append("1. 将原始知识点拆分为上面指定的").append(req.getCount()).append("个名称的知识点,每个知识点需要包含以下信息:\n"); - } else { - prompt.append("1. 将原始知识点拆分为").append(req.getCount()).append("个更具体、更细粒度的知识点,每个知识点需要包含以下信息:\n"); - } - prompt.append(" - knowledgePointName: 拆分后的知识点名称\n"); - prompt.append(" - summary: 知识点的简要总结\n"); - prompt.append(" - exampleQuestions: 该知识点的示例题目列表\n"); - prompt.append("2. 每个知识点不能超出对应年纪和学生的理解范围\n"); - prompt.append("3. 知识点总结必须符合教学逻辑(能直接被教师用于教学视频脚本),应包括:①基本定义;②在学科体系中的地位与作用;③与前后知识的衔接关系;④学习意义与实际应用;⑤常见误区与误解。少于 300 字。\n"); - prompt.append("4. exampleQuestions字段要求:\n"); - prompt.append(" - 每个题目包含type(题型)、question(题目内容)、options(选项,仅选择题需要)、answer(答案)、explanation(解析)字段\n"); - prompt.append(" - 题型包括:包含4种题型(1单选选择题 2多选选择题 4判断题 6填空题)\n"); - prompt.append(" - 每种题型1道题目\n\n"); + String requirements = CollectionUtil.isNotEmpty(req.getKnowledgePointNames()) ? + String.format("1. 将原始知识点拆分为上面指定的%d个名称的知识点,每个知识点需要包含以下信息:\n", req.getCount()) : + String.format("1. 将原始知识点拆分为%d个更具体、更细粒度的知识点,每个知识点需要包含以下信息:\n", req.getCount()); - prompt.append("5. 返回严格的JSON数组格式,示例如下:\n"); - prompt.append("[\n"); - prompt.append(" {\n"); - prompt.append(" \"knowledgePointName\": \"拆分后的知识点名称\",\n"); - prompt.append(" \"summary\": \"知识点总结\",\n"); - prompt.append(" \"exampleQuestions\": [\n"); - prompt.append(" {\n"); - prompt.append(" \"type\": \"1\",\n"); - prompt.append(" \"question\": \"题目内容\",\n"); - prompt.append(" \"options\": [\n"); - prompt.append(" {\"content\": \"A. 选项A内容\", \"sort\": 1},\n"); - prompt.append(" {\"content\": \"B. 选项B内容\", \"sort\": 2},\n"); - prompt.append(" {\"content\": \"C. 选项C内容\", \"sort\": 3},\n"); - prompt.append(" {\"content\": \"D. 选项D内容\", \"sort\": 4}\n"); - prompt.append(" ]\n"); - prompt.append(" \"answer\": \"答案\",\n"); - prompt.append(" \"explanation\": \"解析内容\"\n"); - prompt.append(" }\n"); - prompt.append(" ]\n"); - prompt.append(" }\n"); - prompt.append("]\n"); - - return prompt.toString(); + return """ + 你是一位资深的教学专家,请根据以下知识点进行拆分,将其拆分为%d个更细粒度的知识点。 + + 原始知识点名称:%s + 原始知识点总结:%s + 对应的年级:%s + 对应的学科:%s + %s + 要求: + %s + - knowledgePointName: 拆分后的知识点名称 + - summary: 知识点的简要总结 + - exampleQuestions: 该知识点的示例题目列表 + 2. 每个知识点不能超出对应年纪和学生的理解范围 + 3. 知识点总结必须符合教学逻辑(能直接被教师用于教学视频脚本),应包括:①基本定义;②在学科体系中的地位与作用;③与前后知识的衔接关系;④学习意义与实际应用;⑤常见误区与误解。少于 300 字。 + 4. exampleQuestions字段要求: + - 每个题目包含type(题型)、question(题目内容)、options(选项,仅选择题需要)、answer(答案)、explanation(解析)字段 + - 题型包括:包含4种题型(1单选选择题 2多选选择题 4判断题 6填空题) + - 每种题型1道题目 + + + 5. 返回严格的JSON数组格式,示例如下: + [ + { + "knowledgePointName": "拆分后的知识点名称", + "summary": "知识点总结", + "exampleQuestions": [ + { + "type": "1", + "question": "题目内容", + "options": [ + {"content": "A. 选项A内容", "sort": 1}, + {"content": "B. 选项B内容", "sort": 2}, + {"content": "C. 选项C内容", "sort": 3}, + {"content": "D. 选项D内容", "sort": 4} + ] + "answer": "答案", + "explanation": "解析内容" + } + ] + } + ] + """.formatted( + req.getCount(), + knowledgePoint.getKnowledgePointName(), + summary.getSummary(), + getGradeNameList(req.getGradeIds()), + getSubjectName(knowledgePoint.getSubjectId()), + specificNames, + requirements + ); } @@ -249,53 +260,59 @@ public class PromptBuilder { * @return 提示词 */ public static String buildCheckPrompt(KnowledgePointEntity knowledgePoint, KnowSummaryEntity summary, Collection gradeIds) { - StringBuilder prompt = new StringBuilder(); - - prompt.append("你是一位资深的教学专家,请根据以下知识点判断其内容是否正确和超纲。\n\n"); - - prompt.append("知识点名称:").append(knowledgePoint.getKnowledgePointName()).append("\n"); - prompt.append("知识点总结:").append(summary != null ? summary.getSummary() : "").append("\n"); - prompt.append("对应的年级:").append(getGradeNameList(gradeIds)).append("\n"); - prompt.append("对应的学科:").append(getSubjectName(knowledgePoint.getSubjectId())).append("\n"); - - prompt.append("请判断以下内容:\n"); - prompt.append("1. 该知识点内容是否正确\n"); - prompt.append("2. 该知识点是否超出指定年级学生的学习范围(超纲)\n"); - prompt.append("3. 如果超纲,请说明超纲原因,并给出适合的替换知识点名称和总结\n"); - prompt.append("4. 请提供该知识点的示例题目\n\n"); - - prompt.append("要求:\n"); - prompt.append("1. 返回严格的JSON格式,包含以下字段:\n"); - prompt.append(" - isCorrect: 知识点内容是否正确 (boolean)\n"); - prompt.append(" - isOutOfScope: 是否超纲 (boolean)\n"); - prompt.append(" - outOfScopeReason: 超纲原因 (string, 如果没有超纲则为null)\n"); - prompt.append(" - suggestedKnowledgePointName: 建议的知识点名称 (string, 如果没有超纲则为null)\n"); - prompt.append(" - suggestedSummary: 建议的知识点总结 (string, 如果没有超纲则为null)\n"); - prompt.append(" - exampleQuestions: 示例题目列表 (array)\n"); - prompt.append("2. exampleQuestions字段要求:\n"); - prompt.append(" - 每个题目包含type(题型)、question(题目内容)、options(选项,仅选择题需要)、answer(答案)、explanation(解析)字段\n"); - prompt.append(" - 题型包括:包含4种题型(1单选选择题 2多选选择题 4判断题 6填空题)\n"); - prompt.append(" - 每种题型1道题目\n\n"); - - prompt.append("输出格式示例:\n"); - prompt.append("{\n"); - prompt.append(" \"isCorrect\": true,\n"); - prompt.append(" \"isOutOfScope\": false,\n"); - prompt.append(" \"outOfScopeReason\": null,\n"); - prompt.append(" \"suggestedKnowledgePointName\": null,\n"); - prompt.append(" \"suggestedSummary\": null,\n"); - prompt.append(" \"exampleQuestions\": [\n"); - prompt.append(" {\n"); - prompt.append(" \"type\": \"1\",\n"); - prompt.append(" \"question\": \"题目内容\",\n"); - prompt.append(" \"options\": [\"选项A\", \"选项B\", \"选项C\", \"选项D\"],\n"); - prompt.append(" \"answer\": \"答案\",\n"); - prompt.append(" \"explanation\": \"解析内容\"\n"); - prompt.append(" }\n"); - prompt.append(" ]\n"); - prompt.append("}\n"); - - return prompt.toString(); + return """ + 你是一位资深的教学专家,请根据以下知识点判断其内容是否正确和超纲。 + + 知识点名称:%s + 知识点总结:%s + 对应的年级:%s + 对应的学科:%s + + + 请判断以下内容: + 1. 该知识点内容是否正确 + 2. 该知识点是否超出指定年级学生的学习范围(超纲) + 3. 如果超纲,请说明超纲原因,并给出适合的替换知识点名称和总结 + 4. 请提供该知识点的示例题目 + + + 要求: + 1. 返回严格的JSON格式,包含以下字段: + - isCorrect: 知识点内容是否正确 (boolean) + - isOutOfScope: 是否超纲 (boolean) + - outOfScopeReason: 超纲原因 (string, 如果没有超纲则为null) + - suggestedKnowledgePointName: 建议的知识点名称 (string, 如果没有超纲则为null) + - suggestedSummary: 建议的知识点总结 (string, 如果没有超纲则为null) + - exampleQuestions: 示例题目列表 (array) + 2. exampleQuestions字段要求: + - 每个题目包含type(题型)、question(题目内容)、options(选项,仅选择题需要)、answer(答案)、explanation(解析)字段 + - 题型包括:包含4种题型(1单选选择题 2多选选择题 4判断题 6填空题) + - 每种题型1道题目 + + + 输出格式示例: + { + "isCorrect": true, + "isOutOfScope": false, + "outOfScopeReason": null, + "suggestedKnowledgePointName": null, + "suggestedSummary": null, + "exampleQuestions": [ + { + "type": "1", + "question": "题目内容", + "options": ["选项A", "选项B", "选项C", "选项D"], + "answer": "答案", + "explanation": "解析内容" + } + ] + } + """.formatted( + knowledgePoint.getKnowledgePointName(), + summary != null ? summary.getSummary() : "", + getGradeNameList(gradeIds), + getSubjectName(knowledgePoint.getSubjectId()) + ); } /** @@ -306,100 +323,107 @@ public class PromptBuilder { * @return 提示词 */ public static String buildPromptForTeachTextGeneration(TeachGoalsEntity teachGoal, List methods, GenerateTaskReq req) { - StringBuilder prompt = new StringBuilder(); - prompt.append("你是一位经验丰富的教师,请根据以下教学目标和教学重点,针对每种教学方法分别生成一份详细且有针对性的讲课稿。\n\n"); - - // 添加教学目标信息 - prompt.append("教学目标: ").append(teachGoal.getGoal()).append("\n"); - prompt.append("教学重点: ").append(teachGoal.getPoint()).append("\n\n"); - if(Objects.nonNull(req.getGradeIds())){ - prompt.append("对应的年级:").append(getGradeName(req.getGradeId())).append("\n"); - } - if(Objects.nonNull(req.getSubjectId())){ - prompt.append("对应的学科:").append(getSubjectName(req.getSubjectId())).append("\n\n"); - } - - // 添加教学方法信息 - prompt.append("教学方法列表:\n"); + StringBuilder methodsBuilder = new StringBuilder(); for (int i = 0; i < methods.size(); i++) { TeachMethodsEntity method = methods.get(i); - prompt.append((i + 1)).append(". ").append(method.getName()); + methodsBuilder.append((i + 1)).append(". ").append(method.getName()); if (method.getDescription() != null && !method.getDescription().isEmpty()) { - prompt.append(": ").append(method.getDescription()); + methodsBuilder.append(": ").append(method.getDescription()); } - prompt.append("(methodId: ").append(method.getId()).append(")\n"); + methodsBuilder.append("(methodId: ").append(method.getId()).append(")\n"); } - prompt.append("\n"); - // 添加要求 - prompt.append("请根据以上教学目标、重点和教学方法生成详细的讲课稿,要求如下:\n"); - prompt.append("1. 内容详实,语言生动,适合课堂讲解\n"); - prompt.append("2. 结构清晰,逻辑性强\n"); - prompt.append("3. 包含导入、新授、巩固练习、小结等教学环节\n"); - prompt.append("4. 每种教学方法都需要生成对应的讲课稿\n"); - prompt.append("5. 字数不少于800字\n"); - prompt.append("6. 以JSON数组格式输出,每个元素包含sort(排序)、methodId(教学方法Id)和text(讲课稿内容)、isDefault(是否默认,1:是,0:否),videoContent(讲课稿内容对应的视频内容)字段\n\n"); - - // 添加输出示例 - prompt.append("示例输出格式:\n"); - prompt.append("[\n"); - prompt.append(" {\n"); - prompt.append(" \"sort\": 1,\n"); - prompt.append(" \"methodId\": 385,\n"); - prompt.append(" \"isDefault\": 1,\n"); - prompt.append(" \"text\": \"这里是针对第一种教学方法的讲课稿内容...\",\n"); - prompt.append(" \"videoContent\": \"针对第一种教学方法的讲课稿内容对应的视频内容...\",\n"); - prompt.append(" },\n"); - prompt.append(" {\n"); - prompt.append(" \"sort\": 2,\n"); - prompt.append(" \"methodId\": 386,\n"); - prompt.append(" \"isDefault\": 0,\n"); - prompt.append(" \"text\": \"这里是针对第二种教学方法的讲课稿内容...\",\n"); - prompt.append(" \"videoContent\": \"针对第二种教学方法的讲课稿内容对应的视频内容...\",\n"); - prompt.append(" }\n"); - prompt.append(" {\n"); - prompt.append(" \"sort\": 3,\n"); - prompt.append(" \"isDefault\": 0,\n"); - prompt.append(" \"methodId\": 387,\n"); - prompt.append(" \"text\": \"这里是针对第三种教学方法的讲课稿内容...\",\n"); - prompt.append(" \"videoContent\": \"针对第三种教学方法的讲课稿内容对应的视频内容...\",\n"); - prompt.append("]"); - - return prompt.toString(); + return """ + 你是一位经验丰富的教师,请根据以下教学目标和教学重点,针对每种教学方法分别生成一份详细且有针对性的讲课稿。 + + 教学目标: %s + 教学重点: %s + %s%s + 教学方法列表: + %s + + 请根据以上教学目标、重点和教学方法生成详细的讲课稿,要求如下: + 1. 内容详实,语言生动,适合课堂讲解 + 2. 结构清晰,逻辑性强 + 3. 包含导入、新授、巩固练习、小结等教学环节 + 4. 每种教学方法都需要生成对应的讲课稿 + 5. 字数不少于800字 + 6. 以JSON数组格式输出,每个元素包含sort(排序)、methodId(教学方法Id)和text(讲课稿内容)、isDefault(是否默认,1:是,0:否),videoContent(讲课稿内容对应的视频内容)字段 + + + 示例输出格式: + [ + { + "sort": 1, + "methodId": 385, + "isDefault": 1, + "text": "这里是针对第一种教学方法的讲课稿内容...", + "videoContent": "针对第一种教学方法的讲课稿内容对应的视频内容...", + }, + { + "sort": 2, + "methodId": 386, + "isDefault": 0, + "text": "这里是针对第二种教学方法的讲课稿内容...", + "videoContent": "针对第二种教学方法的讲课稿内容对应的视频内容...", + } + { + "sort": 3, + "isDefault": 0, + "methodId": 387, + "text": "这里是针对第三种教学方法的讲课稿内容...", + "videoContent": "针对第三种教学方法的讲课稿内容对应的视频内容...", + } + ] + """.formatted( + teachGoal.getGoal(), + teachGoal.getPoint(), + Objects.nonNull(req.getGradeIds()) ? "对应的年级:" + getGradeName(req.getGradeId()) + "\n" : "", + Objects.nonNull(req.getSubjectId()) ? "对应的学科:" + getSubjectName(req.getSubjectId()) + "\n\n" : "", + methodsBuilder.toString() + ); } public static String buildPromptForTeachGoalGeneration(KnowledgePointEntity knowledgePoint, Integer count, GenerateTaskReq req) { - StringBuilder prompt = new StringBuilder(); - prompt.append("你是一位资深的'").append(getGradeTypeName(req.getGradeType())).append("'教学专家,请根据以下知识点生成").append(count).append("个教学目标(考点)。\n\n"); - - // 添加知识点信息 - prompt.append("知识点名称:").append(knowledgePoint.getKnowledgePointName()).append("\n"); - prompt.append("知识点总结:").append(req.getSummary()).append("\n"); - prompt.append("对应的年级:").append(getGradeNameList(req.getGradeIds())).append("\n"); - prompt.append("对应的学科:").append(getSubjectName(req.getSubjectId())).append("\n\n"); - - prompt.append("要求:\n"); - prompt.append(" 1. 教学目标必须基于\"").append(knowledgePoint.getKnowledgePointName()).append("\"这个知识点\n"); - prompt.append(" 2. 教学目标应适合对应年级的学生理解和掌握\n"); - prompt.append(" 3. 每个教学目标应包含具体的学习成果描述\n"); - prompt.append(" 4. 提供对应的教学重点\n"); - prompt.append(" 5. 可以提供推荐的视频教学链接(可选)\n"); - prompt.append(" 6. 每个教学目标需要有一个排序编号\n"); - prompt.append(" 7. 输出的格式必须是json格式,字段如下:\n"); - prompt.append(" - goal: 教学目标内容\n"); - prompt.append(" - point: 教学重点\n"); - prompt.append(" - sort: 排序编号\n\n"); - - prompt.append("输出格式示例:\n"); - prompt.append("[\n"); - prompt.append(" {\n"); - prompt.append(" \"goal\": \"教学目标内容\",\n"); - prompt.append(" \"point\": \"教学重点\",\n"); - prompt.append(" \"sort\": 1,\n"); - prompt.append(" }\n"); - prompt.append("]\n"); - - return prompt.toString(); + return """ + 你是一位资深的'%s'教学专家,请根据以下知识点生成%d个教学目标(考点)。 + + 知识点名称:%s + 知识点总结:%s + 对应的年级:%s + 对应的学科:%s + + + 要求: + 1. 教学目标必须基于"%s"这个知识点 + 2. 教学目标应适合对应年级的学生理解和掌握 + 3. 每个教学目标应包含具体的学习成果描述 + 4. 提供对应的教学重点 + 5. 可以提供推荐的视频教学链接(可选) + 6. 每个教学目标需要有一个排序编号 + 7. 输出的格式必须是json格式,字段如下: + - goal: 教学目标内容 + - point: 教学重点 + - sort: 排序编号 + + + 输出格式示例: + [ + { + "goal": "教学目标内容", + "point": "教学重点", + "sort": 1, + } + ] + """.formatted( + getGradeTypeName(req.getGradeType()), + count, + knowledgePoint.getKnowledgePointName(), + req.getSummary(), + getGradeNameList(req.getGradeIds()), + getSubjectName(req.getSubjectId()), + knowledgePoint.getKnowledgePointName() + ); } /** @@ -409,69 +433,74 @@ public class PromptBuilder { * @return 提示词 */ public static String buildPromptForTeachTextVideoScriptGeneration(TeachTextsEntity text) { - StringBuilder prompt = new StringBuilder(); - - prompt.append("请根据以下视频内容描述,生成一个完整的Python脚本,该脚本能够直接生成高质量的教学视频。\n\n"); - - prompt.append("## 视频内容描述:\n"); - prompt.append(text.getVideoContent()); - prompt.append("\n\n"); - - prompt.append("## 核心要求:\n"); - prompt.append("1. **视频类型**: 生成的是纯视觉教学视频,不需要包含任何字幕或音频\n"); - prompt.append("2. **视频质量**: 必须是高质量的视觉呈现,包括清晰的图形、动画和视觉效果\n"); - prompt.append("3. **教学重点**: 通过视觉元素(图表、动画、代码演示、视觉示例等)清晰地传达教学内容\n\n"); - - prompt.append("## Python代码要求:\n"); - - prompt.append("### 1. 代码结构:\n"); - prompt.append("- 必须是完整的、可直接运行的独立脚本\n"); - prompt.append("- 包含所有必要的导入语句和依赖\n"); - prompt.append("- 包含主程序入口,可直接执行生成视频\n\n"); - - prompt.append("### 2. 视觉生成要求:\n"); - prompt.append("- **使用专业的视觉库**: 如Matplotlib, Plotly, Seaborn, MoviePy, OpenCV, PIL等\n"); - prompt.append("- **高质量视觉效果**:\n"); - prompt.append(" - 清晰美观的图表和可视化\n"); - prompt.append(" - 流畅的动画和过渡效果\n"); - prompt.append(" - 合适的颜色搭配和视觉层次\n"); - prompt.append(" - 适当的文本标注(作为图形的一部分,而非字幕)\n"); - prompt.append("- **视觉叙事**: 通过视觉序列清晰地展示概念演变或步骤流程\n\n"); - - prompt.append("### 3. 代码质量:\n"); - prompt.append("- **完整注释**:\n"); - prompt.append(" - 文件头部:说明视频主题、主要视觉元素和依赖\n"); - prompt.append(" - 函数/类:详细说明功能和参数\n"); - prompt.append(" - 关键步骤:解释视觉生成逻辑\n"); - prompt.append("- **错误处理**: 包含适当的异常处理和边界情况\n"); - prompt.append("- **模块化设计**: 将不同功能封装为函数或类\n\n"); - - prompt.append("### 4. 依赖管理:\n"); - prompt.append("- 在文件开头通过注释明确列出所有第三方库\n"); - prompt.append("- 提供pip安装命令的注释\n\n"); - - prompt.append("### 5. 特别注意事项:\n"); - prompt.append("- **不要生成音频处理代码**(如声音添加、音轨处理)\n"); - prompt.append("- **不要生成字幕相关代码**(如字幕叠加、文本时间轴)\n"); - prompt.append("- **专注于视觉表达**:通过图形、动画、图表等视觉元素传达信息\n"); - prompt.append("- **视频时长**:根据内容复杂度,生成合理时长的视频(通常在1-5分钟)\n\n"); - - prompt.append("## 输出格式:\n"); - prompt.append("严格按以下JSON格式输出,不要包含任何解释或额外文本:\n"); - prompt.append("[\n"); - prompt.append(" {\n"); - prompt.append(" \"pythonScript\": \"完整的Python脚本代码,可直接运行生成高质量无字幕无音频的教学视频\"\n"); - prompt.append(" }\n"); - prompt.append("]\n\n"); - - prompt.append("## 示例思考方向(根据具体内容调整):\n"); - prompt.append("- 如果是编程教学:生成代码执行过程的可视化动画\n"); - prompt.append("- 如果是数学教学:生成公式推导的逐步动画演示\n"); - prompt.append("- 如果是数据科学:生成数据处理流程的可视化\n"); - prompt.append("- 如果是算法教学:生成算法步骤的动态演示\n"); - prompt.append("- 如果是概念讲解:生成概念演变的分步图示\n"); - - return prompt.toString(); + return """ + 请根据以下视频内容描述,生成一个完整的Python脚本,该脚本能够直接生成高质量的教学视频。 + + ## 视频内容描述: + %s + + + ## 核心要求: + 1. **视频类型**: 生成的是纯视觉教学视频,不需要包含任何字幕或音频 + 2. **视频质量**: 必须是高质量的视觉呈现,包括清晰的图形、动画和视觉效果 + 3. **教学重点**: 通过视觉元素(图表、动画、代码演示、视觉示例等)清晰地传达教学内容 + + + ## Python代码要求: + + ### 1. 代码结构: + - 必须是完整的、可直接运行的独立脚本 + - 包含所有必要的导入语句和依赖 + - 包含主程序入口,可直接执行生成视频 + + + ### 2. 视觉生成要求: + - **使用专业的视觉库**: 如Matplotlib, Plotly, Seaborn, MoviePy, OpenCV, PIL等 + - **高质量视觉效果**: + - 清晰美观的图表和可视化 + - 流畅的动画和过渡效果 + - 合适的颜色搭配和视觉层次 + - 适当的文本标注(作为图形的一部分,而非字幕) + - **视觉叙事**: 通过视觉序列清晰地展示概念演变或步骤流程 + + + ### 3. 代码质量: + - **完整注释**: + - 文件头部:说明视频主题、主要视觉元素和依赖 + - 函数/类:详细说明功能和参数 + - 关键步骤:解释视觉生成逻辑 + - **错误处理**: 包含适当的异常处理和边界情况 + - **模块化设计**: 将不同功能封装为函数或类 + + + ### 4. 依赖管理: + - 在文件开头通过注释明确列出所有第三方库 + - 提供pip安装命令的注释 + + + ### 5. 特别注意事项: + - **不要生成音频处理代码**(如声音添加、音轨处理) + - **不要生成字幕相关代码**(如字幕叠加、文本时间轴) + - **专注于视觉表达**:通过图形、动画、图表等视觉元素传达信息 + - **视频时长**:根据内容复杂度,生成合理时长的视频(通常在1-5分钟) + + + ## 输出格式: + 严格按以下JSON格式输出,不要包含任何解释或额外文本: + [ + { + "pythonScript": "完整的Python脚本代码,可直接运行生成高质量无字幕无音频的教学视频" + } + ] + + + ## 示例思考方向(根据具体内容调整): + - 如果是编程教学:生成代码执行过程的可视化动画 + - 如果是数学教学:生成公式推导的逐步动画演示 + - 如果是数据科学:生成数据处理流程的可视化 + - 如果是算法教学:生成算法步骤的动态演示 + - 如果是概念讲解:生成概念演变的分步图示 + """.formatted(text.getVideoContent()); } private static boolean isNeedLatex(Integer subjectId) { @@ -486,91 +515,57 @@ public class PromptBuilder { * @return 题型描述 */ private static String getQuestionTypeText(Integer type) { - switch (type) { - case 1: - return "选择题"; - case 2: - return "多选选择题"; - case 3: - return "问答题"; - case 4: - return "判断题"; - case 5: - return "计算题"; - case 6: - return "填空题"; - default: - return "其他题型"; - } + return switch (type) { + case 1 -> "选择题"; + case 2 -> "多选选择题"; + case 3 -> "问答题"; + case 4 -> "判断题"; + case 5 -> "计算题"; + case 6 -> "填空题"; + default -> "其他题型"; + }; } private static String getGradeTypeName(Integer gradeType) { - switch (gradeType) { - case 1: - return "小学"; - case 2: - return "初中"; - case 3: - return "高中"; - default: - throw new RuntimeException("未知的学段类型"); - } + return switch (gradeType) { + case 1 -> "小学"; + case 2 -> "初中"; + case 3 -> "高中"; + default -> throw new RuntimeException("未知的学段类型"); + }; } private static String getSubjectName(Integer subjectId) { - switch (subjectId) { - case 1: - return "语文"; - case 2: - return "数学"; - case 3: - return "英语"; - case 4: - return "品德与生活"; - case 5: - return "科学"; - case 6: - return "历史"; - case 7: - return "地理"; - case 8: - return "生物学"; - case 9: - return "政治"; - case 10: - return "物理"; - case 11: - return "化学"; - default: - throw new RuntimeException("未知的学科类型"); - } + return switch (subjectId) { + case 1 -> "语文"; + case 2 -> "数学"; + case 3 -> "英语"; + case 4 -> "品德与生活"; + case 5 -> "科学"; + case 6 -> "历史"; + case 7 -> "地理"; + case 8 -> "生物学"; + case 9 -> "政治"; + case 10 -> "物理"; + case 11 -> "化学"; + default -> throw new RuntimeException("未知的学科类型"); + }; } private static String getGradeName(Integer gradeId) { - switch (gradeId) { - case 1: - return "一年级"; - case 2: - return "二年级"; - case 3: - return "三年级"; - case 4: - return "四年级"; - case 5: - return "五年级"; - case 6: - return "六年级"; - case 7: - return "初一"; - case 8: - return "初二"; - case 9: - return "初三"; - case 10: - return "高中"; - default: - throw new RuntimeException("未知的学科类型"); - } + return switch (gradeId) { + case 1 -> "一年级"; + case 2 -> "二年级"; + case 3 -> "三年级"; + case 4 -> "四年级"; + case 5 -> "五年级"; + case 6 -> "六年级"; + case 7 -> "初一"; + case 8 -> "初二"; + case 9 -> "初三"; + case 10 -> "高中"; + default -> throw new RuntimeException("未知的学科类型"); + }; } private static String getGradeNameList(Collection gradeIds) { diff --git a/seer-teacher/seer-teacher-service/src/main/java/com/seer/teach/teacher/service/IAiScenarioConfigService.java b/seer-teacher/seer-teacher-service/src/main/java/com/seer/teach/teacher/service/IAiScenarioConfigService.java index 4e8d0d2..d11956e 100644 --- a/seer-teacher/seer-teacher-service/src/main/java/com/seer/teach/teacher/service/IAiScenarioConfigService.java +++ b/seer-teacher/seer-teacher-service/src/main/java/com/seer/teach/teacher/service/IAiScenarioConfigService.java @@ -8,9 +8,9 @@ import com.seer.teach.teacher.module.entity.AiScenarioConfigEntity; * AI场景配置表 服务类 *

* - * @author System - * @since 2025-10-27 */ public interface IAiScenarioConfigService extends IService { + + AiScenarioConfigEntity getOneByScenarioCode(String scenarioCode); } \ No newline at end of file diff --git a/seer-teacher/seer-teacher-service/src/main/java/com/seer/teach/teacher/service/impl/AiScenarioConfigServiceImpl.java b/seer-teacher/seer-teacher-service/src/main/java/com/seer/teach/teacher/service/impl/AiScenarioConfigServiceImpl.java index b51dcb6..b88ef98 100644 --- a/seer-teacher/seer-teacher-service/src/main/java/com/seer/teach/teacher/service/impl/AiScenarioConfigServiceImpl.java +++ b/seer-teacher/seer-teacher-service/src/main/java/com/seer/teach/teacher/service/impl/AiScenarioConfigServiceImpl.java @@ -1,9 +1,16 @@ package com.seer.teach.teacher.service.impl; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.seer.teach.common.constants.CommonConstant; +import com.seer.teach.common.enums.ResultCodeEnum; +import com.seer.teach.common.utils.AssertUtils; import com.seer.teach.teacher.module.entity.AiScenarioConfigEntity; +import com.seer.teach.teacher.module.entity.AiScenarioEntity; import com.seer.teach.teacher.module.mapper.AiScenarioConfigMapper; import com.seer.teach.teacher.service.IAiScenarioConfigService; +import com.seer.teach.teacher.service.IAiScenarioService; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; /** @@ -14,7 +21,23 @@ import org.springframework.stereotype.Service; * @author System * @since 2025-10-27 */ +@RequiredArgsConstructor @Service public class AiScenarioConfigServiceImpl extends ServiceImpl implements IAiScenarioConfigService { + private final IAiScenarioService aiScenarioService; + + + @Override + public AiScenarioConfigEntity getOneByScenarioCode(String scenarioCode) { + LambdaQueryWrapper scenarioWrapper = new LambdaQueryWrapper<>(); + scenarioWrapper.eq(AiScenarioEntity::getScenarioCode, scenarioCode); + AiScenarioEntity aiScenario = aiScenarioService.getOne(scenarioWrapper); + AssertUtils.notNull(aiScenario, ResultCodeEnum.SCENARIO_NOT_FOUND); + LambdaQueryWrapper configWrapper = new LambdaQueryWrapper<>(); + configWrapper.eq(AiScenarioConfigEntity::getScenarioId, aiScenario.getId()); + configWrapper.eq(AiScenarioConfigEntity::getEnabled, CommonConstant.ENABLE); + configWrapper.orderByAsc(AiScenarioConfigEntity::getPriority); + return super.getOne(configWrapper); + } } \ No newline at end of file From 9b90abf8e8e0a9868ec609481daf44ee97ded5cf Mon Sep 17 00:00:00 2001 From: Wang Date: Sat, 27 Dec 2025 18:24:59 +0800 Subject: [PATCH 4/6] =?UTF-8?q?=E5=9F=BA=E4=BA=8Espring=20ai=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0dashscope,openpi=E7=9A=84chat=20client?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/seer/teach/ai/service/AiChatModelService.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/service/AiChatModelService.java b/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/service/AiChatModelService.java index 3024b20..1e62043 100644 --- a/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/service/AiChatModelService.java +++ b/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/service/AiChatModelService.java @@ -40,7 +40,7 @@ public class AiChatModelService { private final AiModelClientFactory aiModelClientFactory; - public ChatResponse chatMessage(String scenarioCode, Object params){ + public ChatResponse chatMessage(String scenarioCode, Object prompt){ AiScenarioConfigEntity scenarioConfig = aiScenarioConfigService.getOneByScenarioCode(scenarioCode); log.info("scenarioConfig:{}", scenarioConfig); @@ -53,8 +53,7 @@ public class AiChatModelService { ChatModel chatModel = client.createChatModel(modelConfig).orElseThrow(() -> new CommonException(ResultCodeEnum.AI_MODEL_NOT_FOUND)); Optional options = client.createChatOptions(BeanUtil.beanToMap(scenarioConfig)); - Prompt prompt = new Prompt(params.toString(), options.orElse(null)); - return chatModel.call(prompt); + return chatModel.call(new Prompt(prompt.toString(), options.orElse(null))); } public Flux chatStream(String scenarioCode, Object params){ From 3e1e89c213588898e500787a87979a313c4697e4 Mon Sep 17 00:00:00 2001 From: Wang Date: Mon, 29 Dec 2025 17:00:47 +0800 Subject: [PATCH 5/6] =?UTF-8?q?=E5=9F=BA=E4=BA=8Espring-ai=E7=94=9F?= =?UTF-8?q?=E6=88=90=E6=95=99=E5=AD=A6=E7=9B=AE=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../teach/admin/SeerAdminApplication.java | 2 +- seer-admin/src/main/resources/application.yml | 6 +- seer-dependencies/pom.xml | 19 +++- seer-teacher/seer-teacher-ai/pom.xml | 7 +- .../client/model/AbstractAiModelClient.java | 6 +- .../teach/ai/client/model/AiModelClient.java | 12 --- .../model/config/DashScopeClientConfig.java | 99 +++++++++++++++++++ .../model/dashscope/DashScopeModelClient.java | 26 ++--- .../model/openai/OpenAIModelClient.java | 12 +-- .../seer-teacher-service-admin/pom.xml | 6 ++ .../impl/AbstractGenerationService.java | 15 ++- 11 files changed, 153 insertions(+), 57 deletions(-) create mode 100644 seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/config/DashScopeClientConfig.java diff --git a/seer-admin/src/main/java/com/seer/teach/admin/SeerAdminApplication.java b/seer-admin/src/main/java/com/seer/teach/admin/SeerAdminApplication.java index 121946b..367799f 100644 --- a/seer-admin/src/main/java/com/seer/teach/admin/SeerAdminApplication.java +++ b/seer-admin/src/main/java/com/seer/teach/admin/SeerAdminApplication.java @@ -15,7 +15,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; @Slf4j @EnableFeignClients(basePackages = "com.seer.teach.*.api") -@SpringBootApplication(scanBasePackages = "com.seer",exclude = QuartzAutoConfiguration.class) +@SpringBootApplication(scanBasePackages = "com.seer",exclude = {QuartzAutoConfiguration.class}) @EnableTransactionManagement @EnableAspectJAutoProxy @EnableAsync diff --git a/seer-admin/src/main/resources/application.yml b/seer-admin/src/main/resources/application.yml index d4574a2..56d71aa 100644 --- a/seer-admin/src/main/resources/application.yml +++ b/seer-admin/src/main/resources/application.yml @@ -11,13 +11,8 @@ spring: main: allow-bean-definition-overriding: true allow-circular-references: true - mvc: - profiles: active: dev - mvc: - pathmatch: - matching-strategy: ant_path_matcher flyway: enabled: true locations: classpath:db/mysql @@ -39,6 +34,7 @@ spring: server-addr: 192.168.0.39:8848 # 配置中心地址 file-extension: yaml # 配置文件后缀(yaml/properties) namespace: ${spring.profiles.active} + #日志 logging: config: classpath:logback-${spring.profiles.active}.xml diff --git a/seer-dependencies/pom.xml b/seer-dependencies/pom.xml index 9577480..38a7611 100644 --- a/seer-dependencies/pom.xml +++ b/seer-dependencies/pom.xml @@ -16,7 +16,7 @@ 2025.0.0.0 1.1.2 - 1.1.0.0-M5 + 1.1.0.0-RC2 @@ -660,6 +660,12 @@ ${spring-ai-alibaba.version} + + com.alibaba.cloud.ai + spring-ai-alibaba-dashscope + ${spring-ai-alibaba.version} + + org.springframework.ai spring-ai-starter-model-deepseek @@ -668,8 +674,17 @@ org.springframework.ai - spring-ai-starter-model-deepseek + spring-ai-deepseek ${spring-ai.version} + compile + true + + + + org.springframework.ai + spring-ai-openai + ${spring-ai.version} + compile diff --git a/seer-teacher/seer-teacher-ai/pom.xml b/seer-teacher/seer-teacher-ai/pom.xml index 6af3ff8..ac91d20 100644 --- a/seer-teacher/seer-teacher-ai/pom.xml +++ b/seer-teacher/seer-teacher-ai/pom.xml @@ -26,17 +26,18 @@ com.alibaba.cloud.ai - spring-ai-alibaba-starter-dashscope + spring-ai-alibaba-dashscope org.springframework.ai - spring-ai-starter-model-openai + spring-ai-openai + compile org.springframework.ai - spring-ai-starter-model-deepseek + spring-ai-deepseek diff --git a/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/AbstractAiModelClient.java b/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/AbstractAiModelClient.java index f53bd21..2506171 100644 --- a/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/AbstractAiModelClient.java +++ b/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/AbstractAiModelClient.java @@ -5,6 +5,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.ai.model.tool.DefaultToolExecutionEligibilityPredicate; import org.springframework.ai.model.tool.ToolCallingManager; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.retry.support.RetryTemplate; import org.springframework.web.client.ResponseErrorHandler; import org.springframework.web.client.RestClient; @@ -24,9 +25,11 @@ public abstract class AbstractAiModelClient implements AiModelClient { protected ResponseErrorHandler responseErrorHandler; @Autowired(required = false) + @Qualifier("aiRestClient") protected RestClient.Builder restClientBuilder; @Autowired(required = false) + @Qualifier("aiWebClient") protected WebClient.Builder webClientBuilder; @Autowired @@ -35,8 +38,7 @@ public abstract class AbstractAiModelClient implements AiModelClient { @Autowired protected ToolCallingManager toolCallingManager; - @Autowired(required = false) - protected ObservationRegistry observationRegistry; + protected ObservationRegistry observationRegistry = ObservationRegistry.create(); protected final DefaultToolExecutionEligibilityPredicate toolExecutionEligibilityPredicate = new DefaultToolExecutionEligibilityPredicate(); diff --git a/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/AiModelClient.java b/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/AiModelClient.java index 0b1a2f0..59035ad 100644 --- a/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/AiModelClient.java +++ b/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/AiModelClient.java @@ -1,6 +1,5 @@ package com.seer.teach.ai.client.model; -import com.alibaba.cloud.ai.dashscope.audio.synthesis.SpeechSynthesisModel; import com.seer.teach.ai.client.model.config.ModelConfig; import org.springframework.ai.audio.tts.TextToSpeechModel; import org.springframework.ai.chat.model.ChatModel; @@ -43,17 +42,6 @@ public interface AiModelClient { return Optional.empty(); } - /** - * 获取语音合成模型(Speech Synthesis Model) - * 用于将文本转换为语音音频,仅dashscope平台支持 - * - * @param modelConfig 模型配置 - * @return 语音合成模型实例的Optional包装 - */ - default Optional createSpeechSynthesisModel(ModelConfig modelConfig) { - return Optional.empty(); - } - /** * 获取文本转语音模型(Text To Speech Model) * 用于将文本转换为语音输出,支持多种平台 diff --git a/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/config/DashScopeClientConfig.java b/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/config/DashScopeClientConfig.java new file mode 100644 index 0000000..737fa4b --- /dev/null +++ b/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/config/DashScopeClientConfig.java @@ -0,0 +1,99 @@ +package com.seer.teach.ai.client.model.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.http.codec.json.Jackson2JsonDecoder; +import org.springframework.http.codec.json.Jackson2JsonEncoder; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.web.client.RestClient; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +/** + * DashScope API 配置类 + * 用于配置restClient以正确处理聊天完成请求 + */ +@Slf4j +@Configuration +public class DashScopeClientConfig { + + /** + * 创建用于DashScope API的RestClient + */ + @Bean + public RestClient aiRestClient() { + ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json() + .modules(new JavaTimeModule()) + .build(); + + MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(objectMapper); + + return RestClient.builder() + .requestInterceptor(new LoggingInterceptor()) + .messageConverters(converters -> converters.add(converter)) + .build(); + } + + /** + * 创建用于DashScope API的WebClient并启用日志 + */ + @Bean + public WebClient aiWebClient() { + ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json() + .modules(new JavaTimeModule()) + .build(); + + return WebClient.builder() + .codecs(configurer -> configurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(objectMapper))) + .codecs(configurer -> configurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(objectMapper))) + .filter(ExchangeFilterFunction.ofRequestProcessor(clientRequest -> { + log.info("Request: {} {}", clientRequest.method(), clientRequest.url()); + clientRequest.headers().forEach((name, values) -> + values.forEach(value -> log.info("{}={}", name, value))); + return Mono.just(clientRequest); + })) + .filter(ExchangeFilterFunction.ofResponseProcessor(clientResponse -> { + log.info("Response Status: {}", clientResponse.statusCode()); + clientResponse.headers().asHttpHeaders().forEach((name, values) -> + values.forEach(value -> log.info("{}={}", name, value))); + return Mono.just(clientResponse); + })) + .build(); + } + + + public static class LoggingInterceptor implements ClientHttpRequestInterceptor { + + @Override + public ClientHttpResponse intercept( + HttpRequest request, + byte[] body, + ClientHttpRequestExecution execution) throws IOException { + + // 记录请求信息 + log.info("HTTP请求方法: {}", request.getMethod()); + log.info("HTTP请求URL: {}", request.getURI()); + log.info("HTTP请求头: {}", request.getHeaders()); + if (body.length > 0) { + log.info("HTTP请求体: {}", new String(body, StandardCharsets.UTF_8)); + } + + // 执行请求 + ClientHttpResponse response = execution.execute(request, body); + + return response; + } + } +} \ No newline at end of file diff --git a/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/dashscope/DashScopeModelClient.java b/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/dashscope/DashScopeModelClient.java index 8ec0fce..6e93ce1 100644 --- a/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/dashscope/DashScopeModelClient.java +++ b/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/dashscope/DashScopeModelClient.java @@ -1,14 +1,10 @@ package com.seer.teach.ai.client.model.dashscope; -import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeChatProperties; -import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeConnectionProperties; -import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeEmbeddingProperties; import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; import com.alibaba.cloud.ai.dashscope.api.DashScopeAudioSpeechApi; import com.alibaba.cloud.ai.dashscope.api.DashScopeImageApi; import com.alibaba.cloud.ai.dashscope.audio.DashScopeAudioSpeechModel; import com.alibaba.cloud.ai.dashscope.audio.DashScopeAudioSpeechOptions; -import com.alibaba.cloud.ai.dashscope.audio.synthesis.SpeechSynthesisModel; import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingModel; @@ -17,12 +13,11 @@ import com.alibaba.cloud.ai.dashscope.image.DashScopeImageOptions; import com.seer.teach.ai.client.model.AbstractAiModelClient; import com.seer.teach.ai.client.model.config.ModelConfig; import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.audio.tts.TextToSpeechModel; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.prompt.ChatOptions; -import org.springframework.ai.document.MetadataMode; import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.image.ImageModel; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.Map; @@ -40,14 +35,9 @@ import java.util.Optional; @Component public class DashScopeModelClient extends AbstractAiModelClient { - private DashScopeConnectionProperties connectionProperties; - - @Autowired(required = false) - private DashScopeChatProperties chatProperties; - @Override public String getPlatformName() { - return "dashscope"; + return "aliyun_bailian"; } @Override @@ -125,17 +115,15 @@ public class DashScopeModelClient extends AbstractAiModelClient { } @Override - public Optional createSpeechSynthesisModel(ModelConfig modelConfig) { + public Optional createTextToSpeechModel(ModelConfig modelConfig) { try { DashScopeAudioSpeechApi audioSpeechApi = new DashScopeAudioSpeechApi(modelConfig.getApiKey(), modelConfig.getUrl()); DashScopeAudioSpeechOptions options = DashScopeAudioSpeechOptions.builder() .model(modelConfig.getModel()) - .requestText(DashScopeAudioSpeechApi.RequestTextType.PLAIN_TEXT) .voice("longyingxiao") .build(); DashScopeAudioSpeechModel speechSynthesisModel = new DashScopeAudioSpeechModel(audioSpeechApi, options, retryTemplate); - return Optional.of(speechSynthesisModel); } catch (Exception e) { log.error("创建DashScope语音合成模型失败", e); @@ -147,10 +135,9 @@ public class DashScopeModelClient extends AbstractAiModelClient { @Override public Optional createEmbeddingModel(ModelConfig modelConfig) { try { - DashScopeEmbeddingProperties embeddingProperties = new DashScopeEmbeddingProperties(); DashScopeApi dashScopeApi = createDashScopeApi(modelConfig.getApiKey(), modelConfig.getUrl()); - DashScopeEmbeddingModel embeddingModel = new DashScopeEmbeddingModel(dashScopeApi, MetadataMode.EMBED, embeddingProperties.getOptions(), retryTemplate, observationRegistry); + DashScopeEmbeddingModel embeddingModel = new DashScopeEmbeddingModel(dashScopeApi); return Optional.of(embeddingModel); } catch (Exception e) { @@ -162,11 +149,10 @@ public class DashScopeModelClient extends AbstractAiModelClient { /** * 创建DashScope API实例 */ - private DashScopeApi createDashScopeApi(String apiKey, String baseUrl) { + private DashScopeApi createDashScopeApi(String apiKey, String completionsPath) { DashScopeApi.Builder builder = DashScopeApi.builder() .apiKey(apiKey) - .baseUrl(baseUrl); - + .baseUrl("https://dashscope.aliyuncs.com"); // 如果有可用的客户端构建器,添加它们 if (webClientBuilder != null) { builder.webClientBuilder(webClientBuilder); diff --git a/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/openai/OpenAIModelClient.java b/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/openai/OpenAIModelClient.java index 17c9aa7..229f54c 100644 --- a/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/openai/OpenAIModelClient.java +++ b/seer-teacher/seer-teacher-ai/src/main/java/com/seer/teach/ai/client/model/openai/OpenAIModelClient.java @@ -6,13 +6,9 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.ai.audio.tts.TextToSpeechModel; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.prompt.ChatOptions; -import org.springframework.ai.document.MetadataMode; import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.image.ImageModel; import org.springframework.ai.model.SimpleApiKey; -import org.springframework.ai.model.openai.autoconfigure.OpenAiAudioSpeechProperties; -import org.springframework.ai.model.openai.autoconfigure.OpenAiChatProperties; -import org.springframework.ai.model.openai.autoconfigure.OpenAiEmbeddingProperties; import org.springframework.ai.openai.OpenAiAudioSpeechModel; import org.springframework.ai.openai.OpenAiChatModel; import org.springframework.ai.openai.OpenAiChatOptions; @@ -131,8 +127,7 @@ public class OpenAIModelClient extends AbstractAiModelClient { .responseErrorHandler(responseErrorHandler) .build(); - OpenAiAudioSpeechProperties properties = new OpenAiAudioSpeechProperties(); - TextToSpeechModel textToSpeechModel = new OpenAiAudioSpeechModel(audioApi, properties.getOptions(), retryTemplate); + TextToSpeechModel textToSpeechModel = new OpenAiAudioSpeechModel(audioApi); return Optional.of(textToSpeechModel); } catch (Exception e) { log.error("创建OpenAI文本转语音模型失败", e); @@ -144,8 +139,7 @@ public class OpenAIModelClient extends AbstractAiModelClient { public Optional createEmbeddingModel(ModelConfig modelConfig) { try { OpenAiApi openAiApi = createOpenAiApi(modelConfig.getApiKey(), modelConfig.getUrl()); - OpenAiEmbeddingProperties properties = new OpenAiEmbeddingProperties(); - EmbeddingModel embeddingModel = new OpenAiEmbeddingModel(openAiApi, MetadataMode.EMBED,properties.getOptions(), retryTemplate); + EmbeddingModel embeddingModel = new OpenAiEmbeddingModel(openAiApi); return Optional.of(embeddingModel); } catch (Exception e) { log.error("创建OpenAI嵌入模型失败", e); @@ -157,8 +151,6 @@ public class OpenAIModelClient extends AbstractAiModelClient { return OpenAiApi.builder() .baseUrl(baseUrl) .apiKey(new SimpleApiKey(apiKey)) - .completionsPath(OpenAiChatProperties.DEFAULT_COMPLETIONS_PATH) - .embeddingsPath(OpenAiEmbeddingProperties.DEFAULT_EMBEDDINGS_PATH) .restClientBuilder(restClientBuilder) .webClientBuilder(webClientBuilder).build(); } diff --git a/seer-teacher/seer-teacher-service-admin/pom.xml b/seer-teacher/seer-teacher-service-admin/pom.xml index b8c6c34..7978baf 100644 --- a/seer-teacher/seer-teacher-service-admin/pom.xml +++ b/seer-teacher/seer-teacher-service-admin/pom.xml @@ -38,6 +38,12 @@ ${project.version} + + ${project.groupId} + seer-teacher-ai + ${project.version} + + com.squareup.okhttp3 okhttp diff --git a/seer-teacher/seer-teacher-service-admin/src/main/java/com/seer/teach/admin/service/impl/AbstractGenerationService.java b/seer-teacher/seer-teacher-service-admin/src/main/java/com/seer/teach/admin/service/impl/AbstractGenerationService.java index a4a3b97..c528da6 100644 --- a/seer-teacher/seer-teacher-service-admin/src/main/java/com/seer/teach/admin/service/impl/AbstractGenerationService.java +++ b/seer-teacher/seer-teacher-service-admin/src/main/java/com/seer/teach/admin/service/impl/AbstractGenerationService.java @@ -2,11 +2,14 @@ package com.seer.teach.admin.service.impl; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.seer.teach.ai.service.AiChatModelService; import com.seer.teach.common.constants.CommonConstant; import com.seer.teach.common.entity.BaseEntity; import com.seer.teach.teacher.service.AiModelCallService; import com.seer.teach.teacher.service.platform.LlmResponse; import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.model.Generation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.transaction.support.TransactionTemplate; @@ -16,6 +19,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.function.Function; /** @@ -41,6 +45,9 @@ public abstract class AbstractGenerationService { @Autowired protected TransactionTemplate transactionTemplate; + @Autowired + protected AiChatModelService chatModelService; + /** * 检查当前线程是否被中断 * @@ -78,6 +85,10 @@ public abstract class AbstractGenerationService { */ protected LlmResponse callLLM(String scenarioCode, String prompt, Map params, Integer userId) { log.info("调用大模型,场景编码: {}, 提示词长度: {}", scenarioCode, prompt.length()); + //ChatResponse chatResponse = chatModelService.chatMessage(scenarioCode, prompt); + LlmResponse response = new LlmResponse(); + //Generation result = chatResponse.getResult(); + return aiModelCallService.callModel(scenarioCode, prompt, params, userId); } @@ -160,8 +171,8 @@ public abstract class AbstractGenerationService { * @param saver 保存器函数 */ protected Integer executeBatchProcess(R request, - Function> generator, - java.util.function.Consumer> saver) { + Function> generator, + java.util.function.Consumer> saver) { // 检查任务是否被取消 String taskId = getTaskIdFromRequest(request); if (isThreadInterrupted() || isTaskCancelled(taskId)) { From da1f49001d551d47946ea1c6c4a61123401c4148 Mon Sep 17 00:00:00 2001 From: Wang Date: Mon, 29 Dec 2025 17:52:31 +0800 Subject: [PATCH 6/6] =?UTF-8?q?=E4=BF=AE=E6=94=B9seer-gateway=E7=9A=84?= =?UTF-8?q?=E7=BD=91=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- seer-gateway/pom.xml | 2 +- .../src/main/resources/application.yml | 39 +++++++++++++++++++ .../src/main/resources/logback-dev.xml | 4 +- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/seer-gateway/pom.xml b/seer-gateway/pom.xml index 86c8eb8..d408cd2 100644 --- a/seer-gateway/pom.xml +++ b/seer-gateway/pom.xml @@ -21,7 +21,7 @@ org.springframework.cloud - spring-cloud-starter-gateway + spring-cloud-starter-gateway-server-webflux diff --git a/seer-gateway/src/main/resources/application.yml b/seer-gateway/src/main/resources/application.yml index c59d0a2..f84994c 100644 --- a/seer-gateway/src/main/resources/application.yml +++ b/seer-gateway/src/main/resources/application.yml @@ -32,6 +32,45 @@ spring: server-addr: 192.168.0.39:8848 # 配置中心地址 file-extension: yaml # 配置文件后缀(yaml/properties) namespace: ${spring.profiles.active} + gateway: + discovery: + locator: + enabled: true # 开启通过服务发现创建路由 + lower-case-service-id: true # 服务名小写 + routes: + - id: seer-admin # 路由的编号 + uri: lb://seer-admin + predicates: + - Path=/seer/admin/** + - id: seer-teacher + uri: lb://seer-teacher + predicates: + - Path=/seer/teacher/** + - id: seer-iot + uri: lb://seer-iot + predicates: + - Path=/seer/iot/** + - id: seer-mall + uri: lb://seer-mall + predicates: + - Path=/seer/mall/** + - id: seer-pay + uri: lb://seer-pay + predicates: + - Path=/seer/pay/** + - id: seer-user + uri: lb://seer-user + predicates: + - Path=/seer/user/** + - id: seer-mp + uri: lb://seer-mp + predicates: + - Path=/seer/mp/** + - id: seer-open-api + uri: lb://seer-open-api + predicates: + - Path=/seer/open/rest/** + config: import: - optional:nacos:${spring.application.name}-${spring.profiles.active}.yaml diff --git a/seer-gateway/src/main/resources/logback-dev.xml b/seer-gateway/src/main/resources/logback-dev.xml index 86a3500..6c86e72 100644 --- a/seer-gateway/src/main/resources/logback-dev.xml +++ b/seer-gateway/src/main/resources/logback-dev.xml @@ -78,12 +78,12 @@ - + - +