Commit 45a9dfc4fac642cb497ad4356d42b5a442f43ded

Authored by YunaiV
1 parent 69d99aa4

feat:【ai 大模型】增加 thinking 深度思考

yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageRespVO.java
... ... @@ -37,6 +37,9 @@ public class AiChatMessageRespVO {
37 37 @Schema(description = "聊天内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好,你好啊")
38 38 private String content;
39 39  
  40 + @Schema(description = "推理内容", example = "要达到这个目标,你需要...")
  41 + private String reasoningContent;
  42 +
40 43 @Schema(description = "是否携带上下文", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
41 44 private Boolean useContext;
42 45  
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendRespVO.java
... ... @@ -29,6 +29,9 @@ public class AiChatMessageSendRespVO {
29 29 @Schema(description = "聊天内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好,你好啊")
30 30 private String content;
31 31  
  32 + @Schema(description = "推理内容", example = "要达到这个目标,你需要...")
  33 + private String reasoningContent;
  34 +
32 35 @Schema(description = "知识库段落编号数组", example = "[1,2,3]")
33 36 private List<Long> segmentIds;
34 37  
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java
... ... @@ -87,6 +87,10 @@ public class AiChatMessageDO extends BaseDO {
87 87 * 聊天内容
88 88 */
89 89 private String content;
  90 + /**
  91 + * 推理内容
  92 + */
  93 + private String reasoningContent;
90 94  
91 95 /**
92 96 * 是否携带上下文
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java
... ... @@ -3,8 +3,6 @@ package cn.iocoder.yudao.module.ai.service.chat;
3 3 import cn.hutool.core.collection.CollUtil;
4 4 import cn.hutool.core.util.ObjUtil;
5 5 import cn.hutool.core.util.StrUtil;
6   -import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
7   -import cn.iocoder.yudao.module.ai.util.AiUtils;
8 6 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
9 7 import cn.iocoder.yudao.framework.common.pojo.PageResult;
10 8 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
... ... @@ -21,6 +19,7 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO;
21 19 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiToolDO;
22 20 import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatMessageMapper;
23 21 import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants;
  22 +import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
24 23 import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeDocumentService;
25 24 import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeSegmentService;
26 25 import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchReqBO;
... ... @@ -28,6 +27,7 @@ import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchR
28 27 import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService;
29 28 import cn.iocoder.yudao.module.ai.service.model.AiModelService;
30 29 import cn.iocoder.yudao.module.ai.service.model.AiToolService;
  30 +import cn.iocoder.yudao.module.ai.util.AiUtils;
31 31 import jakarta.annotation.Resource;
32 32 import lombok.extern.slf4j.Slf4j;
33 33 import org.springframework.ai.chat.messages.Message;
... ... @@ -118,8 +118,10 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
118 118 ChatResponse chatResponse = chatModel.call(prompt);
119 119  
120 120 // 3.3 更新响应内容
121   - String newContent = chatResponse.getResult().getOutput().getText();
122   - chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(newContent));
  121 + String newContent = AiUtils.getChatResponseContent(chatResponse);
  122 + String newReasoningContent = AiUtils.getChatResponseReasoningContent(chatResponse);
  123 + chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId())
  124 + .setContent(newContent).setReasoningContent(newReasoningContent));
123 125 // 3.4 响应结果
124 126 Map<Long, AiKnowledgeDocumentDO> documentMap = knowledgeDocumentService.getKnowledgeDocumentMap(
125 127 convertSet(knowledgeSegments, AiKnowledgeSegmentSearchRespBO::getDocumentId));
... ... @@ -168,6 +170,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
168 170  
169 171 // 4.3 流式返回
170 172 StringBuffer contentBuffer = new StringBuffer();
  173 + StringBuffer reasoningContentBuffer = new StringBuffer();
171 174 return streamResponse.map(chunk -> {
172 175 // 处理知识库的返回,只有首次才有
173 176 List<AiChatMessageRespVO.KnowledgeSegment> segments = null;
... ... @@ -181,22 +184,31 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
181 184 });
182 185 }
183 186 // 响应结果
184   - String newContent = chunk.getResult() != null ? chunk.getResult().getOutput().getText() : null;
185   - newContent = StrUtil.nullToDefault(newContent, ""); // 避免 null 的 情况
186   - contentBuffer.append(newContent);
  187 + String newContent = AiUtils.getChatResponseContent(chunk);
  188 + String newReasoningContent = AiUtils.getChatResponseReasoningContent(chunk);
  189 + if (StrUtil.isNotEmpty(newContent)) {
  190 + contentBuffer.append(newContent);
  191 + }
  192 + if (StrUtil.isNotEmpty(newReasoningContent)) {
  193 + reasoningContentBuffer.append(newReasoningContent);
  194 + }
187 195 return success(new AiChatMessageSendRespVO()
188 196 .setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class))
189 197 .setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class)
190   - .setContent(newContent).setSegments(segments)));
  198 + .setContent(StrUtil.nullToDefault(newContent, "")) // 避免 null 的 情况
  199 + .setReasoningContent(StrUtil.nullToDefault(newReasoningContent, "")) // 避免 null 的 情况
  200 + .setSegments(segments))); // 知识库返回
191 201 }).doOnComplete(() -> {
192 202 // 忽略租户,因为 Flux 异步无法透传租户
193 203 TenantUtils.executeIgnore(() -> chatMessageMapper.updateById(
194   - new AiChatMessageDO().setId(assistantMessage.getId()).setContent(contentBuffer.toString())));
  204 + new AiChatMessageDO().setId(assistantMessage.getId()).setContent(contentBuffer.toString())
  205 + .setReasoningContent(reasoningContentBuffer.toString())));
195 206 }).doOnError(throwable -> {
196 207 log.error("[sendChatMessageStream][userId({}) sendReqVO({}) 发生异常]", userId, sendReqVO, throwable);
197 208 // 忽略租户,因为 Flux 异步无法透传租户
198 209 TenantUtils.executeIgnore(() -> chatMessageMapper.updateById(
199   - new AiChatMessageDO().setId(assistantMessage.getId()).setContent(throwable.getMessage())));
  210 + new AiChatMessageDO().setId(assistantMessage.getId()).setContent(throwable.getMessage())
  211 + .setReasoningContent(reasoningContentBuffer.toString())));
200 212 }).onErrorResume(error -> Flux.just(error(ErrorCodeConstants.CHAT_STREAM_ERROR)));
201 213 }
202 214  
... ...
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/util/AiUtils.java
... ... @@ -11,7 +11,9 @@ import org.springaicommunity.qianfan.QianFanChatOptions;
11 11 import org.springframework.ai.anthropic.AnthropicChatOptions;
12 12 import org.springframework.ai.azure.openai.AzureOpenAiChatOptions;
13 13 import org.springframework.ai.chat.messages.*;
  14 +import org.springframework.ai.chat.model.ChatResponse;
14 15 import org.springframework.ai.chat.prompt.ChatOptions;
  16 +import org.springframework.ai.deepseek.DeepSeekAssistantMessage;
15 17 import org.springframework.ai.deepseek.DeepSeekChatOptions;
16 18 import org.springframework.ai.minimax.MiniMaxChatOptions;
17 19 import org.springframework.ai.ollama.api.OllamaOptions;
... ... @@ -45,6 +47,7 @@ public class AiUtils {
45 47 switch (platform) {
46 48 case TONG_YI:
47 49 return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).withMaxToken(maxTokens)
  50 + .withEnableThinking(true) // TODO 芋艿:默认都开启 thinking 模式,后续可以让用户配置
48 51 .withToolNames(toolNames).withToolContext(toolContext).build();
49 52 case YI_YAN:
50 53 return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build();
... ... @@ -106,4 +109,27 @@ public class AiUtils {
106 109 return context;
107 110 }
108 111  
  112 + @SuppressWarnings("ConstantValue")
  113 + public static String getChatResponseContent(ChatResponse response) {
  114 + if (response == null
  115 + || response.getResult() == null
  116 + || response.getResult().getOutput() == null) {
  117 + return null;
  118 + }
  119 + return response.getResult().getOutput().getText();
  120 + }
  121 +
  122 + @SuppressWarnings("ConstantValue")
  123 + public static String getChatResponseReasoningContent(ChatResponse response) {
  124 + if (response == null
  125 + || response.getResult() == null
  126 + || response.getResult().getOutput() == null) {
  127 + return null;
  128 + }
  129 + if (response.getResult().getOutput() instanceof DeepSeekAssistantMessage) {
  130 + return ((DeepSeekAssistantMessage) (response.getResult().getOutput())).getReasoningContent();
  131 + }
  132 + return null;
  133 + }
  134 +
109 135 }
110 136 \ No newline at end of file
... ...
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/TongYiChatModelTests.java
... ... @@ -75,7 +75,8 @@ public class TongYiChatModelTests {
75 75 List<Message> messages = new ArrayList<>();
76 76 messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
77 77 DashScopeChatOptions options = DashScopeChatOptions.builder()
78   - .withModel("qwen3-235b-a22b-thinking-2507")
  78 +// .withModel("qwen3-235b-a22b-thinking-2507")
  79 + .withModel("qwen-max-2025-01-25")
79 80 .withEnableThinking(true) // 必须设置,否则会报错
80 81 .build();
81 82  
... ...