Commit 802dee2fc353f41d16fb4c7176f7ededaf6f0fa2
1 parent
3a10fedd
【新增】AI:发送消息时,增加上下文
Showing
3 changed files
with
72 additions
and
19 deletions
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendReqVO.java
| @@ -19,4 +19,7 @@ public class AiChatMessageSendReqVO { | @@ -19,4 +19,7 @@ public class AiChatMessageSendReqVO { | ||
| 19 | @NotEmpty(message = "聊天内容不能为空") | 19 | @NotEmpty(message = "聊天内容不能为空") |
| 20 | private String content; | 20 | private String content; |
| 21 | 21 | ||
| 22 | + @Schema(description = "是否携带上下文", example = "true") | ||
| 23 | + private Boolean useContext; | ||
| 24 | + | ||
| 22 | } | 25 | } |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java
| 1 | package cn.iocoder.yudao.module.ai.dal.dataobject.chat; | 1 | package cn.iocoder.yudao.module.ai.dal.dataobject.chat; |
| 2 | 2 | ||
| 3 | +import com.baomidou.mybatisplus.annotation.TableId; | ||
| 3 | import org.springframework.ai.chat.messages.MessageType; | 4 | import org.springframework.ai.chat.messages.MessageType; |
| 4 | import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; | 5 | import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; |
| 5 | import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; | 6 | import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; |
| @@ -27,14 +28,23 @@ public class AiChatMessageDO extends BaseDO { | @@ -27,14 +28,23 @@ public class AiChatMessageDO extends BaseDO { | ||
| 27 | /** | 28 | /** |
| 28 | * 编号,作为每条聊天记录的唯一标识符 | 29 | * 编号,作为每条聊天记录的唯一标识符 |
| 29 | */ | 30 | */ |
| 31 | + @TableId | ||
| 30 | private Long id; | 32 | private Long id; |
| 31 | 33 | ||
| 32 | /** | 34 | /** |
| 33 | * 会话编号 | 35 | * 会话编号 |
| 34 | * | 36 | * |
| 35 | - * 关联 {@link AiChatConversationDO#getId()} | 37 | + * 关联 {@link AiChatConversationDO#getId()} 字段 |
| 36 | */ | 38 | */ |
| 37 | private Long conversationId; | 39 | private Long conversationId; |
| 40 | + /** | ||
| 41 | + * 回复消息编号 | ||
| 42 | + * | ||
| 43 | + * 关联 {@link #id} 字段 | ||
| 44 | + * | ||
| 45 | + * 大模型回复的消息编号,用于“问答”的关联 | ||
| 46 | + */ | ||
| 47 | + private Long replyId; | ||
| 38 | 48 | ||
| 39 | /** | 49 | /** |
| 40 | * 消息类型 | 50 | * 消息类型 |
| @@ -75,6 +85,9 @@ public class AiChatMessageDO extends BaseDO { | @@ -75,6 +85,9 @@ public class AiChatMessageDO extends BaseDO { | ||
| 75 | */ | 85 | */ |
| 76 | private String content; | 86 | private String content; |
| 77 | 87 | ||
| 78 | - // TODO 芋艿:是否作为上下文语料?use_context,待定 | 88 | + /** |
| 89 | + * 是否携带上下文 | ||
| 90 | + */ | ||
| 91 | + private Boolean useContext; | ||
| 79 | 92 | ||
| 80 | } | 93 | } |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/impl/AiChatServiceImpl.java
| 1 | package cn.iocoder.yudao.module.ai.service.impl; | 1 | package cn.iocoder.yudao.module.ai.service.impl; |
| 2 | 2 | ||
| 3 | +import cn.hutool.core.collection.CollUtil; | ||
| 4 | +import cn.hutool.core.collection.ListUtil; | ||
| 5 | +import cn.hutool.core.util.ArrayUtil; | ||
| 6 | +import cn.hutool.core.util.BooleanUtil; | ||
| 3 | import cn.hutool.core.util.ObjUtil; | 7 | import cn.hutool.core.util.ObjUtil; |
| 4 | import cn.hutool.core.util.StrUtil; | 8 | import cn.hutool.core.util.StrUtil; |
| 5 | import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; | 9 | import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; |
| @@ -109,15 +113,14 @@ public class AiChatServiceImpl implements AiChatService { | @@ -109,15 +113,14 @@ public class AiChatServiceImpl implements AiChatService { | ||
| 109 | StreamingChatClient chatClient = chatClientFactory.getStreamingChatClient(platform); | 113 | StreamingChatClient chatClient = chatClientFactory.getStreamingChatClient(platform); |
| 110 | 114 | ||
| 111 | // 2. 插入 user 发送消息 | 115 | // 2. 插入 user 发送消息 |
| 112 | - AiChatMessageDO userMessage = createChatMessage(conversation.getId(), model, | ||
| 113 | - userId, conversation.getRoleId(), MessageType.USER, sendReqVO.getContent()); | 116 | + AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model, |
| 117 | + userId, conversation.getRoleId(), MessageType.USER, sendReqVO.getContent(), sendReqVO.getUseContext()); | ||
| 114 | 118 | ||
| 115 | // 3.1 插入 assistant 接收消息 | 119 | // 3.1 插入 assistant 接收消息 |
| 116 | - AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), model, | ||
| 117 | - userId, conversation.getRoleId(), MessageType.ASSISTANT, ""); | 120 | + AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model, |
| 121 | + userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext()); | ||
| 118 | 122 | ||
| 119 | // 3.2 创建 chat 需要的 Prompt | 123 | // 3.2 创建 chat 需要的 Prompt |
| 120 | - // TODO 消息上下文 | ||
| 121 | Prompt prompt = buildPrompt(conversation, historyMessages, sendReqVO); | 124 | Prompt prompt = buildPrompt(conversation, historyMessages, sendReqVO); |
| 122 | Flux<ChatResponse> streamResponse = chatClient.stream(prompt); | 125 | Flux<ChatResponse> streamResponse = chatClient.stream(prompt); |
| 123 | 126 | ||
| @@ -139,32 +142,66 @@ public class AiChatServiceImpl implements AiChatService { | @@ -139,32 +142,66 @@ public class AiChatServiceImpl implements AiChatService { | ||
| 139 | } | 142 | } |
| 140 | 143 | ||
| 141 | private Prompt buildPrompt(AiChatConversationDO conversation, List<AiChatMessageDO> messages, AiChatMessageSendReqVO sendReqVO) { | 144 | private Prompt buildPrompt(AiChatConversationDO conversation, List<AiChatMessageDO> messages, AiChatMessageSendReqVO sendReqVO) { |
| 142 | - // TODO 芋艿:1)保留 n 个上下文;2)每一轮 token 数量 | ||
| 143 | -// if (conversation.getMaxContexts() != null && messages.size() > conversation.getMaxContexts()) { | ||
| 144 | -// | ||
| 145 | -// } | ||
| 146 | // 1. 构建 Prompt Message 列表 | 145 | // 1. 构建 Prompt Message 列表 |
| 147 | List<Message> chatMessages = new ArrayList<>(); | 146 | List<Message> chatMessages = new ArrayList<>(); |
| 148 | // 1.1 system context 角色设定 | 147 | // 1.1 system context 角色设定 |
| 149 | chatMessages.add(new SystemMessage(conversation.getSystemMessage())); | 148 | chatMessages.add(new SystemMessage(conversation.getSystemMessage())); |
| 150 | // 1.2 history message 历史消息 | 149 | // 1.2 history message 历史消息 |
| 151 | - messages.forEach(message -> chatMessages.add(new ChatMessage(message.getType().toUpperCase(), message.getContent()))); | 150 | + List<AiChatMessageDO> contextMessages = filterContextMessages(messages, conversation, sendReqVO); |
| 151 | + contextMessages.forEach(message -> chatMessages.add(new ChatMessage(message.getType().toUpperCase(), message.getContent()))); | ||
| 152 | // 1.3 user message 新发送消息 | 152 | // 1.3 user message 新发送消息 |
| 153 | chatMessages.add(new UserMessage(sendReqVO.getContent())); | 153 | chatMessages.add(new UserMessage(sendReqVO.getContent())); |
| 154 | 154 | ||
| 155 | // 2. 构建 ChatOptions 对象 TODO 芋艿:临时注释掉;等文心一言兼容了; | 155 | // 2. 构建 ChatOptions 对象 TODO 芋艿:临时注释掉;等文心一言兼容了; |
| 156 | + // TODO 每一轮 token 数量 | ||
| 156 | // ChatOptions chatOptions = ChatOptionsBuilder.builder().withTemperature(conversation.getTemperature().floatValue()).build(); | 157 | // ChatOptions chatOptions = ChatOptionsBuilder.builder().withTemperature(conversation.getTemperature().floatValue()).build(); |
| 157 | // return new Prompt(chatMessages, null); | 158 | // return new Prompt(chatMessages, null); |
| 158 | return new Prompt(chatMessages); | 159 | return new Prompt(chatMessages); |
| 159 | } | 160 | } |
| 160 | 161 | ||
| 161 | - private AiChatMessageDO createChatMessage(Long conversationId, AiChatModelDO model, | ||
| 162 | - Long userId, Long roleId, | ||
| 163 | - MessageType messageType, String content) { | ||
| 164 | - AiChatMessageDO message = new AiChatMessageDO() | ||
| 165 | - .setConversationId(conversationId).setModel(model.getModel()).setModelId(model.getId()) | ||
| 166 | - .setUserId(userId).setRoleId(roleId) | ||
| 167 | - .setType(messageType.getValue()).setContent(content); | 162 | + /** |
| 163 | + * 从历史消息中,获得倒序的 n 组消息作为消息上下文 | ||
| 164 | + * | ||
| 165 | + * n 组:指的是 user + assistant 形成一组 | ||
| 166 | + * | ||
| 167 | + * @param messages 消息列表 | ||
| 168 | + * @param conversation 会话 | ||
| 169 | + * @param sendReqVO 发送请求 | ||
| 170 | + * @return 消息上下文 | ||
| 171 | + */ | ||
| 172 | + private List<AiChatMessageDO> filterContextMessages(List<AiChatMessageDO> messages, AiChatConversationDO conversation, AiChatMessageSendReqVO sendReqVO) { | ||
| 173 | + if (conversation.getMaxContexts() == null || ObjUtil.notEqual(sendReqVO.getUseContext(), Boolean.TRUE)) { | ||
| 174 | + return Collections.emptyList(); | ||
| 175 | + } | ||
| 176 | + List<AiChatMessageDO> contextMessages = new ArrayList<>(conversation.getMaxContexts() * 2); | ||
| 177 | + for (int i = messages.size() - 1; i >= 0; i--) { | ||
| 178 | + AiChatMessageDO assistantMessage = CollUtil.get(messages, i); | ||
| 179 | + if (assistantMessage == null || assistantMessage.getReplyId() == null) { | ||
| 180 | + continue; | ||
| 181 | + } | ||
| 182 | + AiChatMessageDO userMessage = CollUtil.get(messages, i - 1); | ||
| 183 | + if (userMessage == null || ObjUtil.notEqual(assistantMessage.getReplyId(), userMessage.getId()) | ||
| 184 | + || StrUtil.isEmpty(assistantMessage.getContent())) { | ||
| 185 | + continue; | ||
| 186 | + } | ||
| 187 | + // 由于后续要 reverse 反转,所以先添加 assistantMessage | ||
| 188 | + contextMessages.add(assistantMessage); | ||
| 189 | + contextMessages.add(userMessage); | ||
| 190 | + // 超过最大上下文,结束 | ||
| 191 | + if (contextMessages.size() >= conversation.getMaxContexts() * 2) { | ||
| 192 | + break; | ||
| 193 | + } | ||
| 194 | + } | ||
| 195 | + Collections.reverse(contextMessages); | ||
| 196 | + return contextMessages; | ||
| 197 | + } | ||
| 198 | + | ||
| 199 | + private AiChatMessageDO createChatMessage(Long conversationId, Long replyId, | ||
| 200 | + AiChatModelDO model, Long userId, Long roleId, | ||
| 201 | + MessageType messageType, String content, Boolean useContext) { | ||
| 202 | + AiChatMessageDO message = new AiChatMessageDO().setConversationId(conversationId).setReplyId(replyId) | ||
| 203 | + .setModel(model.getModel()).setModelId(model.getId()).setUserId(userId).setRoleId(roleId) | ||
| 204 | + .setType(messageType.getValue()).setContent(content).setUseContext(useContext); | ||
| 168 | message.setCreateTime(LocalDateTime.now()); | 205 | message.setCreateTime(LocalDateTime.now()); |
| 169 | chatMessageMapper.insert(message); | 206 | chatMessageMapper.insert(message); |
| 170 | return message; | 207 | return message; |