Commit 9b2f2f581b01361093c543114365ba16c0816a16

Authored by YunaiV
1 parent ca34d165

feat:【ai 大模型】增加联网搜索功能

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;