package cn.iocoder.yudao.module.ai.framework.ai.config; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.module.ai.framework.ai.core.model.AiModelFactory; import cn.iocoder.yudao.module.ai.framework.ai.core.model.AiModelFactoryImpl; import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.doubao.DouBaoChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.grok.GrokChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi; import cn.iocoder.yudao.module.ai.framework.ai.core.model.minimax.MiniMaxChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.moonshot.MoonshotChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowApiConstants; import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.stepfun.StepFunChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi; import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.yiyan.YiYanChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.zhipu.ZhiPuChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchClient; import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.bocha.AiBoChaWebSearchClient; import cn.iocoder.yudao.module.ai.tool.method.PersonService; import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; import com.alibaba.cloud.ai.dashscope.api.DashScopeImageApi; import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; import com.alibaba.cloud.ai.dashscope.embedding.text.DashScopeEmbeddingModel; import com.alibaba.cloud.ai.dashscope.embedding.text.DashScopeEmbeddingOptions; import com.alibaba.cloud.ai.dashscope.image.DashScopeImageModel; import com.alibaba.cloud.ai.dashscope.image.DashScopeImageOptions; import io.micrometer.observation.ObservationRegistry; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.deepseek.DeepSeekChatModel; import org.springframework.ai.deepseek.DeepSeekChatOptions; import org.springframework.ai.deepseek.api.DeepSeekApi; import org.springframework.ai.document.MetadataMode; import org.springframework.ai.embedding.BatchingStrategy; import org.springframework.ai.embedding.TokenCountBatchingStrategy; import org.springframework.ai.model.tool.ToolCallingManager; import org.springframework.ai.openai.OpenAiChatModel; import org.springframework.ai.openai.OpenAiChatOptions; import org.springframework.ai.retry.RetryUtils; import org.springframework.ai.support.ToolCallbacks; import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator; import org.springframework.ai.tokenizer.TokenCountEstimator; import org.springframework.ai.tool.ToolCallback; import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusServiceClientProperties; import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreProperties; import org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreProperties; import org.springframework.ai.vectorstore.redis.autoconfigure.RedisVectorStoreProperties; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.List; /** * 芋道 AI 自动配置 * * @author fansili */ @Configuration @EnableConfigurationProperties({ YudaoAiProperties.class, QdrantVectorStoreProperties.class, // 解析 Qdrant 配置 RedisVectorStoreProperties.class, // 解析 Redis 配置 MilvusVectorStoreProperties.class, MilvusServiceClientProperties.class // 解析 Milvus 配置 }) @Slf4j public class AiAutoConfiguration { @Bean public AiModelFactory aiModelFactory() { return new AiModelFactoryImpl(); } @Bean @ConditionalOnMissingBean public ObservationRegistry observationRegistry() { // 特殊:兜底有 ObservationRegistry Bean,避免相关的 ChatModel 创建报错。相关 issue:https://t.zsxq.com/CuPu4 return ObservationRegistry.NOOP; } // ========== 各种 AI Client 创建 ========== @Bean @ConditionalOnMissingBean @ConditionalOnProperty(value = "spring.ai.dashscope.api-key") public DashScopeChatModel dashScopeChatModel(@Value("${spring.ai.dashscope.api-key}") String apiKey, ToolCallingManager toolCallingManager, ObservationRegistry observationRegistry) { return buildTongYiChatModel(apiKey, toolCallingManager, observationRegistry); } @Bean @ConditionalOnMissingBean @ConditionalOnProperty(value = "spring.ai.dashscope.api-key") public DashScopeImageModel dashScopeImageModel(@Value("${spring.ai.dashscope.api-key}") String apiKey, ObservationRegistry observationRegistry) { return buildTongYiImagesModel(apiKey, observationRegistry); } @Bean @ConditionalOnMissingBean @ConditionalOnProperty(value = "spring.ai.dashscope.api-key") public DashScopeEmbeddingModel dashScopeEmbeddingModel(@Value("${spring.ai.dashscope.api-key}") String apiKey, ObservationRegistry observationRegistry) { return buildTongYiEmbeddingModel(apiKey, null, observationRegistry); } public static DashScopeChatModel buildTongYiChatModel(String apiKey) { return buildTongYiChatModel(apiKey, getToolCallingManager(), getObservationRegistry()); } private static DashScopeChatModel buildTongYiChatModel(String apiKey, ToolCallingManager toolCallingManager, ObservationRegistry observationRegistry) { DashScopeApi dashScopeApi = DashScopeApi.builder().apiKey(apiKey).build(); DashScopeChatOptions options = DashScopeChatOptions.builder() .model(DashScopeApi.DEFAULT_CHAT_MODEL) .temperature(0.7) .build(); return new DashScopeChatModel(dashScopeApi, options, toolCallingManager, RetryUtils.DEFAULT_RETRY_TEMPLATE, observationRegistry); } public static DashScopeImageModel buildTongYiImagesModel(String apiKey) { return buildTongYiImagesModel(apiKey, getObservationRegistry()); } private static DashScopeImageModel buildTongYiImagesModel(String apiKey, ObservationRegistry observationRegistry) { DashScopeImageApi dashScopeImageApi = DashScopeImageApi.builder().apiKey(apiKey).build(); DashScopeImageOptions options = DashScopeImageOptions.builder() .model(DashScopeImageApi.DEFAULT_IMAGE_MODEL) .build(); return new DashScopeImageModel(dashScopeImageApi, options, RetryUtils.DEFAULT_RETRY_TEMPLATE, observationRegistry); } public static DashScopeEmbeddingModel buildTongYiEmbeddingModel(String apiKey, String model) { return buildTongYiEmbeddingModel(apiKey, model, getObservationRegistry()); } private static DashScopeEmbeddingModel buildTongYiEmbeddingModel(String apiKey, String model, ObservationRegistry observationRegistry) { DashScopeApi dashScopeApi = DashScopeApi.builder().apiKey(apiKey).build(); DashScopeEmbeddingOptions options = DashScopeEmbeddingOptions.builder() .model(StrUtil.blankToDefault(model, DashScopeApi.DEFAULT_EMBEDDING_MODEL)) .build(); return new DashScopeEmbeddingModel(dashScopeApi, MetadataMode.EMBED, options, RetryUtils.DEFAULT_RETRY_TEMPLATE, observationRegistry); } @Bean @ConditionalOnProperty(value = "yudao.ai.doubao.enable", havingValue = "true") public DouBaoChatModel douBaoChatClient(YudaoAiProperties yudaoAiProperties) { YudaoAiProperties.DouBao properties = yudaoAiProperties.getDoubao(); return buildDouBaoChatClient(properties); } public DouBaoChatModel buildDouBaoChatClient(YudaoAiProperties.DouBao properties) { if (StrUtil.isEmpty(properties.getModel())) { properties.setModel(DouBaoChatModel.MODEL_DEFAULT); } DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder() .deepSeekApi(DeepSeekApi.builder() .baseUrl(DouBaoChatModel.BASE_URL) .completionsPath(DouBaoChatModel.COMPLETE_PATH) .apiKey(properties.getApiKey()) .build()) .options(DeepSeekChatOptions.builder() .model(properties.getModel()) .temperature(properties.getTemperature()) .maxTokens(properties.getMaxTokens()) .topP(properties.getTopP()) .build()) .build(); return new DouBaoChatModel(openAiChatModel); } @Bean @ConditionalOnProperty(value = "yudao.ai.siliconflow.enable", havingValue = "true") public SiliconFlowChatModel siliconFlowChatClient(YudaoAiProperties yudaoAiProperties) { YudaoAiProperties.SiliconFlow properties = yudaoAiProperties.getSiliconflow(); return buildSiliconFlowChatClient(properties); } public SiliconFlowChatModel buildSiliconFlowChatClient(YudaoAiProperties.SiliconFlow properties) { if (StrUtil.isEmpty(properties.getModel())) { properties.setModel(SiliconFlowApiConstants.MODEL_DEFAULT); } DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder() .deepSeekApi(DeepSeekApi.builder() .baseUrl(SiliconFlowApiConstants.DEFAULT_BASE_URL) .apiKey(properties.getApiKey()) .build()) .options(DeepSeekChatOptions.builder() .model(properties.getModel()) .temperature(properties.getTemperature()) .maxTokens(properties.getMaxTokens()) .topP(properties.getTopP()) .build()) .build(); return new SiliconFlowChatModel(openAiChatModel); } @Bean @ConditionalOnProperty(value = "yudao.ai.hunyuan.enable", havingValue = "true") public HunYuanChatModel hunYuanChatClient(YudaoAiProperties yudaoAiProperties) { YudaoAiProperties.HunYuan properties = yudaoAiProperties.getHunyuan(); return buildHunYuanChatClient(properties); } public HunYuanChatModel buildHunYuanChatClient(YudaoAiProperties.HunYuan properties) { if (StrUtil.isEmpty(properties.getModel())) { properties.setModel(HunYuanChatModel.MODEL_DEFAULT); } if (StrUtil.isEmpty(properties.getBaseUrl())) { properties.setBaseUrl(HunYuanChatModel.BASE_URL); } // 创建 DeepSeekChatModel、HunYuanChatModel 对象 DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder() .deepSeekApi(DeepSeekApi.builder() .baseUrl(properties.getBaseUrl()) .completionsPath(HunYuanChatModel.COMPLETE_PATH) .apiKey(properties.getApiKey()) .build()) .options(DeepSeekChatOptions.builder() .model(properties.getModel()) .temperature(properties.getTemperature()) .maxTokens(properties.getMaxTokens()) .topP(properties.getTopP()) .build()) .build(); return new HunYuanChatModel(openAiChatModel); } @Bean @ConditionalOnProperty(value = "yudao.ai.xinghuo.enable", havingValue = "true") public XingHuoChatModel xingHuoChatClient(YudaoAiProperties yudaoAiProperties) { YudaoAiProperties.XingHuo properties = yudaoAiProperties.getXinghuo(); return buildXingHuoChatClient(properties); } public XingHuoChatModel buildXingHuoChatClient(YudaoAiProperties.XingHuo properties) { if (StrUtil.isEmpty(properties.getModel())) { properties.setModel(XingHuoChatModel.MODEL_DEFAULT); } return XingHuoChatModel.builder() .apiKey(properties.getApiKey()) .options(DeepSeekChatOptions.builder() .model(properties.getModel()) .temperature(properties.getTemperature()) .maxTokens(properties.getMaxTokens()) .topP(properties.getTopP()) .build()) .build(); } @Bean @ConditionalOnProperty(value = "yudao.ai.baichuan.enable", havingValue = "true") public BaiChuanChatModel baiChuanChatClient(YudaoAiProperties yudaoAiProperties) { YudaoAiProperties.BaiChuan properties = yudaoAiProperties.getBaichuan(); return buildBaiChuanChatClient(properties); } public BaiChuanChatModel buildBaiChuanChatClient(YudaoAiProperties.BaiChuan properties) { if (StrUtil.isEmpty(properties.getModel())) { properties.setModel(BaiChuanChatModel.MODEL_DEFAULT); } DeepSeekChatModel deepSeekChatModel = DeepSeekChatModel.builder() .deepSeekApi(DeepSeekApi.builder() .baseUrl(BaiChuanChatModel.BASE_URL) .apiKey(properties.getApiKey()) .completionsPath(BaiChuanChatModel.COMPLETE_PATH) .build()) .options(DeepSeekChatOptions.builder() .model(properties.getModel()) .temperature(properties.getTemperature()) .maxTokens(properties.getMaxTokens()) .topP(properties.getTopP()) .build()) .build(); return new BaiChuanChatModel(deepSeekChatModel); } @Bean @ConditionalOnProperty(value = "yudao.ai.yiyan.enable", havingValue = "true") public YiYanChatModel yiYanChatClient(YudaoAiProperties yudaoAiProperties) { YudaoAiProperties.YiYan properties = yudaoAiProperties.getYiyan(); return buildYiYanChatClient(properties); } public YiYanChatModel buildYiYanChatClient(YudaoAiProperties.YiYan properties) { if (StrUtil.isEmpty(properties.getModel())) { properties.setModel(YiYanChatModel.MODEL_DEFAULT); } return new YiYanChatModel(buildDeepSeekCompatibleChatModel( StrUtil.blankToDefault(properties.getBaseUrl(), YiYanChatModel.BASE_URL), null, properties.getApiKey(), properties.getModel(), properties.getTemperature(), properties.getMaxTokens(), properties.getTopP())); } @Bean @ConditionalOnProperty(value = "yudao.ai.zhipu.enable", havingValue = "true") public ZhiPuChatModel zhiPuChatClient(YudaoAiProperties yudaoAiProperties) { YudaoAiProperties.ZhiPu properties = yudaoAiProperties.getZhipu(); return buildZhiPuChatClient(properties); } public ZhiPuChatModel buildZhiPuChatClient(YudaoAiProperties.ZhiPu properties) { if (StrUtil.isEmpty(properties.getModel())) { properties.setModel(ZhiPuChatModel.MODEL_DEFAULT); } DeepSeekChatModel deepSeekChatModel = DeepSeekChatModel.builder() .deepSeekApi(DeepSeekApi.builder() .baseUrl(StrUtil.blankToDefault(properties.getBaseUrl(), ZhiPuChatModel.BASE_URL)) .apiKey(properties.getApiKey()) .build()) .options(DeepSeekChatOptions.builder() .model(properties.getModel()) .temperature(properties.getTemperature()) .maxTokens(properties.getMaxTokens()) .topP(properties.getTopP()) .build()) .build(); return new ZhiPuChatModel(deepSeekChatModel); } @Bean @ConditionalOnProperty(value = "yudao.ai.minimax.enable", havingValue = "true") public MiniMaxChatModel miniMaxChatClient(YudaoAiProperties yudaoAiProperties) { YudaoAiProperties.MiniMax properties = yudaoAiProperties.getMinimax(); return buildMiniMaxChatClient(properties); } public MiniMaxChatModel buildMiniMaxChatClient(YudaoAiProperties.MiniMax properties) { if (StrUtil.isEmpty(properties.getModel())) { properties.setModel(MiniMaxChatModel.MODEL_DEFAULT); } return new MiniMaxChatModel(buildDeepSeekCompatibleChatModel( StrUtil.blankToDefault(properties.getBaseUrl(), MiniMaxChatModel.BASE_URL), null, properties.getApiKey(), properties.getModel(), properties.getTemperature(), properties.getMaxTokens(), properties.getTopP())); } @Bean @ConditionalOnProperty(value = "yudao.ai.moonshot.enable", havingValue = "true") public MoonshotChatModel moonshotChatClient(YudaoAiProperties yudaoAiProperties) { YudaoAiProperties.Moonshot properties = yudaoAiProperties.getMoonshot(); return buildMoonshotChatClient(properties); } public MoonshotChatModel buildMoonshotChatClient(YudaoAiProperties.Moonshot properties) { if (StrUtil.isEmpty(properties.getModel())) { properties.setModel(MoonshotChatModel.MODEL_DEFAULT); } return new MoonshotChatModel(buildDeepSeekCompatibleChatModel( StrUtil.blankToDefault(properties.getBaseUrl(), MoonshotChatModel.BASE_URL), MoonshotChatModel.COMPLETE_PATH, properties.getApiKey(), properties.getModel(), properties.getTemperature(), properties.getMaxTokens(), properties.getTopP())); } @Bean @ConditionalOnProperty(value = "yudao.ai.stepfun.enable", havingValue = "true") public StepFunChatModel stepFunChatClient(YudaoAiProperties yudaoAiProperties) { YudaoAiProperties.StepFun properties = yudaoAiProperties.getStepfun(); return buildStepFunChatClient(properties); } public StepFunChatModel buildStepFunChatClient(YudaoAiProperties.StepFun properties) { if (StrUtil.isEmpty(properties.getModel())) { properties.setModel(StepFunChatModel.MODEL_DEFAULT); } return new StepFunChatModel(buildDeepSeekCompatibleChatModel( StrUtil.blankToDefault(properties.getBaseUrl(), StepFunChatModel.BASE_URL), StepFunChatModel.COMPLETE_PATH, properties.getApiKey(), properties.getModel(), properties.getTemperature(), properties.getMaxTokens(), properties.getTopP())); } private static DeepSeekChatModel buildDeepSeekCompatibleChatModel(String baseUrl, String completionsPath, String apiKey, String model, Double temperature, Integer maxTokens, Double topP) { DeepSeekApi.Builder apiBuilder = DeepSeekApi.builder() .baseUrl(baseUrl) .apiKey(apiKey); if (StrUtil.isNotEmpty(completionsPath)) { apiBuilder.completionsPath(completionsPath); } return DeepSeekChatModel.builder() .deepSeekApi(apiBuilder.build()) .options(DeepSeekChatOptions.builder() .model(model) .temperature(temperature) .maxTokens(maxTokens) .topP(topP) .build()) .build(); } @Bean @ConditionalOnProperty(value = "yudao.ai.midjourney.enable", havingValue = "true") public MidjourneyApi midjourneyApi(YudaoAiProperties yudaoAiProperties) { YudaoAiProperties.Midjourney config = yudaoAiProperties.getMidjourney(); return new MidjourneyApi(config.getBaseUrl(), config.getApiKey(), config.getNotifyUrl()); } @Bean @ConditionalOnProperty(value = "yudao.ai.suno.enable", havingValue = "true") public SunoApi sunoApi(YudaoAiProperties yudaoAiProperties) { return new SunoApi(yudaoAiProperties.getSuno().getBaseUrl()); } @Bean @ConditionalOnProperty(value = "yudao.ai.grok.enable", havingValue = "true") public GrokChatModel grokChatClient(YudaoAiProperties yudaoAiProperties) { YudaoAiProperties.Grok properties = yudaoAiProperties.getGrok(); return buildGrokChatClient(properties); } public GrokChatModel buildGrokChatClient(YudaoAiProperties.Grok properties) { if (StrUtil.isEmpty(properties.getModel())) { properties.setModel(GrokChatModel.MODEL_DEFAULT); } OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() .options(OpenAiChatOptions.builder() .baseUrl(StrUtil.blankToDefault(properties.getBaseUrl(), GrokChatModel.BASE_URL)) .apiKey(properties.getApiKey()) .model(properties.getModel()) .temperature(properties.getTemperature()) .maxTokens(properties.getMaxTokens()) .topP(properties.getTopP()) .build()) .build(); return new GrokChatModel(openAiChatModel); } // ========== RAG 相关 ========== @Bean public TokenCountEstimator tokenCountEstimator() { return new JTokkitTokenCountEstimator(); } @Bean public BatchingStrategy batchingStrategy() { return new TokenCountBatchingStrategy(); } private static ToolCallingManager getToolCallingManager() { return SpringUtil.getBean(ToolCallingManager.class); } private static ObservationRegistry getObservationRegistry() { return SpringUtil.getBean(ObservationRegistry.class); } // ========== Web Search 相关 ========== @Bean @ConditionalOnProperty(value = "yudao.ai.web-search.enable", havingValue = "true") public AiWebSearchClient webSearchClient(YudaoAiProperties yudaoAiProperties) { return new AiBoChaWebSearchClient(yudaoAiProperties.getWebSearch().getApiKey()); } // ========== MCP 相关 ========== /** * 参考自 MCP Server Boot Starter */ @Bean public List toolCallbacks(PersonService personService) { return List.of(ToolCallbacks.from(personService)); } }