From 3e1e89c213588898e500787a87979a313c4697e4 Mon Sep 17 00:00:00 2001 From: Wang Date: Mon, 29 Dec 2025 17:00:47 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9F=BA=E4=BA=8Espring-ai=E7=94=9F=E6=88=90?= =?UTF-8?q?=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)) {