fix(aihr): trim sop evidence snippets

This commit is contained in:
2026-07-09 15:37:01 +08:00
parent ba8c35904e
commit 2ef3e39744
2 changed files with 72 additions and 5 deletions
@@ -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) {
@@ -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);
}
}