Commit c05d7c9f9521ad2b1d1534c66423f8ce4fa9f535

Authored by xiaoxin
1 parent 6b651bae

【新增】AI:对话消息记录召回段落

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;  
4 -import org.springframework.ai.chat.messages.MessageType;  
5 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; 3 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
  4 +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO;
6 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; 5 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
7 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; 6 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
8 import com.baomidou.mybatisplus.annotation.KeySequence; 7 import com.baomidou.mybatisplus.annotation.KeySequence;
  8 +import com.baomidou.mybatisplus.annotation.TableField;
  9 +import com.baomidou.mybatisplus.annotation.TableId;
9 import com.baomidou.mybatisplus.annotation.TableName; 10 import com.baomidou.mybatisplus.annotation.TableName;
  11 +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
10 import lombok.*; 12 import lombok.*;
  13 +import org.springframework.ai.chat.messages.MessageType;
  14 +
  15 +import java.util.List;
11 16
12 /** 17 /**
13 * AI Chat 消息 DO 18 * AI Chat 消息 DO
@@ -66,6 +71,15 @@ public class AiChatMessageDO extends BaseDO { @@ -66,6 +71,15 @@ public class AiChatMessageDO extends BaseDO {
66 */ 71 */
67 private Long roleId; 72 private Long roleId;
68 73
  74 +
  75 + /**
  76 + * 段落编号数组
  77 + *
  78 + * 关联 {@link AiKnowledgeSegmentDO#getId()} 字段
  79 + */
  80 + @TableField(typeHandler = JacksonTypeHandler.class)
  81 + private List<Long> segmentIds;
  82 +
69 /** 83 /**
70 * 模型标志 84 * 模型标志
71 */ 85 */
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java
@@ -90,13 +90,16 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { @@ -90,13 +90,16 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
90 AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model, 90 AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model,
91 userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext()); 91 userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext());
92 92
93 - // 3.2 创建 chat 需要的 Prompt  
94 - Prompt prompt = buildPrompt(conversation, historyMessages, model, sendReqVO); 93 + // 3.2 召回段落
  94 + List<AiKnowledgeSegmentDO> segmentList = recallSegment(sendReqVO.getContent(), conversation.getKnowledgeId());
  95 +
  96 + // 3.3 创建 chat 需要的 Prompt
  97 + Prompt prompt = buildPrompt(conversation, historyMessages, segmentList, model, sendReqVO);
95 ChatResponse chatResponse = chatModel.call(prompt); 98 ChatResponse chatResponse = chatModel.call(prompt);
96 99
97 - // 3.3 段式返回 100 + // 3.4 段式返回
98 String newContent = chatResponse.getResult().getOutput().getContent(); 101 String newContent = chatResponse.getResult().getOutput().getContent();
99 - chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(newContent)); 102 + chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setSegmentIds(convertList(segmentList, AiKnowledgeSegmentDO::getId)).setContent(newContent));
100 return new AiChatMessageSendRespVO().setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class)) 103 return new AiChatMessageSendRespVO().setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class))
101 .setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class).setContent(newContent)); 104 .setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class).setContent(newContent));
102 } 105 }
@@ -121,11 +124,15 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { @@ -121,11 +124,15 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
121 AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model, 124 AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model,
122 userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext()); 125 userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext());
123 126
124 - // 3.2 构建 Prompt,并进行调用  
125 - Prompt prompt = buildPrompt(conversation, historyMessages, model, sendReqVO); 127 +
  128 + // 3.2 召回段落
  129 + List<AiKnowledgeSegmentDO> segmentList = recallSegment(sendReqVO.getContent(), conversation.getKnowledgeId());
  130 +
  131 + // 3.3 构建 Prompt,并进行调用
  132 + Prompt prompt = buildPrompt(conversation, historyMessages, segmentList, model, sendReqVO);
