Commit 276ef98ff1b0387559225efb276f5509fc175873
1 parent
275d1fb6
【新增】AI:conversation 发送消息时,增加上下文
Showing
4 changed files
with
98 additions
and
98 deletions
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageRespVO.java
| ... | ... | @@ -19,10 +19,10 @@ public class AiChatMessageRespVO { |
| 19 | 19 | private String type; // 参见 MessageType 枚举类 |
| 20 | 20 | |
| 21 | 21 | @Schema(description = "用户编号", example = "4096") |
| 22 | - private Long userId; // 仅当 user 发送时非空 | |
| 22 | + private Long userId; | |
| 23 | 23 | |
| 24 | 24 | @Schema(description = "角色编号", example = "888") |
| 25 | - private Long roleId; // 仅当 assistant 回复时非空 | |
| 25 | + private Long roleId; | |
| 26 | 26 | |
| 27 | 27 | @Schema(description = "模型标志", requiredMode = Schema.RequiredMode.REQUIRED, example = "gpt-3.5-turbo") |
| 28 | 28 | private String model; // 参见 AiOpenAiModelEnum 枚举类 | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java
| ... | ... | @@ -47,16 +47,12 @@ public class AiChatMessageDO extends BaseDO { |
| 47 | 47 | /** |
| 48 | 48 | * 用户编号 |
| 49 | 49 | * |
| 50 | - * 仅当 user 发送时非空 | |
| 51 | - * | |
| 52 | 50 | * 关联 AdminUserDO 的 userId 字段 |
| 53 | 51 | */ |
| 54 | 52 | private Long userId; |
| 55 | 53 | /** |
| 56 | 54 | * 角色编号 |
| 57 | 55 | * |
| 58 | - * 仅当 assistant 回复时非空 | |
| 59 | - * | |
| 60 | 56 | * 关联 {@link AiChatRoleDO#getId()} 字段 |
| 61 | 57 | */ |
| 62 | 58 | private Long roleId; | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/impl/AiChatServiceImpl.java
| 1 | 1 | package cn.iocoder.yudao.module.ai.service.impl; |
| 2 | 2 | |
| 3 | -import cn.hutool.core.exceptions.ExceptionUtil; | |
| 4 | 3 | import cn.hutool.core.util.ObjUtil; |
| 5 | 4 | import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; |
| 5 | +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; | |
| 6 | 6 | import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendRespVO; |
| 7 | 7 | import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO; |
| 8 | -import org.springframework.ai.chat.ChatClient; | |
| 9 | 8 | import org.springframework.ai.chat.ChatResponse; |
| 10 | 9 | import org.springframework.ai.chat.StreamingChatClient; |
| 11 | -import org.springframework.ai.chat.messages.MessageType; | |
| 10 | +import org.springframework.ai.chat.messages.*; | |
| 11 | +import org.springframework.ai.chat.prompt.ChatOptions; | |
| 12 | +import org.springframework.ai.chat.prompt.ChatOptionsBuilder; | |
| 12 | 13 | import org.springframework.ai.chat.prompt.Prompt; |
| 13 | -import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; | |
| 14 | 14 | import cn.iocoder.yudao.module.ai.config.AiChatClientFactory; |
| 15 | 15 | import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageRespVO; |
| 16 | 16 | import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendReqVO; |
| 17 | 17 | import cn.iocoder.yudao.module.ai.convert.AiChatMessageConvert; |
| 18 | 18 | import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatMessageDO; |
| 19 | 19 | import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; |
| 20 | -import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; | |
| 21 | 20 | import cn.iocoder.yudao.module.ai.dal.mysql.AiChatMessageMapper; |
| 22 | 21 | import cn.iocoder.yudao.module.ai.service.chat.AiChatConversationService; |
| 23 | 22 | import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService; |
| ... | ... | @@ -30,10 +29,7 @@ import org.springframework.transaction.annotation.Transactional; |
| 30 | 29 | import reactor.core.publisher.Flux; |
| 31 | 30 | |
| 32 | 31 | import java.time.LocalDateTime; |
| 33 | -import java.util.List; | |
| 34 | -import java.util.Map; | |
| 35 | -import java.util.Set; | |
| 36 | -import java.util.concurrent.atomic.AtomicInteger; | |
| 32 | +import java.util.*; | |
| 37 | 33 | import java.util.stream.Collectors; |
| 38 | 34 | |
| 39 | 35 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| ... | ... | @@ -53,64 +49,49 @@ public class AiChatServiceImpl implements AiChatService { |
| 53 | 49 | |
| 54 | 50 | private final AiChatClientFactory chatClientFactory; |
| 55 | 51 | |
| 56 | - private final AiChatMessageMapper aiChatMessageMapper; | |
| 52 | + private final AiChatMessageMapper chatMessageMapper; | |
| 53 | + | |
| 57 | 54 | private final AiChatConversationService chatConversationService; |
| 58 | 55 | private final AiChatModelService chatModalService; |
| 59 | 56 | private final AiChatRoleService chatRoleService; |
| 60 | 57 | |
| 61 | 58 | @Transactional(rollbackFor = Exception.class) |
| 62 | 59 | public AiChatMessageRespVO chat(AiChatMessageSendReqVO req) { |
| 63 | - Long loginUserId = SecurityFrameworkUtils.getLoginUserId(); | |
| 64 | - // 查询对话 | |
| 65 | - AiChatConversationDO conversation = chatConversationService.validateExists(req.getConversationId()); | |
| 66 | - // 获取对话模型 | |
| 67 | - AiChatModelDO chatModel = chatModalService.validateChatModel(conversation.getModelId()); | |
| 68 | - // 获取角色信息 | |
| 69 | - AiChatRoleDO chatRoleDO = conversation.getRoleId() != null ? chatRoleService.validateChatRole(conversation.getRoleId()) : null; | |
| 70 | - // 获取 client 类型 | |
| 71 | - AiPlatformEnum platformEnum = AiPlatformEnum.validatePlatform(chatModel.getPlatform()); | |
| 72 | - // 保存 chat message | |
| 73 | - insertChatMessage(conversation.getId(), MessageType.USER, loginUserId, conversation.getRoleId(), | |
| 74 | - chatModel.getModel(), chatModel.getId(), req.getContent()); | |
| 75 | - String content = null; | |
| 76 | - int tokens = 0; | |
| 77 | - try { | |
| 78 | - // 创建 chat 需要的 Prompt | |
| 79 | - Prompt prompt = new Prompt(req.getContent()); | |
| 80 | - // TODO @芋艿 @范 看要不要支持这些 | |
| 81 | -// req.setTopK(req.getTopK()); | |
| 82 | -// req.setTopP(req.getTopP()); | |
| 83 | -// req.setTemperature(req.getTemperature()); | |
| 84 | - // 发送 call 调用 | |
| 85 | - ChatClient chatClient = chatClientFactory.getChatClient(platformEnum); | |
| 86 | - ChatResponse call = chatClient.call(prompt); | |
| 87 | - content = call.getResult().getOutput().getContent(); | |
| 88 | - tokens = call.getResults().size(); | |
| 89 | - // 更新 conversation | |
| 90 | - } catch (Exception e) { | |
| 91 | - content = ExceptionUtil.getMessage(e); | |
| 92 | - } finally { | |
| 93 | - // 保存 chat message | |
| 94 | - insertChatMessage(conversation.getId(), MessageType.SYSTEM, loginUserId, conversation.getRoleId(), | |
| 95 | - chatModel.getModel(), chatModel.getId(), content); | |
| 96 | - } | |
| 97 | - return new AiChatMessageRespVO().setContent(content); | |
| 98 | - } | |
| 99 | - | |
| 100 | - private AiChatMessageDO insertChatMessage(Long conversationId, MessageType messageType, Long loginUserId, Long roleId, | |
| 101 | - String model, Long modelId, String content) { | |
| 102 | - AiChatMessageDO insertChatMessageDO = new AiChatMessageDO() | |
| 103 | - .setConversationId(conversationId) | |
| 104 | - .setType(messageType.getValue()) | |
| 105 | - .setUserId(loginUserId) | |
| 106 | - .setRoleId(roleId) | |
| 107 | - .setModel(model) | |
| 108 | - .setModelId(modelId) | |
| 109 | - .setContent(content); | |
| 110 | - insertChatMessageDO.setCreateTime(LocalDateTime.now()); | |
| 111 | - // 增加 chat message 记录 | |
| 112 | - aiChatMessageMapper.insert(insertChatMessageDO); | |
| 113 | - return insertChatMessageDO; | |
| 60 | + return null; // TODO 芋艿:一起改 | |
| 61 | +// Long loginUserId = SecurityFrameworkUtils.getLoginUserId(); | |
| 62 | +// // 查询对话 | |
| 63 | +// AiChatConversationDO conversation = chatConversationService.validateExists(req.getConversationId()); | |
| 64 | +// // 获取对话模型 | |
| 65 | +// AiChatModelDO chatModel = chatModalService.validateChatModel(conversation.getModelId()); | |
| 66 | +// // 获取角色信息 | |
| 67 | +// AiChatRoleDO chatRoleDO = conversation.getRoleId() != null ? chatRoleService.validateChatRole(conversation.getRoleId()) : null; | |
| 68 | +// // 获取 client 类型 | |
| 69 | +// AiPlatformEnum platformEnum = AiPlatformEnum.validatePlatform(chatModel.getPlatform()); | |
| 70 | +// // 保存 chat message | |
| 71 | +// createChatMessage(conversation.getId(), MessageType.USER, loginUserId, conversation.getRoleId(), | |
| 72 | +// chatModel.getModel(), chatModel.getId(), req.getContent()); | |
| 73 | +// String content = null; | |
| 74 | +// int tokens = 0; | |
| 75 | +// try { | |
| 76 | +// // 创建 chat 需要的 Prompt | |
| 77 | +// Prompt prompt = new Prompt(req.getContent()); | |
| 78 | +// // TODO @芋艿 @范 看要不要支持这些 | |
| 79 | +//// req.setTopK(req.getTopK()); | |
| 80 | +//// req.setTopP(req.getTopP()); | |
| 81 | +//// req.setTemperature(req.getTemperature()); | |
| 82 | +// // 发送 call 调用 | |
| 83 | +// ChatClient chatClient = chatClientFactory.getChatClient(platformEnum); | |
| 84 | +// ChatResponse call = chatClient.call(prompt); | |
| 85 | +// content = call.getResult().getOutput().getContent(); | |
| 86 | +// // 更新 conversation | |
| 87 | +// } catch (Exception e) { | |
| 88 | +// content = ExceptionUtil.getMessage(e); | |
| 89 | +// } finally { | |
| 90 | +// // 保存 chat message | |
| 91 | +// createChatMessage(conversation.getId(), MessageType.SYSTEM, loginUserId, conversation.getRoleId(), | |
| 92 | +// chatModel.getModel(), chatModel.getId(), content); | |
| 93 | +// } | |
| 94 | +// return new AiChatMessageRespVO().setContent(content); | |
| 114 | 95 | } |
| 115 | 96 | |
| 116 | 97 | @Override |
| ... | ... | @@ -120,55 +101,78 @@ public class AiChatServiceImpl implements AiChatService { |
| 120 | 101 | if (ObjUtil.notEqual(conversation.getUserId(), userId)) { |
| 121 | 102 | throw exception(CHAT_CONVERSATION_NOT_EXISTS); // TODO 芋艿:异常情况的对接; |
| 122 | 103 | } |
| 104 | + List<AiChatMessageDO> historyMessages = chatMessageMapper.selectByConversationId(conversation.getId()); | |
| 123 | 105 | // 1.2 校验模型 |
| 124 | 106 | AiChatModelDO model = chatModalService.validateChatModel(conversation.getModelId()); |
| 125 | 107 | AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform()); |
| 126 | 108 | StreamingChatClient chatClient = chatClientFactory.getStreamingChatClient(platform); |
| 127 | 109 | |
| 128 | 110 | // 2. 插入 user 发送消息 |
| 129 | - AiChatMessageDO userMessage = insertChatMessage(conversation.getId(), MessageType.USER, userId, conversation.getRoleId(), | |
| 130 | - conversation.getModel(), conversation.getId(), sendReqVO.getContent()); | |
| 111 | + AiChatMessageDO userMessage = createChatMessage(conversation.getId(), model, | |
| 112 | + userId, conversation.getRoleId(), MessageType.USER, sendReqVO.getContent()); | |
| 113 | + | |
| 114 | + // 3.1 插入 assistant 接收消息 | |
| 115 | + AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), model, | |
| 116 | + userId, conversation.getRoleId(), MessageType.ASSISTANT, ""); | |
| 131 | 117 | |
| 132 | - // 3.1 插入 system 接收消息 | |
| 133 | - AiChatMessageDO systemMessage = insertChatMessage(conversation.getId(), MessageType.SYSTEM, userId, conversation.getRoleId(), | |
| 134 | - conversation.getModel(), conversation.getId(), conversation.getSystemMessage()); | |
| 135 | 118 | // 3.2 创建 chat 需要的 Prompt |
| 136 | 119 | // TODO 消息上下文 |
| 137 | - Prompt prompt = new Prompt(sendReqVO.getContent()); | |
| 138 | -// ChatOptionsBuilder.builder().withTemperature(conversation.getTemperature().floatValue()).build() | |
| 120 | + Prompt prompt = buildPrompt(conversation, historyMessages, sendReqVO); | |
| 139 | 121 | Flux<ChatResponse> streamResponse = chatClient.stream(prompt); |
| 140 | - // 3.3 转换 flex AiChatMessageRespVO | |
| 122 | + | |
| 123 | + // 3.3 流式返回 | |
| 141 | 124 | StringBuffer contentBuffer = new StringBuffer(); |
| 142 | - return streamResponse.map(res -> { | |
| 143 | - contentBuffer.append(res.getResult().getOutput().getContent()); | |
| 144 | - AiChatMessageSendRespVO.Message send = new AiChatMessageSendRespVO.Message().setId(userMessage.getId()) | |
| 145 | - .setType(MessageType.USER.getValue()).setCreateTime(userMessage.getCreateTime()) | |
| 146 | - .setContent(sendReqVO.getContent()); | |
| 147 | - AiChatMessageSendRespVO.Message receive = new AiChatMessageSendRespVO.Message().setId(systemMessage.getId()) | |
| 148 | - .setType(MessageType.SYSTEM.getValue()).setCreateTime(systemMessage.getCreateTime()) | |
| 149 | - .setContent(res.getResult().getOutput().getContent()); | |
| 150 | - return new AiChatMessageSendRespVO().setSend(send).setReceive(receive); | |
| 125 | + return streamResponse.map(response -> { | |
| 126 | + String newContent = response.getResult().getOutput().getContent(); | |
| 127 | + contentBuffer.append(newContent); | |
| 128 | + // 响应结果 | |
| 129 | + return new AiChatMessageSendRespVO().setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class)) | |
| 130 | + .setReceive(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class).setContent(newContent)); | |
| 151 | 131 | }).doOnComplete(() -> { |
| 152 | - log.info("发送完成!"); | |
| 153 | - // 保存 chat message | |
| 154 | - aiChatMessageMapper.updateById(new AiChatMessageDO() | |
| 155 | - .setId(systemMessage.getId()) | |
| 156 | - .setContent(contentBuffer.toString())); | |
| 132 | + chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(contentBuffer.toString())); | |
| 157 | 133 | }).doOnError(throwable -> { |
| 158 | - log.error("发送错误 {}!", throwable.getMessage()); | |
| 159 | - // 更新错误信息 TODO 貌似不应该更新异常 | |
| 160 | - aiChatMessageMapper.updateById(new AiChatMessageDO() | |
| 161 | - .setId(systemMessage.getId()) | |
| 162 | - .setContent(throwable.getMessage())); | |
| 134 | + log.error("[sendChatMessageStream][userId({}) sendReqVO({}) 发生异常]", userId, sendReqVO, throwable); | |
| 135 | + chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(throwable.getMessage())); | |
| 163 | 136 | }); |
| 164 | 137 | } |
| 165 | 138 | |
| 139 | + private Prompt buildPrompt(AiChatConversationDO conversation, List<AiChatMessageDO> messages, AiChatMessageSendReqVO sendReqVO) { | |
| 140 | + // TODO 芋艿:1)保留 n 个上下文;2)每一轮 token 数量 | |
| 141 | +// if (conversation.getMaxContexts() != null && messages.size() > conversation.getMaxContexts()) { | |
| 142 | +// | |
| 143 | +// } | |
| 144 | + // 1. 构建 Prompt Message 列表 | |
| 145 | + List<Message> chatMessages = new ArrayList<>(); | |
| 146 | + // 1.1 system context 角色设定 | |
| 147 | + chatMessages.add(new SystemMessage(conversation.getSystemMessage())); | |
| 148 | + // 1.2 history message 历史消息 | |
| 149 | + messages.forEach(message -> chatMessages.add(new ChatMessage(message.getType().toUpperCase(), message.getContent()))); | |
| 150 | + // 1.3 user message 新发送消息 | |
| 151 | + chatMessages.add(new UserMessage(sendReqVO.getContent())); | |
| 152 | + | |
| 153 | + // 2. 构建 ChatOptions 对象 | |
| 154 | + ChatOptions chatOptions = ChatOptionsBuilder.builder().withTemperature(conversation.getTemperature().floatValue()).build(); | |
| 155 | + return new Prompt(chatMessages, chatOptions); | |
| 156 | + } | |
| 157 | + | |
| 158 | + private AiChatMessageDO createChatMessage(Long conversationId, AiChatModelDO model, | |
| 159 | + Long userId, Long roleId, | |
| 160 | + MessageType messageType, String content) { | |
| 161 | + AiChatMessageDO message = new AiChatMessageDO() | |
| 162 | + .setConversationId(conversationId).setModel(model.getModel()).setModelId(model.getId()) | |
| 163 | + .setUserId(userId).setRoleId(roleId) | |
| 164 | + .setType(messageType.getValue()).setContent(content); | |
| 165 | + message.setCreateTime(LocalDateTime.now()); | |
| 166 | + chatMessageMapper.insert(message); | |
| 167 | + return message; | |
| 168 | + } | |
| 169 | + | |
| 166 | 170 | @Override |
| 167 | 171 | public List<AiChatMessageRespVO> getMessageListByConversationId(Long conversationId) { |
| 168 | 172 | // 校验对话是否存在 |
| 169 | 173 | chatConversationService.validateExists(conversationId); |
| 170 | 174 | // 获取对话所有 message |
| 171 | - List<AiChatMessageDO> aiChatMessageDOList = aiChatMessageMapper.selectByConversationId(conversationId); | |
| 175 | + List<AiChatMessageDO> aiChatMessageDOList = chatMessageMapper.selectByConversationId(conversationId); | |
| 172 | 176 | // 获取模型信息 |
| 173 | 177 | Set<Long> modalIds = aiChatMessageDOList.stream().map(AiChatMessageDO::getModelId).collect(Collectors.toSet()); |
| 174 | 178 | List<AiChatModelDO> modalList = chatModalService.getModalByIds(modalIds); |
| ... | ... | @@ -187,7 +191,7 @@ public class AiChatServiceImpl implements AiChatService { |
| 187 | 191 | |
| 188 | 192 | @Override |
| 189 | 193 | public Boolean deleteMessage(Long id) { |
| 190 | - return aiChatMessageMapper.deleteById(id) > 0; | |
| 194 | + return chatMessageMapper.deleteById(id) > 0; | |
| 191 | 195 | } |
| 192 | 196 | |
| 193 | 197 | } | ... | ... |
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java
| ... | ... | @@ -15,13 +15,13 @@ import lombok.Getter; |
| 15 | 15 | public enum AiPlatformEnum { |
| 16 | 16 | |
| 17 | 17 | OPENAI("OpenAI", "OpenAI"), |
| 18 | - OLLAMA("dall", "dall"), | |
| 18 | + OLLAMA("Ollama", "Ollama"), | |
| 19 | 19 | |
| 20 | 20 | YI_YAN("yiyan", "一言"), |
| 21 | 21 | QIAN_WEN("qianwen", "千问"), |
| 22 | 22 | XING_HUO("xinghuo", "星火"), |
| 23 | 23 | OPEN_AI_DALL("dall", "dall"), |
| 24 | - MIDJOURNEY("Ollama", "Ollama"), | |
| 24 | + MIDJOURNEY("midjourney", "midjourney"), | |
| 25 | 25 | |
| 26 | 26 | ; |
| 27 | 27 | ... | ... |