Merge remote-tracking branch 'origin/master' into dev-chenjiajian

This commit is contained in:
嘉多宝宝 2025-12-30 09:39:07 +08:00
commit 92ad9f88a0
28 changed files with 1410 additions and 546 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

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

View File

@ -70,4 +70,9 @@ public interface CommonConstant {
* 任务锁前缀
*/
String TASK_CANCEL_PREFIX = "ai_task_cancel:";
/**
* 启用
*/
Integer ENABLE = 1;
}

View File

@ -15,8 +15,8 @@
<spring-cloud-openfeign.version>4.3.0</spring-cloud-openfeign.version>
<spring-cloud-alibaba.version>2025.0.0.0</spring-cloud-alibaba.version>
<spring-ai.version>2.0.0-SNAPSHOT</spring-ai.version>
<spring-ai-alibaba.version>1.0.0-M6.1</spring-ai-alibaba.version>
<spring-ai.version>1.1.2</spring-ai.version>
<spring-ai-alibaba.version>1.1.0.0-RC2</spring-ai-alibaba.version>
<!-- 认证 -->
@ -68,7 +68,7 @@
<guava.version>33.5.0-jre</guava.version>
<!-- 异步处理 -->
<spring-retry.version>1.2.5.RELEASE</spring-retry.version>
<spring-retry.version>2.0.8</spring-retry.version>
<!-- 接口文档 -->
<springdoc-openapi.version>2.8.14</springdoc-openapi.version>
@ -186,6 +186,13 @@
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Lombok -->
<dependency>
@ -646,6 +653,52 @@
</exclusions>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
<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>
<version>${spring-ai.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<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>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-ollama</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-agent-framework</artifactId>
<version>${spring-ai-alibaba.version}</version>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>

View File

@ -21,7 +21,7 @@
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<artifactId>spring-cloud-starter-gateway-server-webflux</artifactId>
</dependency>
<dependency>

View File

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

View File

@ -78,12 +78,12 @@
<logger name="org.springframework.data.redis" level="error"/>
<logger name="com.alibaba.nacos.shaded.io.grpc" level="error"/>
<logger name="com.alibaba.nacos.client.naming" level="WARN"/>
<logger name="org.springframework.cloud.gateway.route" level="error"/>
<logger name="org.springframework.cloud.gateway.route" level="debug"/>
<logger name="org.springframework.core.env" level="error"/>
<logger name="java.util.concurrent" level="error"/>
<!--将整个项目的日志设置为debug级别-->
<root level="info">
<root level="debug">
<appender-ref ref="FILE_ERROR"/>
<appender-ref ref="FILE_WARN"/>
<appender-ref ref="FILE_INFO"/>

View File

@ -19,105 +19,6 @@
<module>seer-teacher-service-bootstrap</module>
<module>seer-teacher-api</module>
<module>seer-teacher-service-admin</module>
<module>seer-teacher-ai</module>
</modules>
<dependencies>
<!--繁体转简体-->
<dependency>
<groupId>com.github.houbb</groupId>
<artifactId>opencc4j</artifactId>
<version>1.6.0</version>
</dependency>
<!--MinIO依赖-->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.1</version>
</dependency>
<!--protobaffer-->
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>4.30.2</version>
</dependency>
<!-- 加密库 -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<!--MybatisPlus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
</dependency>
<!-- 代码自动生成器依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
</dependency>
<!--代码生成器需要的引擎-->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
</dependency>
<!--AOP编程jar包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- fastjson2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<!--参数校验-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>
<!-- Sa-Token 权限认证在线文档https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<!-- IP地址检索 -->
<dependency>
<groupId>org.lionsoul</groupId>
<artifactId>ip2region</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.seer.teach</groupId>
<artifactId>seer-teacher</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>seer-teacher-ai</artifactId>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>seer-teacher-service</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-dashscope</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-deepseek</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-ollama</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-observation</artifactId>
<version>1.15.7</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

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

View File

@ -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<String, AiModelClient> serviceMap = new ConcurrentHashMap<>();
public AiModelClientFactoryImpl(List<AiModelClient> 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);
}
}

View File

@ -0,0 +1,45 @@
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.beans.factory.annotation.Qualifier;
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)
@Qualifier("aiRestClient")
protected RestClient.Builder restClientBuilder;
@Autowired(required = false)
@Qualifier("aiWebClient")
protected WebClient.Builder webClientBuilder;
@Autowired
protected RetryTemplate retryTemplate;
@Autowired
protected ToolCallingManager toolCallingManager;
protected ObservationRegistry observationRegistry = ObservationRegistry.create();
protected final DefaultToolExecutionEligibilityPredicate toolExecutionEligibilityPredicate = new DefaultToolExecutionEligibilityPredicate();
}

