package cn.iocoder.yudao.module.ai.util; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.util.collection.SetUtils; import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum; import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; import cn.hutool.extra.spring.SpringUtil; import org.springframework.ai.anthropic.AnthropicChatOptions; import org.springframework.ai.chat.messages.*; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.deepseek.DeepSeekAssistantMessage; import org.springframework.ai.deepseek.DeepSeekChatOptions; import org.springframework.ai.google.genai.GoogleGenAiChatOptions; import org.springframework.ai.ollama.api.OllamaChatOptions; import org.springframework.ai.openai.OpenAiChatOptions; import org.springframework.ai.tool.ToolCallback; import org.springframework.core.env.Environment; import java.util.*; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.API_CONFIG_PLACEHOLDER_NOT_RESOLVED; /** * Spring AI 工具类 * * @author 芋道源码 */ public class AiUtils { public static final String TOOL_CONTEXT_LOGIN_USER = "LOGIN_USER"; public static final String TOOL_CONTEXT_TENANT_ID = "TENANT_ID"; /** * 解析 DB 等动态配置里的 Spring 占位符,例如 ${OPENAI_API_KEY} * * @param value 待解析的配置值 * @return 解析后的配置值 */ public static String resolveSpringPlaceholders(String value) { if (StrUtil.isBlank(value) || !StrUtil.contains(value, "${")) { return value; } try { return SpringUtil.getBean(Environment.class).resolveRequiredPlaceholders(value); } catch (IllegalArgumentException ex) { throw exception(API_CONFIG_PLACEHOLDER_NOT_RESOLVED, value); } } /** * 校验 API Key,避免集成测试使用默认占位值发起调用。 * * @param apiKey API Key */ public static void validateApiKey(String apiKey) { if (StrUtil.isBlank(apiKey) || "sk-xxxx".equals(apiKey)) { throw new IllegalStateException("apiKey 不能为空"); } } /** * 通义千问支持多模态的模型 * * @see 模型广场 * @see 必须开启 withMultiModel 参数 */ public static final Set TONG_YI_MULTI_MODELS = SetUtils.asSet( // qwen3.5 / 3.6 / 3.7 系列(统一多模态主干) "qwen3.7-max", "qwen3.7-plus", "qwen3.7-flash", "qwen3.6-plus", "qwen3.6-flash", "qwen3.5-plus", "qwen3.5-flash", // qwen-vl 视觉理解 "qwen3-vl-plus", "qwen3-vl-flash", "qwen-vl-max", "qwen-vl-plus", "qwen2.5-vl-72b-instruct", "qwen2.5-vl-32b-instruct", "qwen2.5-vl-7b-instruct", "qwen2.5-vl-3b-instruct", // qvq 视觉推理 "qvq-max", "qvq-plus", // qwen-omni 全模态 "qwen3.5-omni-plus", "qwen3.5-omni-flash", "qwen3-omni-flash", "qwen-omni-turbo" ); public static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens) { return buildChatOptions(platform, model, temperature, maxTokens, null, null); } public static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens, List toolCallbacks, Map toolContext) { toolCallbacks = ObjUtil.defaultIfNull(toolCallbacks, Collections.emptyList()); toolContext = ObjUtil.defaultIfNull(toolContext, Collections.emptyMap()); // noinspection EnhancedSwitchMigration switch (platform) { case TONG_YI: return DashScopeChatOptions.builder().model(model).temperature(temperature).maxToken(maxTokens) .enableThinking(true) // TODO 芋艿:默认都开启 thinking 模式,后续可以让用户配置 .multiModel(TONG_YI_MULTI_MODELS.contains(model)) // 是否多模态模型 .toolCallbacks(toolCallbacks).toolContext(toolContext).build(); case DEEP_SEEK: case DOU_BAO: // 复用 DeepSeek 客户端 case HUN_YUAN: // 复用 DeepSeek 客户端 case SILICON_FLOW: // 复用 DeepSeek 客户端 case YI_YAN: // 复用 DeepSeek 客户端 case ZHI_PU: // 复用 DeepSeek 客户端 case XING_HUO: // 复用 DeepSeek 客户端 case MINI_MAX: // 复用 DeepSeek 客户端 case MOONSHOT: // 复用 DeepSeek 客户端 case BAI_CHUAN: // 复用 DeepSeek 客户端 case STEP_FUN: // 复用 DeepSeek 客户端 return DeepSeekChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) .toolCallbacks(toolCallbacks).toolContext(toolContext).build(); case OPENAI: case GROK: // 复用 OpenAI 客户端 return OpenAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) .toolCallbacks(toolCallbacks).toolContext(toolContext).build(); case GEMINI: return GoogleGenAiChatOptions.builder().model(model).temperature(temperature).maxOutputTokens(maxTokens) .toolCallbacks(toolCallbacks).toolContext(toolContext).build(); case AZURE_OPENAI: return OpenAiChatOptions.builder().model(model).deploymentName(model).azure(true) .temperature(temperature).maxTokens(maxTokens) .toolCallbacks(toolCallbacks).toolContext(toolContext).build(); case ANTHROPIC: return AnthropicChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) .toolCallbacks(toolCallbacks).toolContext(toolContext).build(); case OLLAMA: return OllamaChatOptions.builder().model(model).temperature(temperature).numPredict(maxTokens) .toolCallbacks(toolCallbacks).toolContext(toolContext).build(); default: throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); } } public static Message buildMessage(String type, String content) { if (MessageType.USER.getValue().equals(type)) { return new UserMessage(content); } if (MessageType.ASSISTANT.getValue().equals(type)) { return new AssistantMessage(content); } if (MessageType.SYSTEM.getValue().equals(type)) { return new SystemMessage(content); } if (MessageType.TOOL.getValue().equals(type)) { throw new UnsupportedOperationException("暂不支持 tool 消息:" + content); } throw new IllegalArgumentException(StrUtil.format("未知消息类型({})", type)); } public static Map buildCommonToolContext() { Map context = new HashMap<>(); context.put(TOOL_CONTEXT_LOGIN_USER, SecurityFrameworkUtils.getLoginUser()); context.put(TOOL_CONTEXT_TENANT_ID, TenantContextHolder.getTenantId()); return context; } @SuppressWarnings("ConstantValue") public static String getChatResponseContent(ChatResponse response) { if (response == null || response.getResult() == null || response.getResult().getOutput() == null) { return null; } return response.getResult().getOutput().getText(); } @SuppressWarnings("ConstantValue") public static String getChatResponseReasoningContent(ChatResponse response) { if (response == null || response.getResult() == null || response.getResult().getOutput() == null) { return null; } AssistantMessage output = response.getResult().getOutput(); // DeepSeek 通过专属 AssistantMessage 暴露 reasoningContent if (output instanceof DeepSeekAssistantMessage) { return ((DeepSeekAssistantMessage) output).getReasoningContent(); } // 通义千问等通过 metadata 透传 reasoningContent return MapUtil.getStr(output.getMetadata(), "reasoningContent"); } }