Commit 9b2f2f581b01361093c543114365ba16c0816a16
1 parent
ca34d165
feat:【ai 大模型】增加联网搜索功能
Showing
5 changed files
with
104 additions
and
21 deletions
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.http
| @@ -39,14 +39,27 @@ Authorization: {{token}} | @@ -39,14 +39,27 @@ Authorization: {{token}} | ||
| 39 | tenant-id: {{adminTenantId}} | 39 | tenant-id: {{adminTenantId}} |
| 40 | 40 | ||
| 41 | { | 41 | { |
| 42 | - "conversationId": "1781604279872581797", | 42 | + "conversationId": "1781604279872581799", |
| 43 | "content": "说下图片里,有哪些字?", | 43 | "content": "说下图片里,有哪些字?", |
| 44 | "useContext": true | 44 | "useContext": true |
| 45 | } | 45 | } |
| 46 | 46 | ||
| 47 | +### 发送消息(流式)【联网搜索】 | ||
| 48 | +POST {{baseUrl}}/ai/chat/message/send-stream | ||
| 49 | +Content-Type: application/json | ||
| 50 | +Authorization: {{token}} | ||
| 51 | +tenant-id: {{adminTenantId}} | ||
| 52 | + | ||
| 53 | +{ | ||
| 54 | + "conversationId": "1781604279872581799", | ||
| 55 | + "content": "今天是周几?", | ||
| 56 | + "useSearch": true | ||
| 57 | +} | ||
| 58 | + | ||
| 47 | ### 获得指定对话的消息列表 | 59 | ### 获得指定对话的消息列表 |
| 48 | -GET {{baseUrl}}/ai/chat/message/list-by-conversation-id?conversationId=1781604279872581649 | 60 | +GET {{baseUrl}}/ai/chat/message/list-by-conversation-id?conversationId=1781604279872581799 |
| 49 | Authorization: {{token}} | 61 | Authorization: {{token}} |
| 62 | +tenant-id: {{adminTenantId}} | ||
| 50 | 63 | ||
| 51 | ### 删除消息 | 64 | ### 删除消息 |
| 52 | DELETE {{baseUrl}}/ai/chat/message/delete?id=50 | 65 | DELETE {{baseUrl}}/ai/chat/message/delete?id=50 |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageRespVO.java
| 1 | package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message; | 1 | package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message; |
| 2 | 2 | ||
| 3 | +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchResponse; | ||
| 3 | import io.swagger.v3.oas.annotations.media.Schema; | 4 | import io.swagger.v3.oas.annotations.media.Schema; |
| 4 | import lombok.Data; | 5 | import lombok.Data; |
| 5 | 6 | ||
| @@ -49,6 +50,9 @@ public class AiChatMessageRespVO { | @@ -49,6 +50,9 @@ public class AiChatMessageRespVO { | ||
| 49 | @Schema(description = "知识库段落数组") | 50 | @Schema(description = "知识库段落数组") |
| 50 | private List<KnowledgeSegment> segments; | 51 | private List<KnowledgeSegment> segments; |
| 51 | 52 | ||
| 53 | + @Schema(description = "联网搜索的网页内容数组") | ||
| 54 | + private List<AiWebSearchResponse.WebPage> webSearchPages; | ||
| 55 | + | ||
| 52 | @Schema(description = "附件 URL 数组", example = "https://www.iocoder.cn/1.png") | 56 | @Schema(description = "附件 URL 数组", example = "https://www.iocoder.cn/1.png") |
| 53 | private List<String> attachmentUrls; | 57 | private List<String> attachmentUrls; |
| 54 | 58 |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendRespVO.java
| 1 | package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message; | 1 | package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message; |
| 2 | 2 | ||
| 3 | +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchResponse; | ||
| 3 | import io.swagger.v3.oas.annotations.media.Schema; | 4 | import io.swagger.v3.oas.annotations.media.Schema; |
| 4 | import lombok.Data; | 5 | import lombok.Data; |
| 5 | 6 | ||
| @@ -38,6 +39,9 @@ public class AiChatMessageSendRespVO { | @@ -38,6 +39,9 @@ public class AiChatMessageSendRespVO { | ||
| 38 | @Schema(description = "知识库段落数组") | 39 | @Schema(description = "知识库段落数组") |
| 39 | private List<AiChatMessageRespVO.KnowledgeSegment> segments; | 40 | private List<AiChatMessageRespVO.KnowledgeSegment> segments; |
| 40 | 41 | ||
| 42 | + @Schema(description = "联网搜索的网页内容数组") | ||
| 43 | + private List<AiWebSearchResponse.WebPage> webSearchPages; | ||
| 44 | + | ||
| 41 | @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) | 45 | @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) |
| 42 | private LocalDateTime createTime; | 46 | private LocalDateTime createTime; |
| 43 | 47 |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java
| @@ -6,11 +6,16 @@ import cn.iocoder.yudao.framework.mybatis.core.type.StringListTypeHandler; | @@ -6,11 +6,16 @@ import cn.iocoder.yudao.framework.mybatis.core.type.StringListTypeHandler; | ||
| 6 | import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; | 6 | import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; |
| 7 | import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; | 7 | import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; |
| 8 | import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; | 8 | import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; |
| 9 | +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchResponse; | ||
| 9 | import com.baomidou.mybatisplus.annotation.KeySequence; | 10 | import com.baomidou.mybatisplus.annotation.KeySequence; |
| 10 | import com.baomidou.mybatisplus.annotation.TableField; | 11 | import com.baomidou.mybatisplus.annotation.TableField; |
| 11 | import com.baomidou.mybatisplus.annotation.TableId; | 12 | import com.baomidou.mybatisplus.annotation.TableId; |
| 12 | import com.baomidou.mybatisplus.annotation.TableName; | 13 | import com.baomidou.mybatisplus.annotation.TableName; |
| 13 | -import lombok.*; | 14 | +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; |
| 15 | +import lombok.AllArgsConstructor; | ||
| 16 | +import lombok.Builder; | ||
| 17 | +import lombok.Data; | ||
| 18 | +import lombok.NoArgsConstructor; | ||
| 14 | import org.springframework.ai.chat.messages.MessageType; | 19 | import org.springframework.ai.chat.messages.MessageType; |
| 15 | 20 | ||
| 16 | import java.util.List; | 21 | import java.util.List; |
| @@ -107,6 +112,12 @@ public class AiChatMessageDO extends BaseDO { | @@ -107,6 +112,12 @@ public class AiChatMessageDO extends BaseDO { | ||
| 107 | private List<Long> segmentIds; | 112 | private List<Long> segmentIds; |
| 108 | 113 | ||
| 109 | /** | 114 | /** |
| 115 | + * 联网搜索的网页内容数组 | ||
| 116 | + */ | ||
| 117 | + @TableField(typeHandler = JacksonTypeHandler.class) | ||
| 118 | + private List<AiWebSearchResponse.WebPage> webSearchPages; | ||
| 119 | + | ||
| 120 | + /** | ||
| 110 | * 附件 URL 数组 | 121 | * 附件 URL 数组 |
| 111 | */ | 122 | */ |
| 112 | @TableField(typeHandler = StringListTypeHandler.class) | 123 | @TableField(typeHandler = StringListTypeHandler.class) |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java
| @@ -23,6 +23,9 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiToolDO; | @@ -23,6 +23,9 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiToolDO; | ||
| 23 | import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatMessageMapper; | 23 | import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatMessageMapper; |
| 24 | import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants; | 24 | import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants; |
| 25 | import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum; | 25 | import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum; |
| 26 | +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchClient; | ||
| 27 | +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchRequest; | ||
| 28 | +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchResponse; | ||
| 26 | import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeDocumentService; | 29 | import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeDocumentService; |
| 27 | import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeSegmentService; | 30 | import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeSegmentService; |
| 28 | import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchReqBO; | 31 | import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchReqBO; |
| @@ -44,6 +47,7 @@ import org.springframework.ai.chat.model.ChatResponse; | @@ -44,6 +47,7 @@ import org.springframework.ai.chat.model.ChatResponse; | ||
| 44 | import org.springframework.ai.chat.model.StreamingChatModel; | 47 | import org.springframework.ai.chat.model.StreamingChatModel; |
| 45 | import org.springframework.ai.chat.prompt.ChatOptions; | 48 | import org.springframework.ai.chat.prompt.ChatOptions; |
| 46 | import org.springframework.ai.chat.prompt.Prompt; | 49 | import org.springframework.ai.chat.prompt.Prompt; |
| 50 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 47 | import org.springframework.stereotype.Service; | 51 | import org.springframework.stereotype.Service; |
| 48 | import org.springframework.transaction.annotation.Transactional; | 52 | import org.springframework.transaction.annotation.Transactional; |
| 49 | import reactor.core.publisher.Flux; | 53 | import reactor.core.publisher.Flux; |
| @@ -69,6 +73,11 @@ import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.CHAT_MESSAGE_N | @@ -69,6 +73,11 @@ import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.CHAT_MESSAGE_N | ||
| 69 | @Slf4j | 73 | @Slf4j |
| 70 | public class AiChatMessageServiceImpl implements AiChatMessageService { | 74 | public class AiChatMessageServiceImpl implements AiChatMessageService { |
| 71 | 75 | ||
| 76 | + /** | ||
| 77 | + * 联网搜索的结束数 | ||
| 78 | + */ | ||
| 79 | + private static final Integer WEB_SEARCH_COUNT = 10; | ||
| 80 | + | ||
| 72 | // TODO @芋艿:后续优化下对话的 Prompt 整体结构 | 81 | // TODO @芋艿:后续优化下对话的 Prompt 整体结构 |
| 73 | 82 | ||
| 74 | /** | 83 | /** |
| @@ -78,6 +87,10 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | @@ -78,6 +87,10 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | ||
| 78 | "%s\n\n" + // 多个 <Reference></Reference> 的拼接 | 87 | "%s\n\n" + // 多个 <Reference></Reference> 的拼接 |
| 79 | "回答要求:\n- 避免提及你是从 <Reference></Reference> 获取的知识。"; | 88 | "回答要求:\n- 避免提及你是从 <Reference></Reference> 获取的知识。"; |
| 80 | 89 | ||
| 90 | + private static final String WEB_SEARCH_USER_MESSAGE_TEMPLATE = "使用 <WebSearch></WebSearch> 标记中的内容作为本次对话的参考:\n\n" + | ||
| 91 | + "%s\n\n" + // 多个 <WebSearch></WebSearch> 的拼接 | ||
| 92 | + "回答要求:\n- 避免提及你是从 <WebSearch></WebSearch> 获取的知识。"; | ||
| 93 | + | ||
| 81 | /** | 94 | /** |
| 82 | * 附件转 ${@link UserMessage} 的内容模版 | 95 | * 附件转 ${@link UserMessage} 的内容模版 |
| 83 | */ | 96 | */ |
| @@ -102,6 +115,10 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | @@ -102,6 +115,10 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | ||
| 102 | @Resource | 115 | @Resource |
| 103 | private AiToolService toolService; | 116 | private AiToolService toolService; |
| 104 | 117 | ||
| 118 | + @SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection") | ||
| 119 | + @Autowired(required = false) // 由于 yudao.ai.web-search.enable 配置项,可以关闭 AiWebSearchClient 的功能,所以这里只能不强制注入 | ||
| 120 | + private AiWebSearchClient webSearchClient; | ||
| 121 | + | ||
| 105 | @Transactional(rollbackFor = Exception.class) | 122 | @Transactional(rollbackFor = Exception.class) |
| 106 | public AiChatMessageSendRespVO sendMessage(AiChatMessageSendReqVO sendReqVO, Long userId) { | 123 | public AiChatMessageSendRespVO sendMessage(AiChatMessageSendReqVO sendReqVO, Long userId) { |
| 107 | // 1.1 校验对话存在 | 124 | // 1.1 校验对话存在 |
| @@ -115,30 +132,35 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | @@ -115,30 +132,35 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | ||
| 115 | AiModelDO model = modalService.validateModel(conversation.getModelId()); | 132 | AiModelDO model = modalService.validateModel(conversation.getModelId()); |
| 116 | ChatModel chatModel = modalService.getChatModel(model.getId()); | 133 | ChatModel chatModel = modalService.getChatModel(model.getId()); |
| 117 | 134 | ||
| 118 | - // 2. 知识库找回 | 135 | + // 2.1 知识库召回 |
| 119 | List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments = recallKnowledgeSegment( | 136 | List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments = recallKnowledgeSegment( |
| 120 | sendReqVO.getContent(), conversation); | 137 | sendReqVO.getContent(), conversation); |
| 121 | 138 | ||
| 139 | + // 2.2 联网搜索 | ||
| 140 | + AiWebSearchResponse webSearchResponse = Boolean.TRUE.equals(sendReqVO.getUseSearch()) && webSearchClient != null ? | ||
| 141 | + webSearchClient.search(new AiWebSearchRequest().setQuery(sendReqVO.getContent()) | ||
| 142 | + .setSummary(true).setCount(WEB_SEARCH_COUNT)) : null; | ||
| 143 | + | ||
| 122 | // 3. 插入 user 发送消息 | 144 | // 3. 插入 user 发送消息 |
| 123 | AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model, | 145 | AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model, |
| 124 | userId, conversation.getRoleId(), MessageType.USER, sendReqVO.getContent(), sendReqVO.getUseContext(), | 146 | userId, conversation.getRoleId(), MessageType.USER, sendReqVO.getContent(), sendReqVO.getUseContext(), |
| 125 | - null, sendReqVO.getAttachmentUrls()); | 147 | + null, sendReqVO.getAttachmentUrls(), null); |
| 126 | 148 | ||
| 127 | - // 3.1 插入 assistant 接收消息 | 149 | + // 4.1 插入 assistant 接收消息 |
| 128 | AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model, | 150 | AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model, |
| 129 | userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext(), | 151 | userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext(), |
| 130 | - knowledgeSegments, null); | 152 | + knowledgeSegments, null, webSearchResponse); |
| 131 | 153 | ||
| 132 | - // 3.2 创建 chat 需要的 Prompt | ||
| 133 | - Prompt prompt = buildPrompt(conversation, historyMessages, knowledgeSegments, model, sendReqVO); | 154 | + // 4.2 创建 chat 需要的 Prompt |
| 155 | + Prompt prompt = buildPrompt(conversation, historyMessages, knowledgeSegments, webSearchResponse, model, sendReqVO); | ||
| 134 | ChatResponse chatResponse = chatModel.call(prompt); | 156 | ChatResponse chatResponse = chatModel.call(prompt); |
| 135 | 157 | ||
| 136 | - // 3.3 更新响应内容 | 158 | + // 4.3 更新响应内容 |
| 137 | String newContent = AiUtils.getChatResponseContent(chatResponse); | 159 | String newContent = AiUtils.getChatResponseContent(chatResponse); |
| 138 | String newReasoningContent = AiUtils.getChatResponseReasoningContent(chatResponse); | 160 | String newReasoningContent = AiUtils.getChatResponseReasoningContent(chatResponse); |
| 139 | chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()) | 161 | chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()) |
| 140 | .setContent(newContent).setReasoningContent(newReasoningContent)); | 162 | .setContent(newContent).setReasoningContent(newReasoningContent)); |
| 141 | - // 3.4 响应结果 | 163 | + // 4.4 响应结果 |
| 142 | Map<Long, AiKnowledgeDocumentDO> documentMap = knowledgeDocumentService.getKnowledgeDocumentMap( | 164 | Map<Long, AiKnowledgeDocumentDO> documentMap = knowledgeDocumentService.getKnowledgeDocumentMap( |
| 143 | convertSet(knowledgeSegments, AiKnowledgeSegmentSearchRespBO::getDocumentId)); | 165 | convertSet(knowledgeSegments, AiKnowledgeSegmentSearchRespBO::getDocumentId)); |
| 144 | List<AiChatMessageRespVO.KnowledgeSegment> segments = BeanUtils.toBean(knowledgeSegments, | 166 | List<AiChatMessageRespVO.KnowledgeSegment> segments = BeanUtils.toBean(knowledgeSegments, |
| @@ -149,7 +171,8 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | @@ -149,7 +171,8 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | ||
| 149 | return new AiChatMessageSendRespVO() | 171 | return new AiChatMessageSendRespVO() |
| 150 | .setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class)) | 172 | .setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class)) |
| 151 | .setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class) | 173 | .setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class) |
| 152 | - .setContent(newContent).setSegments(segments)); | 174 | + .setContent(newContent).setSegments(segments) |
| 175 | + .setWebSearchPages(webSearchResponse != null ? webSearchResponse.getLists() : null)); | ||
| 153 | } | 176 | } |
| 154 | 177 | ||
| 155 | @Override | 178 | @Override |
| @@ -166,30 +189,36 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | @@ -166,30 +189,36 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | ||
| 166 | AiModelDO model = modalService.validateModel(conversation.getModelId()); | 189 | AiModelDO model = modalService.validateModel(conversation.getModelId()); |
| 167 | StreamingChatModel chatModel = modalService.getChatModel(model.getId()); | 190 | StreamingChatModel chatModel = modalService.getChatModel(model.getId()); |
| 168 | 191 | ||
| 169 | - // 2. 知识库找回 | 192 | + // 2.1 知识库找回 |
| 170 | List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments = recallKnowledgeSegment( | 193 | List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments = recallKnowledgeSegment( |
| 171 | sendReqVO.getContent(), conversation); | 194 | sendReqVO.getContent(), conversation); |
| 172 | 195 | ||
| 196 | + // 2.2 联网搜索 | ||
| 197 | + AiWebSearchResponse webSearchResponse = Boolean.TRUE.equals(sendReqVO.getUseSearch()) && webSearchClient != null ? | ||
| 198 | + webSearchClient.search(new AiWebSearchRequest().setQuery(sendReqVO.getContent()) | ||
| 199 | + .setSummary(true).setCount(WEB_SEARCH_COUNT)) : null; | ||
| 200 | + | ||
| 173 | // 3. 插入 user 发送消息 | 201 | // 3. 插入 user 发送消息 |
| 174 | AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model, | 202 | AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model, |
| 175 | userId, conversation.getRoleId(), MessageType.USER, sendReqVO.getContent(), sendReqVO.getUseContext(), | 203 | userId, conversation.getRoleId(), MessageType.USER, sendReqVO.getContent(), sendReqVO.getUseContext(), |
| 176 | - null, sendReqVO.getAttachmentUrls()); | 204 | + null, sendReqVO.getAttachmentUrls(), null); |
| 177 | 205 | ||
| 178 | // 4.1 插入 assistant 接收消息 | 206 | // 4.1 插入 assistant 接收消息 |
| 179 | AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model, | 207 | AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model, |
| 180 | userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext(), | 208 | userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext(), |
| 181 | - knowledgeSegments, null); | 209 | + knowledgeSegments, null, webSearchResponse); |
| 182 | 210 | ||
| 183 | // 4.2 构建 Prompt,并进行调用 | 211 | // 4.2 构建 Prompt,并进行调用 |
| 184 | - Prompt prompt = buildPrompt(conversation, historyMessages, knowledgeSegments, model, sendReqVO); | 212 | + Prompt prompt = buildPrompt(conversation, historyMessages, knowledgeSegments, webSearchResponse, model, sendReqVO); |
| 185 | Flux<ChatResponse> streamResponse = chatModel.stream(prompt); | 213 | Flux<ChatResponse> streamResponse = chatModel.stream(prompt); |
| 186 | 214 | ||
| 187 | // 4.3 流式返回 | 215 | // 4.3 流式返回 |
| 188 | StringBuffer contentBuffer = new StringBuffer(); | 216 | StringBuffer contentBuffer = new StringBuffer(); |
| 189 | StringBuffer reasoningContentBuffer = new StringBuffer(); | 217 | StringBuffer reasoningContentBuffer = new StringBuffer(); |
| 190 | return streamResponse.map(chunk -> { | 218 | return streamResponse.map(chunk -> { |
| 191 | - // 处理知识库的返回,只有首次才有 | 219 | + // 仅首次:返回知识库、联网搜索 |
| 192 | List<AiChatMessageRespVO.KnowledgeSegment> segments = null; | 220 | List<AiChatMessageRespVO.KnowledgeSegment> segments = null; |
| 221 | + List<AiWebSearchResponse.WebPage> webSearchPages = null; | ||
| 193 | if (StrUtil.isEmpty(contentBuffer)) { | 222 | if (StrUtil.isEmpty(contentBuffer)) { |
| 194 | Map<Long, AiKnowledgeDocumentDO> documentMap = TenantUtils.executeIgnore(() -> | 223 | Map<Long, AiKnowledgeDocumentDO> documentMap = TenantUtils.executeIgnore(() -> |
| 195 | knowledgeDocumentService.getKnowledgeDocumentMap( | 224 | knowledgeDocumentService.getKnowledgeDocumentMap( |
| @@ -198,6 +227,9 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | @@ -198,6 +227,9 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | ||
| 198 | AiKnowledgeDocumentDO document = documentMap.get(segment.getDocumentId()); | 227 | AiKnowledgeDocumentDO document = documentMap.get(segment.getDocumentId()); |
| 199 | segment.setDocumentName(document != null ? document.getName() : null); | 228 | segment.setDocumentName(document != null ? document.getName() : null); |
| 200 | }); | 229 | }); |
| 230 | + if (webSearchResponse != null) { | ||
| 231 | + webSearchPages = webSearchResponse.getLists(); | ||
| 232 | + } | ||
| 201 | } | 233 | } |
| 202 | // 响应结果 | 234 | // 响应结果 |
| 203 | String newContent = AiUtils.getChatResponseContent(chunk); | 235 | String newContent = AiUtils.getChatResponseContent(chunk); |
| @@ -213,7 +245,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | @@ -213,7 +245,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | ||
| 213 | .setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class) | 245 | .setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class) |
| 214 | .setContent(StrUtil.nullToDefault(newContent, "")) // 避免 null 的 情况 | 246 | .setContent(StrUtil.nullToDefault(newContent, "")) // 避免 null 的 情况 |
| 215 | .setReasoningContent(StrUtil.nullToDefault(newReasoningContent, "")) // 避免 null 的 情况 | 247 | .setReasoningContent(StrUtil.nullToDefault(newReasoningContent, "")) // 避免 null 的 情况 |
| 216 | - .setSegments(segments))); // 知识库返回 | 248 | + .setSegments(segments).setWebSearchPages(webSearchPages))); // 知识库 + 联网搜索 |
| 217 | }).doOnComplete(() -> { | 249 | }).doOnComplete(() -> { |
| 218 | // 忽略租户,因为 Flux 异步无法透传租户 | 250 | // 忽略租户,因为 Flux 异步无法透传租户 |
| 219 | TenantUtils.executeIgnore(() -> chatMessageMapper.updateById( | 251 | TenantUtils.executeIgnore(() -> chatMessageMapper.updateById( |
| @@ -239,7 +271,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | @@ -239,7 +271,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | ||
| 239 | return Collections.emptyList(); | 271 | return Collections.emptyList(); |
| 240 | } | 272 | } |
| 241 | 273 | ||
| 242 | - // 2. 遍历找回 | 274 | + // 2. 遍历召回 |
| 243 | List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments = new ArrayList<>(); | 275 | List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments = new ArrayList<>(); |
| 244 | for (Long knowledgeId : role.getKnowledgeIds()) { | 276 | for (Long knowledgeId : role.getKnowledgeIds()) { |
| 245 | knowledgeSegments.addAll(knowledgeSegmentService.searchKnowledgeSegment(new AiKnowledgeSegmentSearchReqBO() | 277 | knowledgeSegments.addAll(knowledgeSegmentService.searchKnowledgeSegment(new AiKnowledgeSegmentSearchReqBO() |
| @@ -250,6 +282,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | @@ -250,6 +282,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | ||
| 250 | 282 | ||
| 251 | private Prompt buildPrompt(AiChatConversationDO conversation, List<AiChatMessageDO> messages, | 283 | private Prompt buildPrompt(AiChatConversationDO conversation, List<AiChatMessageDO> messages, |
| 252 | List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments, | 284 | List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments, |
| 285 | + AiWebSearchResponse webSearchResponse, | ||
| 253 | AiModelDO model, AiChatMessageSendReqVO sendReqVO) { | 286 | AiModelDO model, AiChatMessageSendReqVO sendReqVO) { |
| 254 | List<Message> chatMessages = new ArrayList<>(); | 287 | List<Message> chatMessages = new ArrayList<>(); |
| 255 | // 1.1 System Context 角色设定 | 288 | // 1.1 System Context 角色设定 |
| @@ -265,6 +298,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | @@ -265,6 +298,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | ||
| 265 | if (attachmentUserMessage != null) { | 298 | if (attachmentUserMessage != null) { |
| 266 | chatMessages.add(attachmentUserMessage); | 299 | chatMessages.add(attachmentUserMessage); |
| 267 | } | 300 | } |
| 301 | + // TODO @芋艿:历史的知识库;历史的搜索,要不要拼接? | ||
| 268 | }); | 302 | }); |
| 269 | 303 | ||
| 270 | // 1.3 当前 user message 新发送消息 | 304 | // 1.3 当前 user message 新发送消息 |
| @@ -278,7 +312,20 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | @@ -278,7 +312,20 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | ||
| 278 | chatMessages.add(new UserMessage(String.format(KNOWLEDGE_USER_MESSAGE_TEMPLATE, reference))); | 312 | chatMessages.add(new UserMessage(String.format(KNOWLEDGE_USER_MESSAGE_TEMPLATE, reference))); |
| 279 | } | 313 | } |
| 280 | 314 | ||
| 281 | - // 1.5 附件,通过 UserMessage 实现 | 315 | + // 1.5 联网搜索,通过 UserMessage 实现 |
| 316 | + if (webSearchResponse != null && CollUtil.isNotEmpty(webSearchResponse.getLists())) { | ||
| 317 | + String webSearch = webSearchResponse.getLists().stream() | ||
| 318 | + .map(page -> { | ||
| 319 | + String summary = StrUtil.isNotEmpty(page.getSummary()) ? | ||
| 320 | + "\nSummary: " + page.getSummary() : ""; | ||
| 321 | + return "<WebSearch title=\"" + page.getTitle() + "\" url=\"" + page.getUrl() + "\">" | ||
| 322 | + + StrUtil.blankToDefault(page.getSummary(), page.getSnippet()) + "</WebSearch>"; | ||
| 323 | + }) | ||
| 324 | + .collect(Collectors.joining("\n\n")); | ||
| 325 | + chatMessages.add(new UserMessage(String.format(WEB_SEARCH_USER_MESSAGE_TEMPLATE, webSearch))); | ||
| 326 | + } | ||
| 327 | + | ||
| 328 | + // 1.6 附件,通过 UserMessage 实现 | ||
| 282 | if (CollUtil.isNotEmpty(sendReqVO.getAttachmentUrls())) { | 329 | if (CollUtil.isNotEmpty(sendReqVO.getAttachmentUrls())) { |
| 283 | UserMessage attachmentUserMessage = buildAttachmentUserMessage(sendReqVO.getAttachmentUrls()); | 330 | UserMessage attachmentUserMessage = buildAttachmentUserMessage(sendReqVO.getAttachmentUrls()); |
| 284 | if (attachmentUserMessage != null) { | 331 | if (attachmentUserMessage != null) { |
| @@ -383,12 +430,16 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | @@ -383,12 +430,16 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | ||
| 383 | AiModelDO model, Long userId, Long roleId, | 430 | AiModelDO model, Long userId, Long roleId, |
| 384 | MessageType messageType, String content, Boolean useContext, | 431 | MessageType messageType, String content, Boolean useContext, |
| 385 | List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments, | 432 | List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments, |
| 386 | - List<String> attachmentUrls) { | 433 | + List<String> attachmentUrls, |
| 434 | + AiWebSearchResponse webSearchResponse) { | ||
| 387 | AiChatMessageDO message = new AiChatMessageDO().setConversationId(conversationId).setReplyId(replyId) | 435 | AiChatMessageDO message = new AiChatMessageDO().setConversationId(conversationId).setReplyId(replyId) |
| 388 | .setModel(model.getModel()).setModelId(model.getId()).setUserId(userId).setRoleId(roleId) | 436 | .setModel(model.getModel()).setModelId(model.getId()).setUserId(userId).setRoleId(roleId) |
| 389 | .setType(messageType.getValue()).setContent(content).setUseContext(useContext) | 437 | .setType(messageType.getValue()).setContent(content).setUseContext(useContext) |
| 390 | .setSegmentIds(convertList(knowledgeSegments, AiKnowledgeSegmentSearchRespBO::getId)) | 438 | .setSegmentIds(convertList(knowledgeSegments, AiKnowledgeSegmentSearchRespBO::getId)) |
| 391 | .setAttachmentUrls(attachmentUrls); | 439 | .setAttachmentUrls(attachmentUrls); |
| 440 | + if (webSearchResponse != null) { | ||
| 441 | + message.setWebSearchPages(webSearchResponse.getLists()); | ||
| 442 | + } | ||
| 392 | message.setCreateTime(LocalDateTime.now()); | 443 | message.setCreateTime(LocalDateTime.now()); |
| 393 | chatMessageMapper.insert(message); | 444 | chatMessageMapper.insert(message); |
| 394 | return message; | 445 | return message; |