Commit e0f08a0f02b5bb53b7f3e6201469eb3a5767bf40

Authored by YunaiV
1 parent 6c094aaf

【代码优化】AI:将 ChatClient 替换成 ChatModel,和 Spring AI 对齐

yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java
... ... @@ -70,7 +70,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
70 70 List<AiChatMessageDO> historyMessages = chatMessageMapper.selectListByConversationId(conversation.getId());
71 71 // 1.2 校验模型
72 72 AiChatModelDO model = chatModalService.validateChatModel(conversation.getModelId());
73   - ChatModel chatClient = apiKeyService.getChatClient(model.getKeyId());
  73 + ChatModel chatModel = apiKeyService.getChatModel(model.getKeyId());
74 74  
75 75 // 2. 插入 user 发送消息
76 76 AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model,
... ... @@ -82,7 +82,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
82 82  
83 83 // 3.2 创建 chat 需要的 Prompt
84 84 Prompt prompt = buildPrompt(conversation, historyMessages, model, sendReqVO);
85   - ChatResponse chatResponse = chatClient.call(prompt);
  85 + ChatResponse chatResponse = chatModel.call(prompt);
86 86  
87 87 // 3.3 段式返回
88 88 String newContent = chatResponse.getResult().getOutput().getContent();
... ... @@ -101,7 +101,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
101 101 List<AiChatMessageDO> historyMessages = chatMessageMapper.selectListByConversationId(conversation.getId());
102 102 // 1.2 校验模型
103 103 AiChatModelDO model = chatModalService.validateChatModel(conversation.getModelId());
104   - StreamingChatModel chatClient = apiKeyService.getChatClient(model.getKeyId());
  104 + StreamingChatModel chatModel = apiKeyService.getChatModel(model.getKeyId());
105 105  
106 106 // 2. 插入 user 发送消息
107 107 AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model,
... ... @@ -113,7 +113,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
113 113  
114 114 // 3.2 创建 chat 需要的 Prompt
115 115 Prompt prompt = buildPrompt(conversation, historyMessages, model, sendReqVO);
116   - Flux<ChatResponse> streamResponse = chatClient.stream(prompt);
  116 + Flux<ChatResponse> streamResponse = chatModel.stream(prompt);
