package cn.iocoder.yudao.module.ai.framework.ai.config; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.module.ai.framework.ai.core.model.AiModelFactory; import cn.iocoder.yudao.module.ai.framework.ai.core.model.AiModelFactoryImpl; import io.micrometer.observation.ObservationRegistry; import lombok.extern.slf4j.Slf4j; 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.openai.OpenAiEmbeddingModel; import org.springframework.ai.openai.OpenAiEmbeddingOptions; import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator; import org.springframework.ai.tokenizer.TokenCountEstimator; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import java.time.Duration; /** * 超级管理员 AI 自动配置 * * 使用 OpenAI 兼容接口对接通义千问 DashScope * 向量库配置见 yudao.ai.vector-store 节点,不再依赖 Spring AI 的 Milvus 自动配置属性 */ @Configuration @EnableConfigurationProperties(YudaoAiProperties.class) @Slf4j public class AiAutoConfiguration { private static final String DASHSCOPE_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"; @Bean public AiModelFactory aiModelFactory() { return new AiModelFactoryImpl(); } @Bean @ConditionalOnMissingBean public ObservationRegistry observationRegistry() { return ObservationRegistry.NOOP; } @Bean @ConditionalOnMissingBean public BatchingStrategy batchingStrategy() { return new TokenCountBatchingStrategy(); } @Bean @ConditionalOnMissingBean public TokenCountEstimator tokenCountEstimator() { return new JTokkitTokenCountEstimator(); } // ========== 通义千问 Chat(通过 OpenAI 兼容接口)========== /** * 构建通义千问 Chat 模型 * * @param temperature 采样温度,为 null 时用 0.7(与历史行为一致) * @param maxTokens 单次回复的最大 token 数,为 null 时沿用模型默认值。 * 不传会让长文档的输出被静默截断,表现为「JSON 尾部缺失、解析失败」 * @param timeout 单次调用超时。不设超时的话,模型排队时请求会一直挂到 TCP 层超时 */ public static OpenAiChatModel buildTongYiChatModel(String apiKey, String model, Double temperature, Integer maxTokens, Duration timeout) { return OpenAiChatModel.builder() .options(OpenAiChatOptions.builder() .baseUrl(DASHSCOPE_BASE_URL) .apiKey(apiKey) .model(StrUtil.blankToDefault(model, "qwen-plus")) .temperature(temperature != null ? temperature : 0.7) .maxTokens(maxTokens) .timeout(timeout) .build()) .toolCallingManager(ToolCallingManager.builder().build()) .build(); } // ========== 通义千问 Embedding(通过 OpenAI 兼容接口)========== public static OpenAiEmbeddingModel buildTongYiEmbeddingModel(String apiKey, String model) { return OpenAiEmbeddingModel.builder() .options(OpenAiEmbeddingOptions.builder() .baseUrl(DASHSCOPE_BASE_URL) .apiKey(apiKey) .model(StrUtil.blankToDefault(model, "text-embedding-v3")) .build()) .metadataMode(MetadataMode.EMBED) .build(); } }