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,6 +37,9 @@ public class AiChatMessageRespVO {
37 @Schema(description = "聊天内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好,你好啊") 37 @Schema(description = "聊天内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好,你好啊")
38 private String content; 38 private String content;
39 39
  40 + @Schema(description = "推理内容", example = "要达到这个目标,你需要...")
  41 + private String reasoningContent;
  42 +
40 @Schema(description = "是否携带上下文", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") 43 @Schema(description = "是否携带上下文", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
41 private Boolean useContext; 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,6 +29,9 @@ public class AiChatMessageSendRespVO {
29 @Schema(description = "聊天内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好,你好啊") 29 @Schema(description = "聊天内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好,你好啊")
30 private String content; 30 private String content;
31 31
  32 + @Schema(description = "推理内容", example = "要达到这个目标,你需要...")
  33 + private String reasoningContent;
  34 +
32 @Schema(description = "知识库段落编号数组", example = "[1,2,3]") 35 @Schema(description = "知识库段落编号数组", example = "[1,2,3]")
33 private List<Long> segmentIds; 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,6 +87,10 @@ public class AiChatMessageDO extends BaseDO {
87 * 聊天内容 87 * 聊天内容
88 */ 88 */
89 private String content; 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,8 +3,6 @@ package cn.iocoder.yudao.module.ai.service.chat;
3 import cn.hutool.core.collection.CollUtil; 3 import cn.hutool.core.collection.CollUtil;
4 import cn.hutool.core.util.ObjUtil; 4 import cn.hutool.core.util.ObjUtil;
5 import cn.hutool.core.util.StrUtil; 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 import cn.iocoder.yudao.framework.common.pojo.CommonResult; 6 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
9 import cn.iocoder.yudao.framework.common.pojo.PageResult; 7 import cn.iocoder.yudao.framework.common.pojo.PageResult;
10 import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 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,6 +19,7 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO;
21 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiToolDO; 19 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiToolDO;
22 import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatMessageMapper; 20 import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatMessageMapper;
23 import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants; 21 import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants;
  22 +import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
24 import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeDocumentService; 23 import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeDocumentService;
25 import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeSegmentService; 24 import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeSegmentService;
26 import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchReqBO; 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,6 +27,7 @@ import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchR
28 import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService; 27 import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService;
29 import cn.iocoder.yudao.module.ai.service.model.AiModelService; 28 import cn.iocoder.yudao.module.ai.service.model.AiModelService;
30 import cn.iocoder.yudao.module.ai.service.model.AiToolService; 29 import cn.iocoder.yudao.module.ai.service.model.AiToolService;
  30 +import cn.iocoder.yudao.module.ai.util.AiUtils;
31 import jakarta.annotation.Resource; 31 import jakarta.annotation.Resource;
32 import lombok.extern.slf4j.Slf4j; 32 import lombok.extern.slf4j.Slf4j;
33 import org.springframework.ai.chat.messages.Message; 33 import org.springframework.ai.chat.messages.Message;
@@ -118,8 +118,10 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { @@ -118,8 +118,10 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
118 ChatResponse chatResponse = chatModel.call(prompt); 118 ChatResponse chatResponse = chatModel.call(prompt);
119 119
120 // 3.3 更新响应内容 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 // 3.4 响应结果 125 // 3.4 响应结果
124 Map<Long, AiKnowledgeDocumentDO> documentMap = knowledgeDocumentService.getKnowledgeDocumentMap( 126 Map<Long, AiKnowledgeDocumentDO> documentMap = knowledgeDocumentService.getKnowledgeDocumentMap(
125 convertSet(knowledgeSegments, AiKnowledgeSegmentSearchRespBO::getDocumentId)); 127 convertSet(knowledgeSegments, AiKnowledgeSegmentSearchRespBO::getDocumentId));
@@ -168,6 +170,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { @@ -168,6 +170,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
168 170
169 // 4.3 流式返回 171 // 4.3 流式返回
170 StringBuffer contentBuffer = new StringBuffer(); 172 StringBuffer contentBuffer = new StringBuffer();
  173 + StringBuffer reasoningContentBuffer = new StringBuffer();
171 return streamResponse.map(chunk -> { 174 return streamResponse.map(chunk -> {
172 // 处理知识库的返回,只有首次才有 175 // 处理知识库的返回,只有首次才有
173 List<AiChatMessageRespVO.KnowledgeSegment> segments = null; 176 List<AiChatMessageRespVO.KnowledgeSegment> segments = null;
@@ -181,22 +184,31 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { @@ -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 return success(new AiChatMessageSendRespVO() 195 return success(new AiChatMessageSendRespVO()
188 .setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class)) 196 .setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class))
189 .setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class) 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 }).doOnComplete(() -> { 201 }).doOnComplete(() -> {
192 // 忽略租户,因为 Flux 异步无法透传租户 202 // 忽略租户,因为 Flux 异步无法透传租户
193 TenantUtils.executeIgnore(() -> chatMessageMapper.updateById( 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 }).doOnError(throwable -> { 206 }).doOnError(throwable -> {
196 log.error("[sendChatMessageStream][userId({}) sendReqVO({}) 发生异常]", userId, sendReqVO, throwable); 207 log.error("[sendChatMessageStream][userId({}) sendReqVO({}) 发生异常]", userId, sendReqVO, throwable);
197 // 忽略租户,因为 Flux 异步无法透传租户 208 // 忽略租户,因为 Flux 异步无法透传租户
198 TenantUtils.executeIgnore(() -> chatMessageMapper.updateById( 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 }).onErrorResume(error -> Flux.just(error(ErrorCodeConstants.CHAT_STREAM_ERROR))); 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,7 +11,9 @@ import org.springaicommunity.qianfan.QianFanChatOptions;
11 import org.springframework.ai.anthropic.AnthropicChatOptions; 11 import org.springframework.ai.anthropic.AnthropicChatOptions;
12 import org.springframework.ai.azure.openai.AzureOpenAiChatOptions; 12 import org.springframework.ai.azure.openai.AzureOpenAiChatOptions;
13 import org.springframework.ai.chat.messages.*; 13 import org.springframework.ai.chat.messages.*;
  14 +import org.springframework.ai.chat.model.ChatResponse;
14 import org.springframework.ai.chat.prompt.ChatOptions; 15 import org.springframework.ai.chat.prompt.ChatOptions;
  16 +import org.springframework.ai.deepseek.DeepSeekAssistantMessage;
15 import org.springframework.ai.deepseek.DeepSeekChatOptions; 17 import org.springframework.ai.deepseek.DeepSeekChatOptions;
16 import org.springframework.ai.minimax.MiniMaxChatOptions; 18 import org.springframework.ai.minimax.MiniMaxChatOptions;
17 import org.springframework.ai.ollama.api.OllamaOptions; 19 import org.springframework.ai.ollama.api.OllamaOptions;
@@ -45,6 +47,7 @@ public class AiUtils { @@ -45,6 +47,7 @@ public class AiUtils {
45 switch (platform) { 47 switch (platform) {
46 case TONG_YI: 48 case TONG_YI:
47 return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).withMaxToken(maxTokens) 49 return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).withMaxToken(maxTokens)
  50 + .withEnableThinking(true) // TODO 芋艿:默认都开启 thinking 模式,后续可以让用户配置
48 .withToolNames(toolNames).withToolContext(toolContext).build(); 51 .withToolNames(toolNames).withToolContext(toolContext).build();
49 case YI_YAN: 52 case YI_YAN:
50 return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); 53 return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build();
@@ -106,4 +109,27 @@ public class AiUtils { @@ -106,4 +109,27 @@ public class AiUtils {
106 return context; 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 \ No newline at end of file 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,7 +75,8 @@ public class TongYiChatModelTests {
75 List<Message> messages = new ArrayList<>(); 75 List<Message> messages = new ArrayList<>();
76 messages.add(new UserMessage("详细分析下,如何设计一个电商系统?")); 76 messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
77 DashScopeChatOptions options = DashScopeChatOptions.builder() 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 .withEnableThinking(true) // 必须设置,否则会报错 80 .withEnableThinking(true) // 必须设置,否则会报错
80 .build(); 81 .build();
81 82