Commit 802dee2fc353f41d16fb4c7176f7ededaf6f0fa2

Authored by YunaiV
1 parent 3a10fedd

【新增】AI:发送消息时,增加上下文

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 19 @NotEmpty(message = "聊天内容不能为空")
20 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 1 package cn.iocoder.yudao.module.ai.dal.dataobject.chat;
2 2  
  3 +import com.baomidou.mybatisplus.annotation.TableId;
3 4 import org.springframework.ai.chat.messages.MessageType;
4 5 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
5 6 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
... ... @@ -27,14 +28,23 @@ public class AiChatMessageDO extends BaseDO {
27 28 /**
28 29 * 编号,作为每条聊天记录的唯一标识符
29 30 */
  31 + @TableId
30 32 private Long id;
31 33  
32 34 /**
33 35 * 会话编号
34 36 *
35   - * 关联 {@link AiChatConversationDO#getId()}
  37 + * 关联 {@link AiChatConversationDO#getId()} 字段
36 38 */
37 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 85 */
76 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 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 7 import cn.hutool.core.util.ObjUtil;
4 8 import cn.hutool.core.util.StrUtil;
5 9 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
... ... @@ -109,15 +113,14 @@ public class AiChatServiceImpl implements AiChatService {
109 113 StreamingChatClient chatClient = chatClientFactory.getStreamingChatClient(platform);
110 114  
111 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 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 123 // 3.2 创建 chat 需要的 Prompt
120   - // TODO 消息上下文
121 124 Prompt prompt = buildPrompt(conversation, historyMessages, sendReqVO);
122 125 Flux<ChatResponse> streamResponse = chatClient.stream(prompt);
123 126  
... ... @@ -139,32 +142,66 @@ public class AiChatServiceImpl implements AiChatService {
139 142 }
140 143  
141 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 145 // 1. 构建 Prompt Message 列表
147 146 List<Message> chatMessages = new ArrayList<>();
148 147 // 1.1 system context 角色设定
149 148 chatMessages.add(new SystemMessage(conversation.getSystemMessage()));
150 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 152 // 1.3 user message 新发送消息
153 153 chatMessages.add(new UserMessage(sendReqVO.getContent()));
154 154  
155 155 // 2. 构建 ChatOptions 对象 TODO 芋艿:临时注释掉;等文心一言兼容了;
  156 + // TODO 每一轮 token 数量
156 157 // ChatOptions chatOptions = ChatOptionsBuilder.builder().withTemperature(conversation.getTemperature().floatValue()).build();
157 158 // return new Prompt(chatMessages, null);
158 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 205 message.setCreateTime(LocalDateTime.now());
169 206 chatMessageMapper.insert(message);
170 207 return message;
... ...