From 2ef3e397447d723036469d29534ac3292db94279 Mon Sep 17 00:00:00 2001 From: let5sne Date: Thu, 9 Jul 2026 15:37:01 +0800 Subject: [PATCH] fix(aihr): trim sop evidence snippets --- .../aihr/service/AihrSopSeedService.java | 42 ++++++++++++++++--- .../aihr/service/AihrSopSeedServiceTest.java | 35 ++++++++++++++++ 2 files changed, 72 insertions(+), 5 deletions(-) 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 7075b03a..baa52ac5 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 @@ -97,6 +97,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 int MAX_EVIDENCE_SNIPPETS = 3; private static final String NO_CLEAR_SOP_EVIDENCE = "未在已入库 SOP 中找到明确依据"; private static final String SOP_ANSWER_PROMPT_VERSION = "builtin-sop_answer-v1"; private static final String SOP_ANSWER_SYSTEM_PROMPT = "你是物业一线员工的SOP助手。只返回 JSON,不要 markdown。字段固定为 answer, keyPoints, cautions。" @@ -2209,16 +2210,17 @@ public class AihrSopSeedService { } private SearchResponse responseFromHits(String category, String queryText, List hits, List extraCautions) { - KnowledgeHit first = hits.get(0); + List evidenceHits = evidenceHits(queryText, hits); + KnowledgeHit first = evidenceHits.get(0); String responseCategory = isBlank(category) ? first.category() : category; - List snippets = hits.stream() + List snippets = evidenceHits.stream() .map(hit -> new SnippetResponse("第 " + hit.idx() + " 段:" + hit.title(), hit.content(), hit.fragmentId())) .toList(); Map docs = new LinkedHashMap<>(); - for (KnowledgeHit hit : hits) { + for (KnowledgeHit hit : evidenceHits) { docs.putIfAbsent(hit.title(), new DocResponse(hit.title(), hit.description(), "命中 " + scoreText(hit.score()), "pill-success")); } - SearchDigest digest = searchDigest(queryText, hits); + SearchDigest digest = searchDigest(queryText, evidenceHits); if (digest == null) { return rawHitResponse(responseCategory, queryText, List.copyOf(docs.values()), snippets, extraCautions); } @@ -2240,6 +2242,36 @@ public class AihrSopSeedService { ); } + static List evidenceHits(String queryText, List hits) { + if (hits == null || hits.isEmpty()) { + return List.of(); + } + KnowledgeHit first = hits.get(0); + if (queryMentionsTitle(queryText, first.title())) { + List sameTitle = hits.stream() + .filter(hit -> Objects.equals(hit.title(), first.title())) + .limit(MAX_EVIDENCE_SNIPPETS) + .toList(); + if (!sameTitle.isEmpty()) { + return sameTitle; + } + } + return hits.stream().limit(MAX_EVIDENCE_SNIPPETS).toList(); + } + + private static boolean queryMentionsTitle(String queryText, String title) { + String query = evidenceText(queryText); + String docTitle = evidenceText(title); + return query.length() >= 4 && docTitle.length() >= 4 && (query.contains(docTitle) || docTitle.contains(query)); + } + + private static String evidenceText(String value) { + return Optional.ofNullable(value).orElse("") + .toLowerCase(Locale.ROOT) + .replaceFirst("\\.[a-z0-9]{1,8}$", "") + .replaceAll("[^\\p{IsHan}\\p{Alnum}]+", ""); + } + static SearchResponse rawHitResponse(String category, String queryText, List docs, List snippets, List extraCautions) { if (snippets == null || snippets.isEmpty()) { return noEvidenceResponse(queryText, category, extraCautions); @@ -3283,7 +3315,7 @@ public class AihrSopSeedService { ) { } - private record KnowledgeHit(Long fragmentId, String title, String category, String description, String docId, String content, int idx, double score) { + record KnowledgeHit(Long fragmentId, String title, String category, String description, String docId, String content, int idx, double score) { } private record FeedbackFilterResult(List hits, List cautions) { diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrSopSeedServiceTest.java b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrSopSeedServiceTest.java index 3bad265a..77b4a68b 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrSopSeedServiceTest.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrSopSeedServiceTest.java @@ -33,4 +33,39 @@ public class AihrSopSeedServiceTest { assertTrue(AihrSopSeedService.skipSopReview("verify_demo_questions")); assertFalse(AihrSopSeedService.skipSopReview("knowledge_search")); } + + @Test + @Tag("dev") + public void evidenceHitsKeepsNamedDocumentOnly() { + List hits = List.of( + hit(1L, "生活顾问的一天.mp4"), + hit(2L, "生活顾问的一天.mp4"), + hit(3L, "信息安全管理培训.pptx") + ); + + List evidence = AihrSopSeedService.evidenceHits("生活顾问的一天做什么", hits); + + assertEquals(2, evidence.size()); + assertTrue(evidence.stream().allMatch(hit -> "生活顾问的一天.mp4".equals(hit.title()))); + } + + @Test + @Tag("dev") + public void evidenceHitsCapsGenericQueries() { + List hits = List.of( + hit(1L, "投诉处理 SOP.pdf"), + hit(2L, "催缴沟通 SOP.pdf"), + hit(3L, "报修跟进 SOP.pdf"), + hit(4L, "保洁合同.pdf") + ); + + List evidence = AihrSopSeedService.evidenceHits("生活顾问怎么处理业主问题", hits); + + assertEquals(3, evidence.size()); + assertEquals(3L, evidence.get(2).fragmentId()); + } + + private static AihrSopSeedService.KnowledgeHit hit(Long fragmentId, String title) { + return new AihrSopSeedService.KnowledgeHit(fragmentId, title, "sop", "", "doc-" + fragmentId, "片段内容", 1, 1.0); + } }