基于spring-ai生成教学目标

This commit is contained in:
Wang 2025-12-29 17:00:47 +08:00
parent 9b90abf8e8
commit 3e1e89c213
11 changed files with 153 additions and 57 deletions

View File

@ -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

View File

@ -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

View File

@ -16,7 +16,7 @@
<spring-cloud-alibaba.version>2025.0.0.0</spring-cloud-alibaba.version>
<spring-ai.version>1.1.2</spring-ai.version>
<spring-ai-alibaba.version>1.1.0.0-M5</spring-ai-alibaba.version>
<spring-ai-alibaba.version>1.1.0.0-RC2</spring-ai-alibaba.version>
<!-- 认证 -->
@ -660,6 +660,12 @@
<version>${spring-ai-alibaba.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-dashscope</artifactId>
<version>${spring-ai-alibaba.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-deepseek</artifactId>
@ -668,8 +674,17 @@
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-deepseek</artifactId>
<artifactId>spring-ai-deepseek</artifactId>
<version>${spring-ai.version}</version>
<scope>compile</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai</artifactId>
<version>${spring-ai.version}</version>
<scope>compile</scope>
</dependency>
<dependency>

View File

@ -26,17 +26,18 @@
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
<artifactId>spring-ai-alibaba-dashscope</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
<artifactId>spring-ai-openai</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-deepseek</artifactId>
<artifactId>spring-ai-deepseek</artifactId>
</dependency>
<dependency>

View File

@ -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();

View File

@ -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<SpeechSynthesisModel> createSpeechSynthesisModel(ModelConfig modelConfig) {
return Optional.empty();
}
/**
* 获取文本转语音模型Text To Speech Model
* 用于将文本转换为语音输出支持多种平台

View File

@ -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;
}
}
}

View File

@ -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<SpeechSynthesisModel> createSpeechSynthesisModel(ModelConfig modelConfig) {
public Optional<TextToSpeechModel> 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<EmbeddingModel> 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);

View File

@ -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<EmbeddingModel> 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();
}

View File

@ -38,6 +38,12 @@
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>seer-teacher-ai</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>

View File

@ -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<T, R, E extends BaseEntity> {
@Autowired
protected TransactionTemplate transactionTemplate;
@Autowired
protected AiChatModelService chatModelService;
/**
* 检查当前线程是否被中断
*
@ -78,6 +85,10 @@ public abstract class AbstractGenerationService<T, R, E extends BaseEntity> {
*/
protected LlmResponse callLLM(String scenarioCode, String prompt, Map<String, Object> 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<T, R, E extends BaseEntity> {
* @param saver 保存器函数
*/
protected Integer executeBatchProcess(R request,
Function<R, List<T>> generator,
java.util.function.Consumer<List<T>> saver) {
Function<R, List<T>> generator,
java.util.function.Consumer<List<T>> saver) {
// 检查任务是否被取消
String taskId = getTaskIdFromRequest(request);
if (isThreadInterrupted() || isTaskCancelled(taskId)) {