117 117  
118 118 // 3.3 流式返回
119 119 // TODO 注意:Schedulers.immediate() 目的是,避免默认 Schedulers.parallel() 并发消费 chunk 导致 SSE 响应前端会乱序问题
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java
... ... @@ -98,8 +98,8 @@ public class AiImageServiceImpl implements AiImageService {
98 98 // 1.1 构建请求
99 99 ImageOptions request = buildImageOptions(req);
100 100 // 1.2 执行请求
101   - ImageModel imageClient = apiKeyService.getImageClient(AiPlatformEnum.validatePlatform(req.getPlatform()));
102   - ImageResponse response = imageClient.call(new ImagePrompt(req.getPrompt(), request));
  101 + ImageModel imageModel = apiKeyService.getImageModel(AiPlatformEnum.validatePlatform(req.getPlatform()));
  102 + ImageResponse response = imageModel.call(new ImagePrompt(req.getPrompt(), request));
103 103  
104 104 // 2. 上传到文件服务
105 105 byte[] fileContent = Base64.decode(response.getResult().getOutput().getB64Json());
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java
... ... @@ -81,17 +81,17 @@ public interface AiApiKeyService {
81 81 * @param id 编号
82 82 * @return ChatModel 对象
83 83 */
84   - ChatModel getChatClient(Long id);
  84 + ChatModel getChatModel(Long id);
85 85  
86 86 /**
87   - * 获得 ImageClient 对象
  87 + * 获得 ImageModel 对象
88 88 *
89 89 * TODO 可优化点:目前默认获取 platform 对应的第一个开启的配置用于绘画;后续可以支持配置选择
90 90 *
91 91 * @param platform 平台
92   - * @return ImageClient 对象
  92 + * @return ImageModel 对象
93 93 */
94   - ImageModel getImageClient(AiPlatformEnum platform);
  94 + ImageModel getImageModel(AiPlatformEnum platform);
95 95  
96 96 /**
97 97 * 获得 MidjourneyApi 对象
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java
1 1 package cn.iocoder.yudao.module.ai.service.model;
2 2  
3 3 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
4   -import cn.iocoder.yudao.framework.ai.core.factory.AiClientFactory;
  4 +import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactory;
5 5 import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
6 6 import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
7 7 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
... ... @@ -35,7 +35,7 @@ public class AiApiKeyServiceImpl implements AiApiKeyService {
35 35 private AiApiKeyMapper apiKeyMapper;
36 36  
37 37 @Resource
38   - private AiClientFactory clientFactory;
  38 + private AiModelFactory modelFactory;
39 39  
40 40 @Override
41 41 public Long createApiKey(AiApiKeySaveReqVO createReqVO) {
... ... @@ -98,19 +98,19 @@ public class AiApiKeyServiceImpl implements AiApiKeyService {
98 98 // ========== 与 spring-ai 集成 ==========
99 99  
100 100 @Override
101   - public ChatModel getChatClient(Long id) {
  101 + public ChatModel getChatModel(Long id) {
102 102 AiApiKeyDO apiKey = validateApiKey(id);
103 103 AiPlatformEnum platform = AiPlatformEnum.validatePlatform(apiKey.getPlatform());
104   - return clientFactory.getOrCreateChatClient(platform, apiKey.getApiKey(), apiKey.getUrl());
  104 + return modelFactory.getOrCreateChatClient(platform, apiKey.getApiKey(), apiKey.getUrl());
105 105 }
106 106  
107 107 @Override
108   - public ImageModel getImageClient(AiPlatformEnum platform) {
  108 + public ImageModel getImageModel(AiPlatformEnum platform) {
109 109 AiApiKeyDO apiKey = apiKeyMapper.selectFirstByPlatformAndStatus(platform.getName(), CommonStatusEnum.ENABLE.getStatus());
110 110 if (apiKey == null) {
111 111 throw exception(API_KEY_IMAGE_NODE_FOUND, platform.getName());
112 112 }
113   - return clientFactory.getOrCreateImageClient(platform, apiKey.getApiKey(), apiKey.getUrl());
  113 + return modelFactory.getOrCreateImageModel(platform, apiKey.getApiKey(), apiKey.getUrl());
114 114 }
115 115  
116 116 @Override
... ... @@ -120,7 +120,7 @@ public class AiApiKeyServiceImpl implements AiApiKeyService {
120 120 if (apiKey == null) {
121 121 throw exception(API_KEY_MIDJOURNEY_NOT_FOUND);
122 122 }
123   - return clientFactory.getOrCreateMidjourneyApi(apiKey.getApiKey(), apiKey.getUrl());
  123 + return modelFactory.getOrCreateMidjourneyApi(apiKey.getApiKey(), apiKey.getUrl());
124 124 }
125 125  
126 126 @Override
... ... @@ -130,7 +130,7 @@ public class AiApiKeyServiceImpl implements AiApiKeyService {
130 130 if (apiKey == null) {
131 131 throw exception(API_KEY_SUNO_NOT_FOUND);
132 132 }
133   - return clientFactory.getOrCreateSunoApi(apiKey.getApiKey(), apiKey.getUrl());
  133 + return modelFactory.getOrCreateSunoApi(apiKey.getApiKey(), apiKey.getUrl());
134 134 }
135 135  
136 136 }
137 137 \ No newline at end of file
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java
... ... @@ -54,7 +54,7 @@ public class AiWriteServiceImpl implements AiWriteService {
54 54 public Flux<CommonResult<String>> generateWriteContent(AiWriteGenerateReqVO generateReqVO, Long userId) {
55 55 // 1.1 校验模型 TODO 芋艿 是不是取默认的模型也ok?;那可以,有限拿 chatRole 的角色;如果没有,则获取默认的;
56 56 AiChatModelDO model = chatModalService.getRequiredDefaultChatModel();
57   - StreamingChatModel chatClient = apiKeyService.getChatClient(model.getKeyId());
  57 + StreamingChatModel chatModel = apiKeyService.getChatModel(model.getKeyId());
58 58 AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform());
59 59  
60 60 // 1.2 插入写作信息
... ... @@ -65,7 +65,7 @@ public class AiWriteServiceImpl implements AiWriteService {
65 65 // 2.1 构建提示词
66 66 ChatOptions chatOptions = AiUtils.buildChatOptions(platform, model.getModel(), model.getTemperature(), model.getMaxTokens());
67 67 Prompt prompt = new Prompt(buildWritingPrompt(generateReqVO), chatOptions);
68   - Flux<ChatResponse> streamResponse = chatClient.stream(prompt);
  68 + Flux<ChatResponse> streamResponse = chatModel.stream(prompt);
69 69  
70 70 // 2.2 流式返回
71 71 StringBuffer contentBuffer = new StringBuffer();
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java
1 1 package cn.iocoder.yudao.framework.ai.config;
2 2  
3   -import cn.iocoder.yudao.framework.ai.core.factory.AiClientFactory;
4   -import cn.iocoder.yudao.framework.ai.core.factory.AiClientFactoryImpl;
  3 +import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactory;
  4 +import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactoryImpl;
5 5 import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatClient;
6 6 import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatOptions;
7 7 import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
... ... @@ -28,8 +28,8 @@ import org.springframework.context.annotation.Import;
28 28 public class YudaoAiAutoConfiguration {
29 29  
30 30 @Bean
31   - public AiClientFactory aiClientFactory() {
32   - return new AiClientFactoryImpl();
  31 + public AiModelFactory aiModelFactory() {
  32 + return new AiModelFactoryImpl();
33 33 }
34 34  
35 35 // ========== 各种 AI Client 创建 ==========
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiClientFactory.java renamed to yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactory.java
... ... @@ -7,11 +7,11 @@ import org.springframework.ai.chat.model.ChatModel;
7 7 import org.springframework.ai.image.ImageModel;
8 8  
9 9 /**
10   - * AI 客户端工厂的接口类
  10 + * AI Model 模型工厂的接口类
11 11 *
12 12 * @author fansili
13 13 */
14   -public interface AiClientFactory {
  14 +public interface AiModelFactory {
15 15  
16 16 /**
17 17 * 基于指定配置,获得 ChatModel 对象
... ... @@ -33,29 +33,29 @@ public interface AiClientFactory {
33 33 * @param platform 平台
34 34 * @return ChatModel 对象
35 35 */
36   - ChatModel getDefaultChatClient(AiPlatformEnum platform);
  36 + ChatModel getDefaultChatModel(AiPlatformEnum platform);
37 37  
38 38 /**
39   - * 基于默认配置,获得 ImageClient 对象
  39 + * 基于默认配置,获得 ImageModel 对象
40 40 *
41 41 * 默认配置,指的是在 application.yaml 配置文件中的 spring.ai 相关的配置
42 42 *
43 43 * @param platform 平台
44   - * @return ImageClient 对象
  44 + * @return ImageModel 对象
45 45 */
46   - ImageModel getDefaultImageClient(AiPlatformEnum platform);
  46 + ImageModel getDefaultImageModel(AiPlatformEnum platform);
47 47  
48 48 /**
49   - * 基于指定配置,获得 ImageClient 对象
  49 + * 基于指定配置,获得 ImageModel 对象
50 50 *
51 51 * 如果不存在,则进行创建
52 52 *
53 53 * @param platform 平台
54 54 * @param apiKey API KEY
55 55 * @param url API URL
56   - * @return ImageClient 对象
  56 + * @return ImageModel 对象
57 57 */
58   - ImageModel getOrCreateImageClient(AiPlatformEnum platform, String apiKey, String url);
  58 + ImageModel getOrCreateImageModel(AiPlatformEnum platform, String apiKey, String url);
59 59  
60 60 /**
61 61 * 基于指定配置,获得 MidjourneyApi 对象
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiClientFactoryImpl.java renamed to yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java
... ... @@ -43,11 +43,11 @@ import org.springframework.web.client.RestClient;
43 43 import java.util.List;
44 44  
45 45 /**
46   - * AI 客户端工厂的实现类
  46 + * AI Model 模型工厂的实现类
47 47 *
48 48 * @author 芋道源码
49 49 */
50   -public class AiClientFactoryImpl implements AiClientFactory {
  50 +public class AiModelFactoryImpl implements AiModelFactory {
51 51  
52 52 @Override
53 53 public ChatModel getOrCreateChatClient(AiPlatformEnum platform, String apiKey, String url) {
... ... @@ -55,8 +55,6 @@ public class AiClientFactoryImpl implements AiClientFactory {
55 55 return Singleton.get(cacheKey, (Func0<ChatModel>) () -> {
56 56 //noinspection EnhancedSwitchMigration
57 57 switch (platform) {
58   - case OPENAI:
59   - return buildOpenAiChatClient(apiKey, url);
60 58 case OLLAMA:
61 59 return buildOllamaChatClient(url);
62 60 case YI_YAN:
... ... @@ -67,6 +65,8 @@ public class AiClientFactoryImpl implements AiClientFactory {
67 65 return buildQianWenChatClient(apiKey);
68 66 case DEEP_SEEK:
69 67 return buildDeepSeekChatClient(apiKey);
  68 + case OPENAI:
  69 + return buildOpenAiChatModel(apiKey, url);
70 70 default:
71 71 throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
72 72 }
... ... @@ -74,11 +74,9 @@ public class AiClientFactoryImpl implements AiClientFactory {
74 74 }
75 75  
76 76 @Override
77   - public ChatModel getDefaultChatClient(AiPlatformEnum platform) {
  77 + public ChatModel getDefaultChatModel(AiPlatformEnum platform) {
78 78 //noinspection EnhancedSwitchMigration
79 79 switch (platform) {
80   - case OPENAI:
81   - return SpringUtil.getBean(OpenAiChatModel.class);
82 80 case OLLAMA:
83 81 return SpringUtil.getBean(OllamaChatModel.class);
84 82 case YI_YAN:
... ... @@ -87,13 +85,15 @@ public class AiClientFactoryImpl implements AiClientFactory {
87 85 return SpringUtil.getBean(XingHuoChatClient.class);
88 86 case QIAN_WEN:
89 87 return SpringUtil.getBean(TongYiChatModel.class);
  88 + case OPENAI:
  89 + return SpringUtil.getBean(OpenAiChatModel.class);
90 90 default:
91 91 throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
92 92 }
93 93 }
94 94  
95 95 @Override
96   - public ImageModel getDefaultImageClient(AiPlatformEnum platform) {
  96 + public ImageModel getDefaultImageModel(AiPlatformEnum platform) {
97 97 //noinspection EnhancedSwitchMigration
98 98 switch (platform) {
99 99 case OPENAI:
... ... @@ -106,11 +106,11 @@ public class AiClientFactoryImpl implements AiClientFactory {
106 106 }
107 107  
108 108 @Override
109   - public ImageModel getOrCreateImageClient(AiPlatformEnum platform, String apiKey, String url) {
  109 + public ImageModel getOrCreateImageModel(AiPlatformEnum platform, String apiKey, String url) {
110 110 //noinspection EnhancedSwitchMigration
111 111 switch (platform) {
112 112 case OPENAI:
113   - return buildOpenAiImageClient(apiKey, url);
  113 + return buildOpenAiImageModel(apiKey, url);
114 114 case STABLE_DIFFUSION:
115 115 return buildStabilityAiImageClient(apiKey, url);
116 116 default:
... ... @@ -145,13 +145,22 @@ public class AiClientFactoryImpl implements AiClientFactory {
145 145 /**
146 146 * 可参考 {@link OpenAiAutoConfiguration}
147 147 */
148   - private static OpenAiChatModel buildOpenAiChatClient(String openAiToken, String url) {
  148 + private static OpenAiChatModel buildOpenAiChatModel(String openAiToken, String url) {
149 149 url = StrUtil.blankToDefault(url, ApiUtils.DEFAULT_BASE_URL);
150 150 OpenAiApi openAiApi = new OpenAiApi(url, openAiToken);
151 151 return new OpenAiChatModel(openAiApi);
152 152 }
153 153  
154 154 /**
  155 + * 可参考 {@link OpenAiAutoConfiguration}
  156 + */
  157 + private OpenAiImageModel buildOpenAiImageModel(String openAiToken, String url) {
  158 + url = StrUtil.blankToDefault(url, ApiUtils.DEFAULT_BASE_URL);
  159 + OpenAiImageApi openAiApi = new OpenAiImageApi(url, openAiToken, RestClient.builder());
  160 + return new OpenAiImageModel(openAiApi);
  161 + }
  162 +
  163 + /**
155 164 * 可参考 {@link OllamaAutoConfiguration}
156 165 */
157 166 private static OllamaChatModel buildOllamaChatClient(String url) {
... ... @@ -200,12 +209,6 @@ public class AiClientFactoryImpl implements AiClientFactory {
200 209 return new TongYiAutoConfiguration().tongYiChatClient(generation, chatOptions, connectionProperties);
201 210 }
202 211  
203   - private OpenAiImageModel buildOpenAiImageClient(String openAiToken, String url) {
204   - url = StrUtil.blankToDefault(url, ApiUtils.DEFAULT_BASE_URL);
205   - OpenAiImageApi openAiApi = new OpenAiImageApi(url, openAiToken, RestClient.builder());
206   - return new OpenAiImageModel(openAiApi);
207   - }
208   -
209 212 private StabilityAiImageModel buildStabilityAiImageClient(String apiKey, String url) {
210 213 url = StrUtil.blankToDefault(url, StabilityAiApi.DEFAULT_BASE_URL);
211 214 StabilityAiApi stabilityAiApi = new StabilityAiApi(apiKey, StabilityAiApi.DEFAULT_IMAGE_MODEL, url);
... ...