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 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 4 import lombok.extern.slf4j.Slf4j;
6   -import org.springframework.ai.chat.metadata.ChatGenerationMetadata;
7 5 import org.springframework.ai.chat.model.ChatModel;
8 6 import org.springframework.ai.chat.model.ChatResponse;
9   -import org.springframework.ai.chat.model.Generation;
10 7 import org.springframework.ai.chat.prompt.ChatOptions;
11 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 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 13 * DeepSeek {@link ChatModel} 实现类
29 14 *
30 15 * @author fansili
31 16 */
32 17 @Slf4j
  18 +@RequiredArgsConstructor
33 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 30 @Override
65 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 35 @Override
109 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 40 @Override
162 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 8 import org.springframework.ai.chat.messages.UserMessage;
9 9 import org.springframework.ai.chat.model.ChatResponse;
10 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 14 import reactor.core.publisher.Flux;
12 15  
13 16 import java.util.ArrayList;
... ... @@ -20,7 +23,18 @@ import java.util.List;
20 23 */
21 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 39 @Test
26 40 @Disabled
... ...