From 511b99fe62545d241bc96bf21420f35489527b66 Mon Sep 17 00:00:00 2001 From: Liuyang <2746366019@qq.com> Date: Wed, 5 Mar 2025 17:11:44 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat(llm):=20=E5=A2=9E=E5=8A=A0=E7=9F=A5?= =?UTF-8?q?=E8=AF=86=E5=BA=93=E6=96=87=E6=A1=A3=E6=9F=A5=E8=AF=A2=E5=92=8C?= =?UTF-8?q?=E6=AE=B5=E8=90=BD=E5=91=BD=E4=B8=AD=E7=8E=87=E7=BB=9F=E8=AE=A1?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 ChatReqVO 中添加 groupId 字段,用于区分不同的对话分组 - 新增 getParagraphHitRate 方法,用于获取段落命中率信息 - 优化 chatStream 方法,增加知识库文档查询逻辑 - 新增 ParagraphHitRateListVO、ParagraphHitRateVO 和 ParagraphHitRateWordVO 类,用于段落命中率统计 --- .../conversation/ConversationController.java | 50 ++-- .../admin/conversation/vo/ChatReqVO.java | 2 + .../vo/ParagraphHitRateListVO.java | 15 ++ .../conversation/vo/ParagraphHitRateVO.java | 26 ++ .../vo/ParagraphHitRateWordVO.java | 18 ++ .../conversation/ConversationService.java | 7 + .../conversation/ConversationServiceImpl.java | 253 +++++++++++++++--- .../module/llm/service/http/vo/ChatReqVO.java | 5 + 8 files changed, 322 insertions(+), 54 deletions(-) create mode 100644 yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/controller/admin/conversation/vo/ParagraphHitRateListVO.java create mode 100644 yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/controller/admin/conversation/vo/ParagraphHitRateVO.java create mode 100644 yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/controller/admin/conversation/vo/ParagraphHitRateWordVO.java diff --git a/yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/controller/admin/conversation/ConversationController.java b/yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/controller/admin/conversation/ConversationController.java index b2561ff56..82c7c446b 100644 --- a/yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/controller/admin/conversation/ConversationController.java +++ b/yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/controller/admin/conversation/ConversationController.java @@ -11,7 +11,6 @@ import cn.iocoder.yudao.module.llm.controller.admin.conversation.vo.*; import cn.iocoder.yudao.module.llm.dal.dataobject.conversation.ConversationDO; import cn.iocoder.yudao.module.llm.service.conversation.ConversationService; import cn.iocoder.yudao.module.llm.service.http.vo.TextToImageReqVo; -import com.alibaba.fastjson.JSON; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -27,8 +26,6 @@ import javax.validation.Valid; import java.io.IOException; import java.util.List; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; @@ -113,26 +110,26 @@ public class ConversationController { public SseEmitter streamChat (@Valid @RequestBody ChatReqVO chatReqVO, HttpServletResponse response) { log.info("收到对话推理请求,请求参数: {}", chatReqVO); SseEmitter emitter = new SseEmitter(120_000L); -// ExecutorService executor = Executors.newSingleThreadExecutor(); -// try { -// executor.execute(() -> { -// try { -// conversationService.chatStream(chatReqVO, emitter, response); -// } catch (Exception e) { -// emitter.completeWithError(e); -// } finally { -// executor.shutdown(); -// } -// }); -// } catch (Exception e) { -// log.error("处理对话推理请求时发生异常", e); -// try { -// emitter.completeWithError(e); -// } catch (Exception ex) { -// log.error("无法完成 SseEmitter 错误处理", ex); -// } -// } -// log.info("返回 SseEmitter 对象,准备进行流式响应"); + // ExecutorService executor = Executors.newSingleThreadExecutor(); + // try { + // executor.execute(() -> { + // try { + // conversationService.chatStream(chatReqVO, emitter, response); + // } catch (Exception e) { + // emitter.completeWithError(e); + // } finally { + // executor.shutdown(); + // } + // }); + // } catch (Exception e) { + // log.error("处理对话推理请求时发生异常", e); + // try { + // emitter.completeWithError(e); + // } catch (Exception ex) { + // log.error("无法完成 SseEmitter 错误处理", ex); + // } + // } + // log.info("返回 SseEmitter 对象,准备进行流式响应"); // 异步处理,避免阻塞主线程 CompletableFuture.runAsync(() -> { try { @@ -150,6 +147,13 @@ public class ConversationController { return emitter; } + @GetMapping("/paragraphHitRate") + @Operation(summary = "获得大模型对话记录分页") + @PreAuthorize("@ss.hasPermission('llm:conversation:query')") + public CommonResult getParagraphHitRate (@Valid String uuid, @Valid String groupId) { + return success(conversationService.getParagraphHitRate(uuid,groupId)); + } + @PostMapping("/text-to-image") @Operation(summary = "文字转图片接口") public CommonResult textToImage (@Valid @RequestBody TextToImageReqVo req) { diff --git a/yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/controller/admin/conversation/vo/ChatReqVO.java b/yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/controller/admin/conversation/vo/ChatReqVO.java index c7f211bf5..ad223a1a2 100644 --- a/yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/controller/admin/conversation/vo/ChatReqVO.java +++ b/yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/controller/admin/conversation/vo/ChatReqVO.java @@ -31,4 +31,6 @@ public class ChatReqVO { private Integer maxTokens; @Schema(description = "随机性temperature") private Double temperature; + @Schema(description = "分组id") + private String groupId; } diff --git a/yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/controller/admin/conversation/vo/ParagraphHitRateListVO.java b/yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/controller/admin/conversation/vo/ParagraphHitRateListVO.java new file mode 100644 index 000000000..95595e67d --- /dev/null +++ b/yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/controller/admin/conversation/vo/ParagraphHitRateListVO.java @@ -0,0 +1,15 @@ +package cn.iocoder.yudao.module.llm.controller.admin.conversation.vo; + +import lombok.Data; + +import java.util.List; + +/** + * @Description 段落命中率列表 + */ +@Data +public class ParagraphHitRateListVO { + private String uuid; + private String groupId; + private List wordList; +} diff --git a/yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/controller/admin/conversation/vo/ParagraphHitRateVO.java b/yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/controller/admin/conversation/vo/ParagraphHitRateVO.java new file mode 100644 index 000000000..7a167db64 --- /dev/null +++ b/yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/controller/admin/conversation/vo/ParagraphHitRateVO.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.llm.controller.admin.conversation.vo; + +import lombok.Data; + +import java.util.List; + +/** + * @Description 段落命中率 + */ +@Data +public class ParagraphHitRateVO { + /** + * 段落 + */ + private String paragraph; + + /** + * 命中率 + */ + private String hitRate; + + /** + * 字数 + */ + private Integer wordCount; +} diff --git a/yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/controller/admin/conversation/vo/ParagraphHitRateWordVO.java b/yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/controller/admin/conversation/vo/ParagraphHitRateWordVO.java new file mode 100644 index 000000000..666f7fd5f --- /dev/null +++ b/yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/controller/admin/conversation/vo/ParagraphHitRateWordVO.java @@ -0,0 +1,18 @@ +package cn.iocoder.yudao.module.llm.controller.admin.conversation.vo; + +import lombok.Data; + +import java.util.List; + +/** + * @Description 段落命中率 + */ +@Data +public class ParagraphHitRateWordVO { + /** + * 文件名称 + */ + private String documentName; + + private List paragraphHitRate; +} diff --git a/yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/service/conversation/ConversationService.java b/yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/service/conversation/ConversationService.java index 9a1397859..3cc229fe7 100644 --- a/yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/service/conversation/ConversationService.java +++ b/yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/service/conversation/ConversationService.java @@ -74,4 +74,11 @@ public interface ConversationService { * @param emitter emitter */ void chatStream (@Valid ChatReqVO chatReqVO, SseEmitter emitter, HttpServletResponse response); + + /** + * 获取段落命中率 + * @param uuid + * @return + */ + ParagraphHitRateListVO getParagraphHitRate (@Valid String uuid,@Valid String groupId); } diff --git a/yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/service/conversation/ConversationServiceImpl.java b/yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/service/conversation/ConversationServiceImpl.java index b4e7e43db..e55cc112a 100644 --- a/yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/service/conversation/ConversationServiceImpl.java +++ b/yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/service/conversation/ConversationServiceImpl.java @@ -3,16 +3,15 @@ package cn.iocoder.yudao.module.llm.service.conversation; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONArray; +import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.http.HttpUtils; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.llm.controller.admin.application.vo.ApplicationRespVO; import cn.iocoder.yudao.module.llm.controller.admin.application.vo.ApplicationSaveReqVO; +import cn.iocoder.yudao.module.llm.controller.admin.conversation.vo.*; import cn.iocoder.yudao.module.llm.controller.admin.conversation.vo.ChatReqVO; -import cn.iocoder.yudao.module.llm.controller.admin.conversation.vo.ChatRespVO; -import cn.iocoder.yudao.module.llm.controller.admin.conversation.vo.ConversationPageReqVO; -import cn.iocoder.yudao.module.llm.controller.admin.conversation.vo.ConversationSaveReqVO; import cn.iocoder.yudao.module.llm.controller.admin.datarefluxdata.vo.DataRefluxDataSaveReqVO; import cn.iocoder.yudao.module.llm.dal.dataobject.basemodel.BaseModelDO; import cn.iocoder.yudao.module.llm.dal.dataobject.conversation.ConversationDO; @@ -30,10 +29,12 @@ import cn.iocoder.yudao.module.llm.service.http.vo.*; import cn.iocoder.yudao.module.llm.service.prompttemplates.PromptTemplatesService; import com.alibaba.excel.util.StringUtils; import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONException; import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; @@ -41,6 +42,8 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import javax.annotation.Resource; import javax.servlet.http.HttpServletResponse; +import java.math.RoundingMode; +import java.text.DecimalFormat; import java.util.*; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; @@ -82,6 +85,12 @@ public class ConversationServiceImpl implements ConversationService { // 聊天会话历史记录缓存时间 private final static Long CHAT_HISTORY_REDIS_EXPIRE_SECONDS = 60 * 60 * 24L; + /** + * 知识库文档缓存Key + */ + private final static String KNOWLEDGE_DOCUMENTS_REDIS_KEY = "llm:knowledge:documents"; + private final static Long KNOWLEDGE_DOCUMENTS_REDIS_EXPIRE_SECONDS = 60 * 60 * 24L; + @Override public Integer createConversation (ConversationSaveReqVO createReqVO) { // 插入 @@ -281,7 +290,7 @@ public class ConversationServiceImpl implements ConversationService { * @param chatReqVO 对话请求对象 * @param emitter SseEmitter 对象,用于流式发送响应 */ - @Override + @Override public void chatStream (ChatReqVO chatReqVO, SseEmitter emitter, HttpServletResponse response) { log.info("开始处理对话请求,请求参数: {}", chatReqVO); // 检查系统提示信息,如果为空则尝试从应用中获取 @@ -304,6 +313,57 @@ public class ConversationServiceImpl implements ConversationService { publicModelChatStream(chatReqVO, emitter); } + /** + * 获取段落命中率 + * + * @param uuid + * @return + */ + @Override + public ParagraphHitRateListVO getParagraphHitRate(String uuid, String groupId) { + String redisKey = String.format("%s:%s", KNOWLEDGE_DOCUMENTS_REDIS_KEY, uuid); + List redisResults = stringRedisTemplate.opsForList().range(redisKey, 0, -1); + log.info("[Redis Query] Key: {} | Results count: {}", redisKey, redisResults != null ? redisResults.size() : 0); + + if (CollectionUtils.isNotEmpty(redisResults)) { + for (String jsonResult : redisResults) { + try { + JSONObject jsonObject = JSONObject.parseObject(jsonResult); + log.info("[Processing] Raw JSON: {}", jsonResult); + + if (!isValidHitRateData(jsonObject, uuid, groupId)) { + continue; + } + + // 类型安全转换(FastJSON特性) + return jsonObject.toJavaObject(ParagraphHitRateListVO.class); + + } catch (JSONException e) { + log.error("[JSON Parse Error] Invalid format: {} | Data: {}", e.getMessage(), jsonResult); + } catch (Exception e) { + log.warn("[Data Validation] Skip invalid record: {} | Reason: {}", jsonResult, e.getMessage()); + } + } + } + return null; + } + + /** + * 验证命中率数据有效性 + */ + private boolean isValidHitRateData(JSONObject jsonObj, String expectedUuid, String expectedGroupId) { + if (!jsonObj.containsKey("uuid") || !jsonObj.containsKey("groupId")) { + log.warn("[Validation] Missing required fields. Existing keys: {}", jsonObj.keySet()); + return false; + } + + String actualUuid = jsonObj.getString("uuid"); + String actualGroupId = jsonObj.getString("groupId"); + + return Objects.equals(expectedUuid, actualUuid) + && Objects.equals(expectedGroupId, actualGroupId); + } + /** * 公共模型聊天流式处理方法 * @@ -313,10 +373,17 @@ public class ConversationServiceImpl implements ConversationService { public void publicModelChatStream (ChatReqVO chatReqVO, SseEmitter emitter) { log.info("开始公共模型聊天流式处理,请求参数: {}", chatReqVO); // 检查 UUID 是否为空,若为空则生成一个 - if (StrUtil.isBlank(chatReqVO.getUuid())) { + String uuid = chatReqVO.getUuid(); + if (StrUtil.isBlank(uuid)) { log.info("UUID 为空,生成新的 UUID"); - chatReqVO.setUuid(UUID.randomUUID().toString()); + uuid = UUID.randomUUID().toString(); + chatReqVO.setUuid(uuid); } + + // 为每一次对话设置ID + String groupId = UUID.randomUUID().toString(); + chatReqVO.setGroupId(groupId); + String model = null; String selfModelUrl = ""; // 根据模型类型获取模型信息 @@ -362,29 +429,9 @@ public class ConversationServiceImpl implements ConversationService { List messages = new ArrayList<>(); - // 如果知识库 ID 不为空,先调用知识库获取相关信息 - StringBuilder knowledgeBase = new StringBuilder(); - if (chatReqVO.getKnowledge() != null && chatReqVO.getKnowledge() != 0) { - log.info("知识库 ID 不为空,开始查询知识库,知识库 ID: {}", chatReqVO.getKnowledge()); - LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); - queryWrapper.eq(KnowledgeDocumentsDO::getKnowledgeBaseId, chatReqVO.getKnowledge()); - List fileList = knowledgeDocumentsMapper.selectList(queryWrapper); - for (KnowledgeDocumentsDO knowledgeDocumentsDO : fileList) { - Long id = knowledgeDocumentsDO.getId(); - KnowledgeRagEmbedQueryVO knowledgeRagEmbedQueryVO = new KnowledgeRagEmbedQueryVO(); - knowledgeRagEmbedQueryVO.setFile_id(id.toString()); - knowledgeRagEmbedQueryVO.setQuery(chatReqVO.getPrompt()); - String result = HttpUtils.post(llmBackendProperties.getEmbedQuery(), null, JSON.toJSONString(knowledgeRagEmbedQueryVO)); - com.alibaba.fastjson.JSONArray jsonArray = JSON.parseArray(result); - if (jsonArray != null && !jsonArray.isEmpty()) { - com.alibaba.fastjson.JSONArray jsonArray1 = (com.alibaba.fastjson.JSONArray) jsonArray.get(0); - JSONObject jsonObject = (JSONObject) jsonArray1.get(0); - knowledgeBase.append(jsonObject.get("page_content")); - } - } - log.info("知识库查询完成,获取到的信息: {}", knowledgeBase.toString()); - } - String mess = chatReqVO.getSystemPrompt() + "" + knowledgeBase.toString() + ""; + StringBuilder knowledgeBase = getKnowledgeBase(chatReqVO); + + String mess = chatReqVO.getSystemPrompt() + knowledgeBase.toString(); // 查询历史记录消息,并将查询出来的知识信息放入到 role = system 的消息中 List messageHistoryList = stringRedisTemplate.opsForList().range(CHAT_HIStORY_REDIS_KEY + ":" + chatReqVO.getUuid(), 0, -1); if (messageHistoryList != null && !messageHistoryList.isEmpty()) { @@ -416,10 +463,10 @@ public class ConversationServiceImpl implements ConversationService { ModelCompletionsReqVO modelCompletionsReqVO = new ModelCompletionsReqVO(); modelCompletionsReqVO.setMessages(messages); modelCompletionsReqVO.setModel(model); - log.info("构建模型补全请求对象,请求参数1: {}", modelCompletionsReqVO); + // log.info("构建模型补全请求对象,请求参数1: {}", modelCompletionsReqVO); // 调用模型服务进行流式处理 - ModelCompletionsRespVO modelCompletionsRespVO = modelService.modelCompletionsStream(selfModelUrl, modelCompletionsReqVO, emitter,chatReqVO.getUuid()); + ModelCompletionsRespVO modelCompletionsRespVO = modelService.modelCompletionsStream(selfModelUrl, modelCompletionsReqVO, emitter, chatReqVO.getUuid(), chatReqVO.getGroupId()); if (modelCompletionsRespVO == null) { throw exception(MODEL_COMPLETIONS_ERROR); } @@ -442,6 +489,150 @@ public class ConversationServiceImpl implements ConversationService { log.info("数据回流信息保存完成"); } + @NotNull + private StringBuilder getKnowledgeBase (ChatReqVO chatReqVO) { + final String LOG_PREFIX = "[KnowledgeBase]"; + StringBuilder knowledgeBase = new StringBuilder(); + + // 参数有效性检查 + if (chatReqVO.getKnowledge() == null || chatReqVO.getKnowledge() == 0L) { + log.info("{} 未启用知识库检索,knowledgeId: {}", LOG_PREFIX, chatReqVO.getKnowledge()); + return knowledgeBase; + } + + log.info("{} 开始知识库检索,knowledgeId: {}", LOG_PREFIX, chatReqVO.getKnowledge()); + // 如果知识库 ID 不为空,先调用知识库获取相关信息 + try { + // 1. 查询知识库文档列表 + List documentList = knowledgeDocumentsMapper.selectList(new LambdaQueryWrapper() + .eq(KnowledgeDocumentsDO::getKnowledgeBaseId, chatReqVO.getKnowledge())); + + log.info("{} 查询到{}个关联文档", LOG_PREFIX, documentList.size()); + + // 解析响应数据 + ParagraphHitRateListVO paragraphHitRateListVO = new ParagraphHitRateListVO(); + paragraphHitRateListVO.setUuid(chatReqVO.getUuid()); + paragraphHitRateListVO.setGroupId(chatReqVO.getGroupId()); + List words = new ArrayList<>(); + // 2. 遍历处理每个文档 + for (KnowledgeDocumentsDO document : documentList) { + ParagraphHitRateWordVO rateWordVO = processDocument(document, chatReqVO, knowledgeBase); + words.add(rateWordVO); + } + paragraphHitRateListVO.setWordList(words); + + // 请求结果添加到 Redis,查询段落命中率 + String redisKey = String.format("%s:%s", KNOWLEDGE_DOCUMENTS_REDIS_KEY, chatReqVO.getUuid()); + stringRedisTemplate.opsForList().rightPushIfPresent(redisKey, JSON.toJSONString(paragraphHitRateListVO)); + + List paragraphHitRateList = stringRedisTemplate.opsForList().range(redisKey, 0, -1); + if (paragraphHitRateList != null && !paragraphHitRateList.isEmpty()) { + log.info("{} 知识库查询段落命中率: {}", "[KnowledgeBase]", paragraphHitRateList); + } + + log.info("{} 知识库构建完成,内容长度: {}", LOG_PREFIX, knowledgeBase.length()); + } catch (Exception e) { + log.error("{} 知识库处理异常: {}", LOG_PREFIX, e.getMessage(), e); + throw new ServiceException(100, "知识库处理失败,请稍后重试"); + } + + + return knowledgeBase; + } + + /** + * 处理单个知识库文档的检索逻辑 + */ + private ParagraphHitRateWordVO processDocument (KnowledgeDocumentsDO document, + ChatReqVO chatReqVO, + StringBuilder knowledgeBase) { + ParagraphHitRateWordVO paragraphHitRateListVO = new ParagraphHitRateWordVO(); + try { + log.info("{} 处理文档: {}[ID:{}]", "[KnowledgeBase]", document.getDocumentName(), document.getId()); + + // 构建查询请求 + KnowledgeRagEmbedQueryVO queryVO = new KnowledgeRagEmbedQueryVO() + .setFile_id(document.getId().toString()) + .setQuery(chatReqVO.getPrompt()); + + // 发送向量检索请求 + long start = System.currentTimeMillis(); + String response = HttpUtils.post( + llmBackendProperties.getEmbedQuery(), + null, + JSON.toJSONString(queryVO) + ); + log.info("{} 文档[{}]检索耗时: {}ms", "[KnowledgeBase]", document.getId(), + System.currentTimeMillis() - start); + log.info("[KnowledgeBase] 知识库请求结果:{}", response); + + paragraphHitRateListVO.setDocumentName(document.getDocumentName()); + paragraphHitRateListVO.setParagraphHitRate(parseEmbeddingResponse(response, knowledgeBase, chatReqVO.getUuid())); + } catch (Exception e) { + log.warn("{} 文档[{}]处理异常: {}", "[KnowledgeBase]", document.getId(), e.getMessage()); + // 单个文档失败不影响整体流程 + } + return paragraphHitRateListVO; + } + + /** + * 解析向量检索响应结果 + */ + private List parseEmbeddingResponse (String response, + StringBuilder knowledgeBase, + String uuid) { + if (StringUtils.isBlank(response)) { + log.warn("{} 收到空响应", "[KnowledgeBase]"); + return Collections.emptyList(); + } + + List paragraphHitRateList = new ArrayList<>(); + try { + com.alibaba.fastjson.JSONArray resultArray = JSON.parseArray(response); + if (resultArray == null || resultArray.isEmpty()) { + log.info("{} 无有效检索结果", "[KnowledgeBase]"); + return Collections.emptyList(); + } + + // 获取第一个结果集 + com.alibaba.fastjson.JSONArray firstResult = resultArray.getJSONArray(0); + if (firstResult.isEmpty()) { + return Collections.emptyList(); + } + + // 提取页面内容 + JSONObject content = firstResult.getJSONObject(0); + String pageContent = content.getString("page_content"); + log.info("{} 内容: {}", "[KnowledgeBase]", JSON.toJSONString(pageContent)); + + JSONObject metadata = content.getJSONObject("metadata"); + String fileId = metadata.getString("file_id"); + + Double rate = firstResult.getDouble(1); + DecimalFormat df = new DecimalFormat("#%"); + // 明确指定四舍五入模式 + df.setRoundingMode(RoundingMode.HALF_UP); + String rateResult = df.format(rate); + log.info("{} 命中率: {}", "[KnowledgeBase]", rateResult); + + if (StringUtils.isNotBlank(pageContent)) { + knowledgeBase.append("\n[知识库内容] ").append(pageContent); + log.info("{} 添加知识内容,长度: {}", "[KnowledgeBase]", pageContent.length()); + + ParagraphHitRateVO paragraphHitRateVO = new ParagraphHitRateVO(); + paragraphHitRateVO.setParagraph(pageContent); + paragraphHitRateVO.setHitRate(rateResult); + paragraphHitRateVO.setWordCount(pageContent.length()); + paragraphHitRateList.add(paragraphHitRateVO); + } + + } catch (JSONException e) { + log.error("{} 响应解析失败: {} | 原始响应: {}", "[KnowledgeBase]", e.getMessage(), response); + throw new RuntimeException("知识库响应解析异常"); + } + + return paragraphHitRateList; + } /** * 私有模型聊天 diff --git a/yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/service/http/vo/ChatReqVO.java b/yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/service/http/vo/ChatReqVO.java index b9ffe88ba..5a182aa84 100644 --- a/yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/service/http/vo/ChatReqVO.java +++ b/yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/service/http/vo/ChatReqVO.java @@ -17,6 +17,11 @@ public class ChatReqVO { */ private String uuid; + /** + * 对话分组id + */ + private String groupId; + /** * 是否结束对话 */ From 35e19396e196c89583132c96a6bc1fbfc4512580 Mon Sep 17 00:00:00 2001 From: Liuyang <2746366019@qq.com> Date: Wed, 5 Mar 2025 17:15:46 +0800 Subject: [PATCH 2/4] =?UTF-8?q?fix(llm):=20=E4=BF=AE=E5=A4=8D=E7=9F=A5?= =?UTF-8?q?=E8=AF=86=E5=BA=93=E9=97=AE=E7=AD=94=E4=B8=AD=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E5=86=85=E5=AE=B9=E6=8D=A2=E8=A1=8C=E7=AC=A6=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在提取页面内容后,添加了替换换行符的代码 - 将 pageContent 中的 \n 和 \r 替换为空字符串,避免在返回结果中显示换行符 --- .../module/llm/service/conversation/ConversationServiceImpl.java | 1 + 1 file changed, 1 insertion(+) diff --git a/yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/service/conversation/ConversationServiceImpl.java b/yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/service/conversation/ConversationServiceImpl.java index e55cc112a..57b019556 100644 --- a/yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/service/conversation/ConversationServiceImpl.java +++ b/yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/service/conversation/ConversationServiceImpl.java @@ -603,6 +603,7 @@ public class ConversationServiceImpl implements ConversationService { // 提取页面内容 JSONObject content = firstResult.getJSONObject(0); String pageContent = content.getString("page_content"); + pageContent = pageContent.replace("\n", "").replace("\r", ""); log.info("{} 内容: {}", "[KnowledgeBase]", JSON.toJSONString(pageContent)); JSONObject metadata = content.getJSONObject("metadata"); From 1138058d7405783d364ed1db223716d8007fce96 Mon Sep 17 00:00:00 2001 From: Liuyang <2746366019@qq.com> Date: Wed, 5 Mar 2025 17:23:41 +0800 Subject: [PATCH 3/4] =?UTF-8?q?feat(llm):=20=E4=B8=BA=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E8=A1=A5=E5=85=A8=E8=AF=B7=E6=B1=82=E6=B7=BB=E5=8A=A0=E5=88=86?= =?UTF-8?q?=E7=BB=84=20ID=20=E5=8F=82=E6=95=B0-=20=E5=9C=A8=20modelComplet?= =?UTF-8?q?ionsStream=20=E6=96=B9=E6=B3=95=E4=B8=AD=E6=B7=BB=E5=8A=A0=20gr?= =?UTF-8?q?oupId=20=E5=8F=82=E6=95=B0=20-=20=E5=B0=86=20groupId=20?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E4=BC=A0=E9=80=92=E7=BB=99=20sendPostRequest?= =?UTF-8?q?=20=E5=92=8C=20handleResponseEntity=20=E6=96=B9=E6=B3=95=20-=20?= =?UTF-8?q?=E5=9C=A8=20parseStreamLine=20=E6=96=B9=E6=B3=95=E4=B8=AD?= =?UTF-8?q?=E4=B8=BA=20ChatReqVO=20=E5=AF=B9=E8=B1=A1=E6=B7=BB=E5=8A=A0=20?= =?UTF-8?q?groupId=20=E5=AD=97=E6=AE=B5=20-=20=E4=BC=98=E5=8C=96=E4=BA=86?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=A0=BC=E5=BC=8F=EF=BC=8C=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E4=BA=86=E9=83=A8=E5=88=86=E7=BC=A9=E8=BF=9B=E5=92=8C=E7=A9=BA?= =?UTF-8?q?=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/llm/service/http/ModelService.java | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/service/http/ModelService.java b/yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/service/http/ModelService.java index 2750ef98c..03900dd15 100644 --- a/yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/service/http/ModelService.java +++ b/yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/service/http/ModelService.java @@ -146,7 +146,7 @@ public class ModelService { * @param url 模型服务的 URL * @param req 模型补全请求对象 */ - public ModelCompletionsRespVO modelCompletionsStream (String url, ModelCompletionsReqVO req, SseEmitter emitter, String uuid) { + public ModelCompletionsRespVO modelCompletionsStream (String url, ModelCompletionsReqVO req, SseEmitter emitter, String uuid, String groupId) { req.setStream(true); req.setTemperature(0.2); req.setTop_p(0.9); @@ -168,11 +168,11 @@ public class ModelService { log.info("使用指定URL: {}", url); } - String answer=""; + String answer = ""; try { String jsonString = JSON.toJSONString(req); log.info("开始处理模型补全请求,参数3: {}", jsonString); - answer= sendPostRequest(url, jsonString, emitter, uuid); + answer = sendPostRequest(url, jsonString, emitter, uuid, groupId); } catch (Exception e) { emitter.completeWithError(e); @@ -225,7 +225,7 @@ public class ModelService { * @param requestBody 请求体内容 * @throws IOException 发送请求或处理响应时可能抛出的 IO 异常 */ - private String sendPostRequest (String apiUrl, String requestBody, SseEmitter emitter, String uuid) throws IOException { + private String sendPostRequest (String apiUrl, String requestBody, SseEmitter emitter, String uuid, String groupId) throws IOException { // 创建 HttpClient 实例 HttpClient httpClient = HttpClients.createDefault(); @@ -239,7 +239,7 @@ public class ModelService { HttpResponse response = httpClient.execute(httpPost); // 处理响应实体 - return handleResponseEntity(response, emitter, uuid); + return handleResponseEntity(response, emitter, uuid, groupId); } /** @@ -269,7 +269,7 @@ public class ModelService { * * @param response HttpResponse 对象 */ - private String handleResponseEntity (HttpResponse response, SseEmitter emitter, String uuid) { + private String handleResponseEntity (HttpResponse response, SseEmitter emitter, String uuid, String groupId) { StringBuilder result = new StringBuilder(); // 获取响应实体 HttpEntity responseEntity = response.getEntity(); @@ -285,7 +285,7 @@ public class ModelService { log.info("接收到的响应行数据: {}", line); line = line.replaceAll("\n", " "); - String content = parseStreamLine(line, uuid); + String content = parseStreamLine(line, uuid, groupId); if (content != null) { emitter.send( SseEmitter.event() @@ -294,15 +294,15 @@ public class ModelService { log.info("已发送数据:{}", content); } ChatReqVO chatReqVO = JSONObject.parseObject(content, ChatReqVO.class); - if (content!=null){ + if (content != null) { result.append(chatReqVO.getContent()); } -// // 心跳检测 -// if (System.currentTimeMillis() - lastSendTime > 15_000) { -// emitter.send(SseEmitter.event().comment("heartbeat")); -// lastSendTime = System.currentTimeMillis(); -// } + // // 心跳检测 + // if (System.currentTimeMillis() - lastSendTime > 15_000) { + // emitter.send(SseEmitter.event().comment("heartbeat")); + // lastSendTime = System.currentTimeMillis(); + // } } emitter.complete(); } catch (IOException e) { @@ -318,7 +318,7 @@ public class ModelService { * @param line 流式响应中的单行JSON数据 * @return 处理后的文本内容(若无有效内容返回null) */ - private String parseStreamLine (String line, String uuid) { + private String parseStreamLine (String line, String uuid, String groupId) { if (StringUtils.isNotBlank(line)) { if (line.startsWith("data: ")) { String dataString = extractJsonFromDataString(line); @@ -337,6 +337,7 @@ public class ModelService { content = content.replaceAll("\n", " "); ChatReqVO chatReqVO = new ChatReqVO(); chatReqVO.setUuid(""); + chatReqVO.setGroupId(""); chatReqVO.setContent(content); chatReqVO.setFinish_reason(false); return JSON.toJSONString(chatReqVO); @@ -346,6 +347,7 @@ public class ModelService { ChatReqVO chatReqVO = new ChatReqVO(); chatReqVO.setUuid(uuid); + chatReqVO.setGroupId(groupId); chatReqVO.setContent(""); chatReqVO.setFinish_reason(true); return JSON.toJSONString(chatReqVO); From 768c67f898418817eab3a087977abe04ee8040ac Mon Sep 17 00:00:00 2001 From: Liuyang <2746366019@qq.com> Date: Wed, 5 Mar 2025 17:43:20 +0800 Subject: [PATCH 4/4] =?UTF-8?q?refactor(yudao-module-llm):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E7=9F=A5=E8=AF=86=E6=96=87=E6=A1=A3=E5=8C=B9=E9=85=8D?= =?UTF-8?q?=E7=8E=87=E6=9F=A5=E8=AF=A2=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 rightPushIfPresent 方法替换为 rightPush 方法,确保段落匹配率信息总是被添加到 Redis 列表中 - 优化了知识文档匹配率的查询流程,提高了数据的准确性和可靠性 --- .../llm/service/conversation/ConversationServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/service/conversation/ConversationServiceImpl.java b/yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/service/conversation/ConversationServiceImpl.java index 57b019556..0332c1a52 100644 --- a/yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/service/conversation/ConversationServiceImpl.java +++ b/yudao-module-llm/yudao-module-llm-biz/src/main/java/cn/iocoder/yudao/module/llm/service/conversation/ConversationServiceImpl.java @@ -523,7 +523,7 @@ public class ConversationServiceImpl implements ConversationService { // 请求结果添加到 Redis,查询段落命中率 String redisKey = String.format("%s:%s", KNOWLEDGE_DOCUMENTS_REDIS_KEY, chatReqVO.getUuid()); - stringRedisTemplate.opsForList().rightPushIfPresent(redisKey, JSON.toJSONString(paragraphHitRateListVO)); + stringRedisTemplate.opsForList().rightPush(redisKey, JSON.toJSONString(paragraphHitRateListVO)); List paragraphHitRateList = stringRedisTemplate.opsForList().range(redisKey, 0, -1); if (paragraphHitRateList != null && !paragraphHitRateList.isEmpty()) {