fix(aihr): trim sop evidence snippets
This commit is contained in:
+37
-5
@@ -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<KnowledgeHit> hits, List<String> extraCautions) {
|
||||
KnowledgeHit first = hits.get(0);
|
||||
List<KnowledgeHit> evidenceHits = evidenceHits(queryText, hits);
|
||||
KnowledgeHit first = evidenceHits.get(0);
|
||||
String responseCategory = isBlank(category) ? first.category() : category;
|
||||
List<SnippetResponse> snippets = hits.stream()
|
||||
List<SnippetResponse> snippets = evidenceHits.stream()
|
||||
.map(hit -> new SnippetResponse("第 " + hit.idx() + " 段:" + hit.title(), hit.content(), hit.fragmentId()))
|
||||
.toList();
|
||||
Map<String, DocResponse> 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<KnowledgeHit> evidenceHits(String queryText, List<KnowledgeHit> hits) {
|
||||
if (hits == null || hits.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
KnowledgeHit first = hits.get(0);
|
||||
if (queryMentionsTitle(queryText, first.title())) {
|
||||
List<KnowledgeHit> 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<DocResponse> docs, List<SnippetResponse> snippets, List<String> 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<KnowledgeHit> hits, List<String> cautions) {
|
||||
|
||||
+35
@@ -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<AihrSopSeedService.KnowledgeHit> hits = List.of(
|
||||
hit(1L, "生活顾问的一天.mp4"),
|
||||
hit(2L, "生活顾问的一天.mp4"),
|
||||
hit(3L, "信息安全管理培训.pptx")
|
||||
);
|
||||
|
||||
List<AihrSopSeedService.KnowledgeHit> evidence = AihrSopSeedService.evidenceHits("生活顾问的一天做什么", hits);
|
||||
|
||||
assertEquals(2, evidence.size());
|
||||
assertTrue(evidence.stream().allMatch(hit -> "生活顾问的一天.mp4".equals(hit.title())));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("dev")
|
||||
public void evidenceHitsCapsGenericQueries() {
|
||||
List<AihrSopSeedService.KnowledgeHit> hits = List.of(
|
||||
hit(1L, "投诉处理 SOP.pdf"),
|
||||
hit(2L, "催缴沟通 SOP.pdf"),
|
||||
hit(3L, "报修跟进 SOP.pdf"),
|
||||
hit(4L, "保洁合同.pdf")
|
||||
);
|
||||
|
||||
List<AihrSopSeedService.KnowledgeHit> 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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user