View File

@ -0,0 +1,66 @@
package com.seer.teach.ai.client.model;
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<ChatModel> createChatModel(ModelConfig modelConfig){
return Optional.empty();
}
default Optional<ChatOptions> createChatOptions(Map<String, Object> options){
return Optional.empty();
}
/**
* 获取图像模型
*/
default Optional<ImageModel> createImageModel(ModelConfig modelConfig){
return Optional.empty();
}
/**
* 获取文本转语音模型Text To Speech Model
* 用于将文本转换为语音输出支持多种平台
*
* @param modelConfig 模型配置
* @return 文本转语音模型实例的Optional包装
*/
default Optional<TextToSpeechModel> createTextToSpeechModel(ModelConfig modelConfig) {
return Optional.empty();
}
/**
* 获取嵌入模型Embedding Model
* 用于将文本转换为向量表示支持多种平台
*
* @param modelConfig 模型配置
* @return 嵌入模型实例的Optional包装
*/
default Optional<EmbeddingModel> createEmbeddingModel(ModelConfig modelConfig) {
return Optional.empty();
}
}

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

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

View File

@ -0,0 +1,177 @@
package com.seer.teach.ai.client.model.dashscope;
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.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.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.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 {
@Override
public String getPlatformName() {
return "aliyun_bailian";
}
@Override
public Optional<ChatModel> 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<ChatOptions> createChatOptions(Map<String, Object> options) {
DashScopeChatOptions.DashScopeChatOptionsBuilder builder = DashScopeChatOptions.builder();
for (Map.Entry<String, Object> 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<ImageModel> 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<TextToSpeechModel> createTextToSpeechModel(ModelConfig modelConfig) {
try {
DashScopeAudioSpeechApi audioSpeechApi = new DashScopeAudioSpeechApi(modelConfig.getApiKey(), modelConfig.getUrl());
DashScopeAudioSpeechOptions options = DashScopeAudioSpeechOptions.builder()
.model(modelConfig.getModel())
.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<EmbeddingModel> createEmbeddingModel(ModelConfig modelConfig) {
try {
DashScopeApi dashScopeApi = createDashScopeApi(modelConfig.getApiKey(), modelConfig.getUrl());
DashScopeEmbeddingModel embeddingModel = new DashScopeEmbeddingModel(dashScopeApi);
return Optional.of(embeddingModel);
} catch (Exception e) {
log.error("创建DashScope嵌入模型失败", e);
return Optional.empty();
}
}
/**
* 创建DashScope API实例
*/
private DashScopeApi createDashScopeApi(String apiKey, String completionsPath) {
DashScopeApi.Builder builder = DashScopeApi.builder()
.apiKey(apiKey)
.baseUrl("https://dashscope.aliyuncs.com");
// 如果有可用的客户端构建器添加它们
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();
}
}

View File

@ -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<ChatModel> 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<ChatOptions> createChatOptions(Map<String, Object> options) {
return Optional.empty();
}
}

View File

@ -0,0 +1,157 @@
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.embedding.EmbeddingModel;
import org.springframework.ai.image.ImageModel;
import org.springframework.ai.model.SimpleApiKey;
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<ChatModel> 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<ChatOptions> createChatOptions(Map<String, Object> options) {
OpenAiChatOptions.Builder builder = OpenAiChatOptions.builder();
builder.streamUsage(true);
for (Map.Entry<String, Object> 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<ImageModel> 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<TextToSpeechModel> createTextToSpeechModel(ModelConfig modelConfig) {
try {
OpenAiAudioApi audioApi = OpenAiAudioApi.builder()
.baseUrl(modelConfig.getUrl())
.apiKey(new SimpleApiKey(modelConfig.getApiKey()))
.restClientBuilder(restClientBuilder)
.responseErrorHandler(responseErrorHandler)
.build();
TextToSpeechModel textToSpeechModel = new OpenAiAudioSpeechModel(audioApi);
return Optional.of(textToSpeechModel);
} catch (Exception e) {
log.error("创建OpenAI文本转语音模型失败", e);
return Optional.empty();
}
}
@Override
public Optional<EmbeddingModel> createEmbeddingModel(ModelConfig modelConfig) {
try {
OpenAiApi openAiApi = createOpenAiApi(modelConfig.getApiKey(), modelConfig.getUrl());
EmbeddingModel embeddingModel = new OpenAiEmbeddingModel(openAiApi);
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))
.restClientBuilder(restClientBuilder)
.webClientBuilder(webClientBuilder).build();
}
}

View File

@ -0,0 +1,100 @@
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 prompt){
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<ChatOptions> options = client.createChatOptions(BeanUtil.beanToMap(scenarioConfig));
return chatModel.call(new Prompt(prompt.toString(), options.orElse(null)));
}
public Flux<ChatResponse> 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<ChatOptions> 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<AiApiKeyEntity> aiApiKeyList = aiApiKeyService.list(new LambdaQueryWrapper<>(AiApiKeyEntity.class).eq(AiApiKeyEntity::getPlatform, aiScenarioConfig.getPlatform()));
AiApiKeyEntity apiKey = null;
Optional<AiApiKeyEntity> 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<AiApiKeyEntity> apiKeyOptional = aiApiKeyList.stream().filter(aiApiKey -> Objects.nonNull(aiApiKey.getIsDefault()) && aiApiKey.getIsDefault() == 1).findAny();
if (apiKeyOptional.isPresent()) {
apiKey = apiKeyOptional.get();
}
}
return apiKey;
}
}

