diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java index 52c7953e..f71a8b45 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java @@ -91,6 +91,7 @@ public class AihrSopSeedService { private static final String LOCAL_EMBEDDING_MODEL = "local-hash-v1"; private static final String LOCAL_EMBEDDING_BASE_URL = "local"; private static final int LOCAL_EMBEDDING_DIMENSION = 1536; + private static final String NO_CLEAR_SOP_EVIDENCE = "未在已入库 SOP 中找到明确依据"; private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); private final Map seeds = buildSeeds(); @@ -120,25 +121,19 @@ public class AihrSopSeedService { String category = isBlank(requestedCategory) ? seed.category() : requestedCategory; String position = firstNonBlank(request == null ? null : request.position(), "生活顾问"); String source = normalizeSearchSource(request == null ? null : request.source()); - String queryText = request == null || request.queryText() == null || request.queryText().isBlank() ? seed.question() : request.queryText(); + String queryText = request == null ? "" : Optional.ofNullable(request.queryText()).orElse("").trim(); + if (isBlank(queryText)) { + return withReviewId(noEvidenceResponse("", category), source); + } SearchResponse dbResponse = dbSearch(dbCategory(category), queryText, request == null ? null : request.limit()); - if (dbResponse != null && !dbResponse.snippets().isEmpty()) { + if (dbResponse != null) { + if (isNoEvidenceAnswer(dbResponse.answer())) { + recordKnowledgeGap(queryText, category, position, source); + } return withReviewId(dbResponse, source); } recordKnowledgeGap(queryText, category, position, source); - return withReviewId(new SearchResponse( - queryText, - seed.category(), - seed.answer(), - seed.reference(), - seed.docs(), - seed.snippets(), - seed.training(), - records(), - List.of(), - List.of(), - null - ), source); + return withReviewId(noEvidenceResponse(queryText, category), source); } public SopReviewResponse reviewSearch(Long id, SopReviewRequest request) { @@ -2003,17 +1998,23 @@ public class AihrSopSeedService { docs.putIfAbsent(hit.title(), new DocResponse(hit.title(), hit.description(), "命中 " + scoreText(hit.score()), "pill-success")); } SearchDigest digest = searchDigest(queryText, hits); + if (digest == null) { + return noEvidenceResponse(queryText, responseCategory); + } + if (isNoEvidenceAnswer(digest.answer())) { + return noEvidenceResponse(queryText, responseCategory, digest.cautions()); + } return new SearchResponse( queryText, responseCategory, - digest != null ? digest.answer() : "根据已入库 SOP:" + first.content(), + digest.answer(), "引用 " + docs.size() + " 份文档 / " + snippets.size() + " 个片段", List.copyOf(docs.values()), snippets, List.of("题 1:请按命中 SOP 复述处理步骤。", "题 2:哪些信息必须留痕并可追溯?"), records(), - digest != null ? digest.keyPoints() : List.of(), - digest != null ? digest.cautions() : List.of(), + digest.keyPoints(), + digest.cautions(), null ); } @@ -2063,7 +2064,7 @@ public class AihrSopSeedService { system.put("content", "你是物业一线员工的SOP助手。只返回 JSON,不要 markdown。字段固定为 answer, keyPoints, cautions。" + "answer 是对问题的直接回答(不超过60字,含关键数字/时限);keyPoints 是3到6条可执行要点(每条一句话,含具体数字);" + "cautions 是0到3条注意事项或红线。硬约束:只能基于给定片段内容作答,片段中没有的信息不得编造;" - + "若片段无法回答问题,answer 固定为「未在已入库 SOP 中找到明确依据」且 keyPoints 为空数组。"); + + "若片段无法回答问题,answer 固定为「" + NO_CLEAR_SOP_EVIDENCE + "」且 keyPoints 为空数组。"); ObjectNode user = messages.addObject(); user.put("role", "user"); @@ -2247,6 +2248,30 @@ public class AihrSopSeedService { return score <= 0 ? "片段" : String.format("%.2f", score); } + private static boolean isNoEvidenceAnswer(String answer) { + return NO_CLEAR_SOP_EVIDENCE.equals(Optional.ofNullable(answer).orElse("").trim()); + } + + private static SearchResponse noEvidenceResponse(String queryText, String category) { + return noEvidenceResponse(queryText, category, List.of()); + } + + private static SearchResponse noEvidenceResponse(String queryText, String category, List cautions) { + return new SearchResponse( + queryText, + firstNonBlank(category, "sop"), + NO_CLEAR_SOP_EVIDENCE, + "未找到可引用片段", + List.of(), + List.of(), + List.of(), + records(), + List.of(), + cautions, + null + ); + } + private KnowledgeConfig knowledgeConfig(String category) { List rows = jdbcTemplate.query(""" select id, coalesce(text_block_size, 800) as block_size, coalesce(overlap_char, 120) as overlap_char