Commit f582c9cfa3901921d3b6f75a85ab17bf42cb9fde

Authored by YunaiV
1 parent 22801802

【代码重构】AI:使用 OpenAiApi 接入 deepseek

yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/deepseek/DeepSeekChatModel.java
1 package cn.iocoder.yudao.framework.ai.core.model.deepseek; 1 package cn.iocoder.yudao.framework.ai.core.model.deepseek;
2 2
3 -import cn.hutool.core.collection.ListUtil;  
4 -import cn.hutool.core.lang.Assert; 3 +import lombok.RequiredArgsConstructor;
5 import lombok.extern.slf4j.Slf4j; 4 import lombok.extern.slf4j.Slf4j;
6 -import org.springframework.ai.chat.metadata.ChatGenerationMetadata;  
7 import org.springframework.ai.chat.model.ChatModel; 5 import org.springframework.ai.chat.model.ChatModel;
8 import org.springframework.ai.chat.model.ChatResponse; 6 import org.springframework.ai.chat.model.ChatResponse;
9 -import org.springframework.ai.chat.model.Generation;  
10 import org.springframework.ai.chat.prompt.ChatOptions; 7 import org.springframework.ai.chat.prompt.ChatOptions;
11 import org.springframework.ai.chat.prompt.Prompt; 8 import org.springframework.ai.chat.prompt.Prompt;
12 -import org.springframework.ai.model.ModelOptionsUtils;  
13 -import org.springframework.ai.openai.OpenAiChatOptions;  
14 -import org.springframework.ai.openai.api.OpenAiApi;  
15 -import org.springframework.ai.openai.metadata.OpenAiChatResponseMetadata;  
16 -import org.springframework.ai.retry.RetryUtils;  
17 -import org.springframework.http.ResponseEntity;  
18 -import org.springframework.retry.support.RetryTemplate; 9 +import org.springframework.ai.openai.OpenAiChatModel;
19 import reactor.core.publisher.Flux; 10 import reactor.core.publisher.Flux;
20 11
21 -import java.util.HashMap;  
22 -import java.util.List;  
23 -import java.util.Map;  
24 -  
25 -import static cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatOptions.MODEL_DEFAULT;  
26 -  
27 /** 12 /**
28 * DeepSeek {@link ChatModel} 实现类 13 * DeepSeek {@link ChatModel} 实现类
29 * 14 *
30 * @author fansili 15 * @author fansili
31 */ 16 */
32 @Slf4j 17 @Slf4j
  18 +@RequiredArgsConstructor