126 Flux<ChatResponse> streamResponse = chatModel.stream(prompt); 133 Flux<ChatResponse> streamResponse = chatModel.stream(prompt);
127 134
128 - // 3.3 流式返回 135 + // 3.4 流式返回
129 // TODO 注意:Schedulers.immediate() 目的是,避免默认 Schedulers.parallel() 并发消费 chunk 导致 SSE 响应前端会乱序问题 136 // TODO 注意:Schedulers.immediate() 目的是,避免默认 Schedulers.parallel() 并发消费 chunk 导致 SSE 响应前端会乱序问题
130 StringBuffer contentBuffer = new StringBuffer(); 137 StringBuffer contentBuffer = new StringBuffer();
131 return streamResponse.map(chunk -> { 138 return streamResponse.map(chunk -> {
@@ -138,7 +145,8 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { @@ -138,7 +145,8 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
138 }).doOnComplete(() -> { 145 }).doOnComplete(() -> {
139 // 忽略租户,因为 Flux 异步无法透传租户 146 // 忽略租户,因为 Flux 异步无法透传租户
140 TenantUtils.executeIgnore(() -> 147 TenantUtils.executeIgnore(() ->
141 - chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(contentBuffer.toString()))); 148 + chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setSegmentIds(convertList(segmentList, AiKnowledgeSegmentDO::getId))
  149 + .setContent(contentBuffer.toString())));
142 }).doOnError(throwable -> { 150 }).doOnError(throwable -> {
143 log.error("[sendChatMessageStream][userId({}) sendReqVO({}) 发生异常]", userId, sendReqVO, throwable); 151 log.error("[sendChatMessageStream][userId({}) sendReqVO({}) 发生异常]", userId, sendReqVO, throwable);
144 // 忽略租户,因为 Flux 异步无法透传租户 152 // 忽略租户,因为 Flux 异步无法透传租户
@@ -147,21 +155,26 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { @@ -147,21 +155,26 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
147 }).onErrorResume(error -> Flux.just(error(ErrorCodeConstants.CHAT_STREAM_ERROR))); 155 }).onErrorResume(error -> Flux.just(error(ErrorCodeConstants.CHAT_STREAM_ERROR)));
148 } 156 }
149 157
150 - private Prompt buildPrompt(AiChatConversationDO conversation, List<AiChatMessageDO> messages, 158 + private List<AiKnowledgeSegmentDO> recallSegment(String content, Long knowledgeId) {
  159 + List<AiKnowledgeSegmentDO> segmentList = new ArrayList<>();
  160 + if (Objects.nonNull(knowledgeId)) {
  161 + segmentList = knowledgeSegmentService.similaritySearch(new AiKnowledgeSegmentSearchReqVO().setKnowledgeId(knowledgeId).setContent(content));
  162 + }
  163 + return segmentList;
  164 + }
  165 +
  166 + private Prompt buildPrompt(AiChatConversationDO conversation, List<AiChatMessageDO> messages,List<AiKnowledgeSegmentDO> segmentList,
151 AiChatModelDO model, AiChatMessageSendReqVO sendReqVO) { 167 AiChatModelDO model, AiChatMessageSendReqVO sendReqVO) {
152 // 1. 构建 Prompt Message 列表 168 // 1. 构建 Prompt Message 列表
153 List<Message> chatMessages = new ArrayList<>(); 169 List<Message> chatMessages = new ArrayList<>();
154 170
155 - // 1.1 知识库召回  
156 - if (Objects.nonNull(conversation.getKnowledgeId())) {  
157 - List<AiKnowledgeSegmentDO> segmentList = knowledgeSegmentService.similaritySearch(new AiKnowledgeSegmentSearchReqVO().setKnowledgeId(conversation.getKnowledgeId()).setContent(sendReqVO.getContent()));  
158 - if (CollUtil.isNotEmpty(segmentList)) {  
159 - PromptTemplate promptTemplate = new PromptTemplate(AiChatRoleEnum.AI_KNOWLEDGE_ROLE.getSystemMessage());  
160 - StringBuilder infoBuilder = StrUtil.builder();  
161 - segmentList.forEach(segment -> infoBuilder.append(System.lineSeparator()).append(segment.getContent()));  
162 - Message message = promptTemplate.createMessage(Map.of("info", infoBuilder.toString()));  
163 - chatMessages.add(message);  
164 - } 171 + // 1.1 召回内容消息构建
  172 + if (CollUtil.isNotEmpty(segmentList)) {
  173 + PromptTemplate promptTemplate = new PromptTemplate(AiChatRoleEnum.AI_KNOWLEDGE_ROLE.getSystemMessage());
  174 + StringBuilder infoBuilder = StrUtil.builder();
  175 + segmentList.forEach(segment -> infoBuilder.append(System.lineSeparator()).append(segment.getContent()));
  176 + Message message = promptTemplate.createMessage(Map.of("info", infoBuilder.toString()));
  177 + chatMessages.add(message);
165 } 178 }
166 179
167 // 1.2 system context 角色设定 180 // 1.2 system context 角色设定