View File

@ -30,5 +30,11 @@
<artifactId>spring-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
</dependencies>
</project>

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

View File

@ -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");
return """
你是一个教学题目的专家请根据以下示例题目生成%d道类似的题目
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");
知识点%s
知识点总结%s
要求
1. 题目必须基于相同的教学知识点
2. 题目难度应与示例题目相当
3. 题型必须与示例题目一致
4. 答案必须明确且唯一
5. 提供详细的解题过程或解析
6. 返回严格的JSON数组格式每项包含以下字段
- question: 题干内容
- answer: 答案内容
- solution: 解析内容
%s
输出格式示例
[
{
"question": "题目内容",
"answer": "答案内容",
"solution": "解析内容",
%s
}
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();
]
""".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");
String lowerElementaryMathNote = isLowerElementaryMath ? "\n特别注意这是一、二年级的题目请尽量以图文并茂的形式展示题目内容和选项使题目更适合低年级学生理解。\n" : "";
prompt.append(" }\n");
prompt.append("]\n");
String middleElementaryEnglishNote = isMiddleElementaryEnglish ? "\n特别注意这是三、四年级的英语题目请尽量使用中文出题让题目更容易被学生理解。\n" : "";
// 特殊要求对于小学一二年级的数学题目强调以图片为主
if (isLowerElementaryMath) {
prompt.append("\n特别注意这是一、二年级的题目请尽量以图文并茂的形式展示题目内容和选项使题目更适合低年级学生理解。\n");
}
// 特殊要求对于三四年级的英语题目强调使用中文出题
if (isMiddleElementaryEnglish) {
prompt.append("\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");
String requirements = CollectionUtil.isNotEmpty(req.getKnowledgePointNames()) ?
String.format("1. 将原始知识点拆分为上面指定的%d个名称的知识点每个知识点需要包含以下信息\n", req.getCount()) :
String.format("1. 将原始知识点拆分为%d个更具体、更细粒度的知识点每个知识点需要包含以下信息\n", req.getCount());
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": "解析内容"
}
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");
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();
]
}
]
""".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<Integer> gradeIds) {
StringBuilder prompt = new StringBuilder();
return """
你是一位资深的教学专家请根据以下知识点判断其内容是否正确和超纲
prompt.append("你是一位资深的教学专家,请根据以下知识点判断其内容是否正确和超纲。\n\n");
知识点名称%s
知识点总结%s
对应的年级%s
对应的学科%s
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");
请判断以下内容
1. 该知识点内容是否正确
2. 该知识点是否超出指定年级学生的学习范围超纲
3. 如果超纲请说明超纲原因并给出适合的替换知识点名称和总结
4. 请提供该知识点的示例题目
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");
要求
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道题目
return prompt.toString();
输出格式示例
{
"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<TeachMethodsEntity> 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(是否默认10否)videoContent(讲课稿内容对应的视频内容)字段\n\n");
return """
你是一位经验丰富的教师请根据以下教学目标和教学重点针对每种教学方法分别生成一份详细且有针对性的讲课稿
// 添加输出示例
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("]");
教学目标: %s
教学重点: %s
%s%s
教学方法列表:
%s
return prompt.toString();
请根据以上教学目标重点和教学方法生成详细的讲课稿要求如下:
1. 内容详实语言生动适合课堂讲解
2. 结构清晰逻辑性强
3. 包含导入新授巩固练习小结等教学环节
4. 每种教学方法都需要生成对应的讲课稿
5. 字数不少于800字
6. 以JSON数组格式输出每个元素包含sort(排序)methodId(教学方法Id)和text(讲课稿内容)isDefault(是否默认10)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");
return """
你是一位资深的'%s'教学专家请根据以下知识点生成%d个教学目标考点
// 添加知识点信息
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");
知识点名称%s
知识点总结%s
对应的年级%s
对应的学科%s
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");
要求
1. 教学目标必须基于"%s"这个知识点
2. 教学目标应适合对应年级的学生理解和掌握
3. 每个教学目标应包含具体的学习成果描述
4. 提供对应的教学重点
5. 可以提供推荐的视频教学链接可选
6. 每个教学目标需要有一个排序编号
7. 输出的格式必须是json格式字段如下
- goal: 教学目标内容
- point: 教学重点
- sort: 排序编号
return prompt.toString();
输出格式示例
[
{
"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();
return """
请根据以下视频内容描述生成一个完整的Python脚本该脚本能够直接生成高质量的教学视频
prompt.append("请根据以下视频内容描述生成一个完整的Python脚本该脚本能够直接生成高质量的教学视频。\n\n");
## 视频内容描述:
%s
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");
## 核心要求:
1. **视频类型**: 生成的是纯视觉教学视频不需要包含任何字幕或音频
2. **视频质量**: 必须是高质量的视觉呈现包括清晰的图形动画和视觉效果
3. **教学重点**: 通过视觉元素图表动画代码演示视觉示例等清晰地传达教学内容
prompt.append("## Python代码要求:\n");
prompt.append("### 1. 代码结构:\n");
prompt.append("- 必须是完整的、可直接运行的独立脚本\n");
prompt.append("- 包含所有必要的导入语句和依赖\n");
prompt.append("- 包含主程序入口,可直接执行生成视频\n\n");
## Python代码要求:
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");
### 1. 代码结构:
- 必须是完整的可直接运行的独立脚本
- 包含所有必要的导入语句和依赖
- 包含主程序入口可直接执行生成视频
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");
### 2. 视觉生成要求:
- **使用专业的视觉库**: 如Matplotlib, Plotly, Seaborn, MoviePy, OpenCV, PIL等
- **高质量视觉效果**:
- 清晰美观的图表和可视化
- 流畅的动画和过渡效果
- 合适的颜色搭配和视觉层次
- 适当的文本标注作为图形的一部分而非字幕
- **视觉叙事**: 通过视觉序列清晰地展示概念演变或步骤流程
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");
### 3. 代码质量:
- **完整注释**:
- 文件头部说明视频主题主要视觉元素和依赖
- 函数/详细说明功能和参数
- 关键步骤解释视觉生成逻辑
- **错误处理**: 包含适当的异常处理和边界情况
- **模块化设计**: 将不同功能封装为函数或类
prompt.append("## 示例思考方向(根据具体内容调整):\n");
prompt.append("- 如果是编程教学:生成代码执行过程的可视化动画\n");
prompt.append("- 如果是数学教学:生成公式推导的逐步动画演示\n");
prompt.append("- 如果是数据科学:生成数据处理流程的可视化\n");
prompt.append("- 如果是算法教学:生成算法步骤的动态演示\n");
prompt.append("- 如果是概念讲解:生成概念演变的分步图示\n");
return prompt.toString();
### 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<Integer> gradeIds) {

View File

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

View File

@ -23,6 +23,8 @@ spring:
- optional:nacos:shared-database.yaml
- optional:nacos:shared-minio.yaml
- optional:nacos:shared-redis.yaml
- optional:nacos:shared-sa-token.yaml
- optional:nacos:shared-minio.yaml
cloud:
nacos:
discovery:

View File

@ -8,9 +8,9 @@ import com.seer.teach.teacher.module.entity.AiScenarioConfigEntity;
* AI场景配置表 服务类
* </p>
*
* @author System
* @since 2025-10-27
*/
public interface IAiScenarioConfigService extends IService<AiScenarioConfigEntity> {
AiScenarioConfigEntity getOneByScenarioCode(String scenarioCode);
}

View File

@ -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<AiScenarioConfigMapper, AiScenarioConfigEntity> implements IAiScenarioConfigService {
private final IAiScenarioService aiScenarioService;
@Override
public AiScenarioConfigEntity getOneByScenarioCode(String scenarioCode) {
LambdaQueryWrapper<AiScenarioEntity> scenarioWrapper = new LambdaQueryWrapper<>();
scenarioWrapper.eq(AiScenarioEntity::getScenarioCode, scenarioCode);
AiScenarioEntity aiScenario = aiScenarioService.getOne(scenarioWrapper);
AssertUtils.notNull(aiScenario, ResultCodeEnum.SCENARIO_NOT_FOUND);
LambdaQueryWrapper<AiScenarioConfigEntity> configWrapper = new LambdaQueryWrapper<>();
configWrapper.eq(AiScenarioConfigEntity::getScenarioId, aiScenario.getId());
configWrapper.eq(AiScenarioConfigEntity::getEnabled, CommonConstant.ENABLE);
configWrapper.orderByAsc(AiScenarioConfigEntity::getPriority);
return super.getOne(configWrapper);
}
}