33 public class DeepSeekChatModel implements ChatModel { 19 public class DeepSeekChatModel implements ChatModel {
34 20
35 - private static final String BASE_URL = "https://api.deepseek.com"; 21 + public static final String BASE_URL = "https://api.deepseek.com";
36 22
37 - private final DeepSeekChatOptions defaultOptions;  
38 - private final RetryTemplate retryTemplate; 23 + public static final String MODEL_DEFAULT = "deepseek-chat";
39 24
40 /** 25 /**
41 - * DeepSeek 兼容 OpenAI 的 HTTP 接口,所以复用它的实现,简化接入成本  
42 - *  
43 - * 不过要注意,DeepSeek 没有完全兼容,所以不能使用 {@link org.springframework.ai.openai.OpenAiChatModel} 调用,但是实现会参考它 26 + * 兼容 OpenAI 接口,进行复用
44 */ 27 */
45 - private final OpenAiApi openAiApi;  
46 -  
47 - public DeepSeekChatModel(String apiKey) {  
48 - this(apiKey, DeepSeekChatOptions.builder().model(MODEL_DEFAULT).temperature(0.7F).build());  
49 - }  
50 -  
51 - public DeepSeekChatModel(String apiKey, DeepSeekChatOptions options) {  
52 - this(apiKey, options, RetryUtils.DEFAULT_RETRY_TEMPLATE);  
53 - }  
54 -  
55 - public DeepSeekChatModel(String apiKey, DeepSeekChatOptions options, RetryTemplate retryTemplate) {  
56 - Assert.notEmpty(apiKey, "apiKey 不能为空");  
57 - Assert.notNull(options, "options 不能为空");  
58 - Assert.notNull(retryTemplate, "retryTemplate 不能为空");  
59 - this.openAiApi = new OpenAiApi(BASE_URL, apiKey);  
60 - this.defaultOptions = options;  
61 - this.retryTemplate = retryTemplate;  
62 - } 28 + private final OpenAiChatModel openAiChatModel;
63 29
64 @Override 30 @Override
65 public ChatResponse call(Prompt prompt) { 31 public ChatResponse call(Prompt prompt) {
66 - OpenAiApi.ChatCompletionRequest request = createRequest(prompt, false);  
67 - return this.retryTemplate.execute(ctx -> {  
68 - // 1.1 发起调用  
69 - ResponseEntity<OpenAiApi.ChatCompletion> completionEntity = openAiApi.chatCompletionEntity(request);  
70 - // 1.2 校验结果  
71 - OpenAiApi.ChatCompletion chatCompletion = completionEntity.getBody();  
72 - if (chatCompletion == null) {  
73 - log.warn("No chat completion returned for prompt: {}", prompt);  
74 - return new ChatResponse(ListUtil.of());  
75 - }  
76 - List<OpenAiApi.ChatCompletion.Choice> choices = chatCompletion.choices();  
77 - if (choices == null) {  
78 - log.warn("No choices returned for prompt: {}", prompt);  
79 - return new ChatResponse(ListUtil.of());  
80 - }  
81 -  
82 - // 2. 转换 ChatResponse 返回  
83 - List<Generation> generations = choices.stream().map(choice -> {  
84 - Generation generation = new Generation(choice.message().content(), toMap(chatCompletion.id(), choice));  
85 - if (choice.finishReason() != null) {  
86 - generation.withGenerationMetadata(ChatGenerationMetadata.from(choice.finishReason().name(), null));  
87 - }  
88 - return generation;  
89 - }).toList();  
90 - return new ChatResponse(generations,  
91 - OpenAiChatResponseMetadata.from(completionEntity.getBody()));  
92 - });  
93 - }  
94 -  
95 - private Map<String, Object> toMap(String id, OpenAiApi.ChatCompletion.Choice choice) {  
96 - Map<String, Object> map = new HashMap<>();  
97 - OpenAiApi.ChatCompletionMessage message = choice.message();  
98 - if (message.role() != null) {  
99 - map.put("role", message.role().name());  
100 - }  
101 - if (choice.finishReason() != null) {  
102 - map.put("finishReason", choice.finishReason().name());  
103 - }  
104 - map.put("id", id);  
105 - return map; 32 + return openAiChatModel.call(prompt);
106 } 33 }
107 34
108 @Override 35 @Override
109 public Flux<ChatResponse> stream(Prompt prompt) { 36 public Flux<ChatResponse> stream(Prompt prompt) {
110 - OpenAiApi.ChatCompletionRequest request = createRequest(prompt, true);  
111 - return this.retryTemplate.execute(ctx -> {  
112 - // 1. 发起调用  
113 - Flux<OpenAiApi.ChatCompletionChunk> response = this.openAiApi.chatCompletionStream(request);  
114 - return response.map(chatCompletion -> {  
115 - String id = chatCompletion.id();  
116 - // 2. 转换 ChatResponse 返回  
117 - List<Generation> generations = chatCompletion.choices().stream().map(choice -> {  
118 - String finish = (choice.finishReason() != null ? choice.finishReason().name() : "");  
119 - String role = (choice.delta().role() != null ? choice.delta().role().name() : "");  
120 - if (choice.finishReason() == OpenAiApi.ChatCompletionFinishReason.STOP) {  
121 - // 兜底处理 DeepSeek 返回 STOP 时,role 为空的情况  
122 - role = OpenAiApi.ChatCompletionMessage.Role.ASSISTANT.name();  
123 - }  
124 - Generation generation = new Generation(choice.delta().content(),  
125 - Map.of("id", id, "role", role, "finishReason", finish));  
126 - if (choice.finishReason() != null) {  
127 - generation = generation.withGenerationMetadata(  
128 - ChatGenerationMetadata.from(choice.finishReason().name(), null));  
129 - }  
130 - return generation;  
131 - }).toList();  
132 - return new ChatResponse(generations);  
133 - });  
134 - });  
135 - }  
136 -  
137 - OpenAiApi.ChatCompletionRequest createRequest(Prompt prompt, boolean stream) {  
138 - // 1. 构建 ChatCompletionMessage 对象  
139 - List<OpenAiApi.ChatCompletionMessage> chatCompletionMessages = prompt.getInstructions().stream().map(m ->  
140 - new OpenAiApi.ChatCompletionMessage(m.getContent(), OpenAiApi.ChatCompletionMessage.Role.valueOf(m.getMessageType().name()))).toList();  
141 - OpenAiApi.ChatCompletionRequest request = new OpenAiApi.ChatCompletionRequest(chatCompletionMessages, stream);  
142 -  
143 - // 2.1 补充 prompt 内置的 options  
144 - if (prompt.getOptions() != null) {  
145 - if (prompt.getOptions() instanceof ChatOptions runtimeOptions) {  
146 - OpenAiChatOptions updatedRuntimeOptions = ModelOptionsUtils.copyToTarget(runtimeOptions,  
147 - ChatOptions.class, OpenAiChatOptions.class);  
148 - request = ModelOptionsUtils.merge(updatedRuntimeOptions, request, OpenAiApi.ChatCompletionRequest.class);  
149 - } else {  
150 - throw new IllegalArgumentException("Prompt options are not of type ChatOptions: "  
151 - + prompt.getOptions().getClass().getSimpleName());  
152 - }  
153 - }  
154 - // 2.2 补充默认 options  
155 - if (this.defaultOptions != null) {  
156 - request = ModelOptionsUtils.merge(request, this.defaultOptions, OpenAiApi.ChatCompletionRequest.class);  
157 - }  
158 - return request; 37 + return openAiChatModel.stream(prompt);
159 } 38 }
160 39
161 @Override 40 @Override
162 public ChatOptions getDefaultOptions() { 41 public ChatOptions getDefaultOptions() {
163 - return DeepSeekChatOptions.fromOptions(defaultOptions); 42 + return openAiChatModel.getDefaultOptions();
164 } 43 }
165 44
166 } 45 }
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/deepseek/DeepSeekChatOptions.java deleted
1 -package cn.iocoder.yudao.framework.ai.core.model.deepseek;  
2 -  
3 -import lombok.AllArgsConstructor;  
4 -import lombok.Builder;  
5 -import lombok.Data;  
6 -import lombok.NoArgsConstructor;  
7 -import org.springframework.ai.chat.prompt.ChatOptions;  
8 -  
9 -/**  
10 - * DeepSeek {@link ChatOptions} 实现类  
11 - *  
12 - * 参考文档:<a href="https://platform.deepseek.com/api-docs/zh-cn/">快速开始</a>  
13 - *  
14 - * @author fansili  
15 - */  
16 -@Data  
17 -@NoArgsConstructor  
18 -@AllArgsConstructor  
19 -@Builder  
20 -public class DeepSeekChatOptions implements ChatOptions {  
21 -  
22 - public static final String MODEL_DEFAULT = "deepseek-chat";  
23 -  
24 - /**  
25 - * 模型  
26 - */  
27 - private String model;  
28 - /**  
29 - * 温度  
30 - */  
31 - private Float temperature;  
32 - /**  
33 - * 最大 Token  
34 - */  
35 - private Integer maxTokens;  
36 - /**  
37 - * topP  
38 - */  
39 - private Float topP;  
40 -  
41 - @Override  
42 - public Integer getTopK() {  
43 - return null;  
44 - }  
45 -  
46 - public static DeepSeekChatOptions fromOptions(DeepSeekChatOptions fromOptions) {  
47 - return DeepSeekChatOptions.builder()  
48 - .model(fromOptions.getModel())  
49 - .temperature(fromOptions.getTemperature())  
50 - .maxTokens(fromOptions.getMaxTokens())  
51 - .topP(fromOptions.getTopP())  
52 - .build();  
53 - }  
54 -  
55 -}  
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatOptions.java deleted
1 -package cn.iocoder.yudao.framework.ai.core.model.xinghuo;  
2 -  
3 -import lombok.AllArgsConstructor;  
4 -import lombok.Builder;  
5 -import lombok.Data;  
6 -import lombok.NoArgsConstructor;  
7 -import org.springframework.ai.chat.prompt.ChatOptions;  
8 -  
9 -/**  
10 - * 讯飞星火 {@link ChatOptions} 实现类  
11 - *  
12 - * 参考文档:<a href="https://www.xfyun.cn/doc/spark/HTTP%E8%B0%83%E7%94%A8%E6%96%87%E6%A1%A3.html">HTTP 调用</a>  
13 - *  
14 - * @author fansili  
15 - */  
16 -@Data  
17 -@NoArgsConstructor  
18 -@AllArgsConstructor  
19 -@Builder  
20 -public class XingHuoChatOptions implements ChatOptions {  
21 -  
22 - public static final String MODEL_DEFAULT = "generalv3.5";  
23 -  
24 - /**  
25 - * 模型  
26 - */  
27 - private String model;  
28 - /**  
29 - * 温度  
30 - */  
31 - private Float temperature;  
32 - /**  
33 - * 最大 Token  
34 - */  
35 - private Integer maxTokens;  
36 - /**  
37 - * K 个候选  
38 - */  
39 - private Integer topK;  
40 -  
41 - @Override  
42 - public Float getTopP() {  
43 - return null;  
44 - }  
45 -  
46 - public static XingHuoChatOptions fromOptions(XingHuoChatOptions fromOptions) {  
47 - return XingHuoChatOptions.builder()  
48 - .model(fromOptions.getModel())  
49 - .temperature(fromOptions.getTemperature())  
50 - .maxTokens(fromOptions.getMaxTokens())  
51 - .topK(fromOptions.getTopK())  
52 - .build();  
53 - }  
54 -  
55 -}  
yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/DeepSeekChatModelTests.java
@@ -8,6 +8,9 @@ import org.springframework.ai.chat.messages.SystemMessage; @@ -8,6 +8,9 @@ import org.springframework.ai.chat.messages.SystemMessage;
8 import org.springframework.ai.chat.messages.UserMessage; 8 import org.springframework.ai.chat.messages.UserMessage;
9 import org.springframework.ai.chat.model.ChatResponse; 9 import org.springframework.ai.chat.model.ChatResponse;
10 import org.springframework.ai.chat.prompt.Prompt; 10 import org.springframework.ai.chat.prompt.Prompt;
  11 +import org.springframework.ai.openai.OpenAiChatModel;
  12 +import org.springframework.ai.openai.OpenAiChatOptions;
  13 +import org.springframework.ai.openai.api.OpenAiApi;
11 import reactor.core.publisher.Flux; 14 import reactor.core.publisher.Flux;
12 15
13 import java.util.ArrayList; 16 import java.util.ArrayList;
@@ -20,7 +23,18 @@ import java.util.List; @@ -20,7 +23,18 @@ import java.util.List;
20 */ 23 */
21 public class DeepSeekChatModelTests { 24 public class DeepSeekChatModelTests {
22 25
23 - private final DeepSeekChatModel chatModel = new DeepSeekChatModel("sk-e94db327cc7d457d99a8de8810fc6b12"); 26 + private static final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
  27 + .openAiApi(OpenAiApi.builder()
  28 + .baseUrl(DeepSeekChatModel.BASE_URL)
  29 + .apiKey("sk-e52047409b144d97b791a6a46a2d") // apiKey
  30 + .build())
  31 + .defaultOptions(OpenAiChatOptions.builder()
  32 + .model("deepseek-chat") // 模型
  33 + .temperature(0.7)
  34 + .build())
  35 + .build();
  36 +
  37 + private final DeepSeekChatModel chatModel = new DeepSeekChatModel(openAiChatModel);
24 38
25 @Test 39 @Test
26 @Disabled 40 @Disabled