fix(knowledge): reject irrelevant formal citations
This commit is contained in:
+43
@@ -28,6 +28,23 @@ public final class AihrFormalPolicyClassifier {
|
||||
"车辆被占位", "车辆堵住", "停车冲突", "安全险情", "突发事件"
|
||||
);
|
||||
|
||||
private static final List<EvidenceProfile> EVIDENCE_PROFILES = List.of(
|
||||
evidence(List.of("日常巡检"), List.of("日常巡检", "巡检", "巡视", "巡查", "检查频次")),
|
||||
evidence(List.of("强降雨", "防汛"), List.of("强降雨", "暴雨", "防汛", "汛期", "预警")),
|
||||
evidence(List.of("大件垃圾", "装修垃圾", "装修产生"),
|
||||
List.of("大件垃圾", "装修垃圾", "垃圾清运", "清运", "装修废料")),
|
||||
evidence(List.of("报修受理"), List.of("报修", "维修", "回访")),
|
||||
evidence(List.of("物业费账单", "计费期间", "费用构成"),
|
||||
List.of("物业费", "物业服务费", "账单", "计费", "收费标准")),
|
||||
evidence(List.of("分期缴费"), List.of("分期", "缓缴")),
|
||||
evidence(List.of("长期无人居住", "空置房"), List.of("无人居住", "空置房", "空关房", "空房")),
|
||||
evidence(List.of("维修一直没进展"), List.of("维修", "报修", "催办", "进展")),
|
||||
evidence(List.of("欠费", "催费"), List.of("欠费", "催费", "物业费", "物业服务费")),
|
||||
evidence(List.of("车辆被占位", "车辆堵住", "停车冲突"),
|
||||
List.of("占用他人车位", "占位", "堵住", "车辆停放", "停车")),
|
||||
evidence(List.of("安全险情", "突发事件"), List.of("险情", "突发事件", "应急", "上报"))
|
||||
);
|
||||
|
||||
private AihrFormalPolicyClassifier() {
|
||||
}
|
||||
|
||||
@@ -46,7 +63,33 @@ public final class AihrFormalPolicyClassifier {
|
||||
return containsAny(value, FORMAL_TERMS) || containsAny(value, CONTROLLED_DOMAINS);
|
||||
}
|
||||
|
||||
/**
|
||||
* A formal source is not sufficient by itself: the cited fragment must also contain
|
||||
* a controlled-domain anchor for the question. Unknown policy domains keep the
|
||||
* governance-only behavior until a deterministic profile is defined.
|
||||
*/
|
||||
public static boolean isRelevantFormalEvidence(String question, String evidenceText) {
|
||||
String query = question == null ? "" : question.trim();
|
||||
String evidence = evidenceText == null ? "" : evidenceText.trim();
|
||||
if (query.isEmpty() || evidence.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
for (EvidenceProfile profile : EVIDENCE_PROFILES) {
|
||||
if (containsAny(query, profile.questionSignals())) {
|
||||
return containsAny(evidence, profile.evidenceSignals());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static EvidenceProfile evidence(List<String> questionSignals, List<String> evidenceSignals) {
|
||||
return new EvidenceProfile(questionSignals, evidenceSignals);
|
||||
}
|
||||
|
||||
private static boolean containsAny(String value, List<String> terms) {
|
||||
return terms.stream().anyMatch(value::contains);
|
||||
}
|
||||
|
||||
private record EvidenceProfile(List<String> questionSignals, List<String> evidenceSignals) {
|
||||
}
|
||||
}
|
||||
|
||||
+14
-4
@@ -311,7 +311,8 @@ public class AihrKnowledgeQueryService {
|
||||
SearchResponse legacy = sopService.searchAuthorized(new SearchRequest(
|
||||
request.queryText(), request.category(), request.position(), retrievalSource, request.limit()),
|
||||
principal == null ? "" : principal.extPartyId(), spaceIds);
|
||||
List<Citation> citations = citations(app.tenantId(), spaceIds, legacy.snippets(), formalPolicyOnly);
|
||||
List<Citation> citations = citations(
|
||||
app.tenantId(), spaceIds, legacy.snippets(), formalPolicyOnly, request.queryText());
|
||||
boolean noEvidence = citations.isEmpty();
|
||||
String answer = formalPolicyOnly && noEvidence
|
||||
? "当前已批准且在有效期内的正式资料中没有找到足够依据,不能使用访谈、经验材料或草稿补全政策答案。"
|
||||
@@ -590,7 +591,7 @@ public class AihrKnowledgeQueryService {
|
||||
}
|
||||
|
||||
private List<Citation> citations(String tenantId, Set<Long> allowedSpaceIds, List<SnippetResponse> snippets,
|
||||
boolean formalPolicyOnly) {
|
||||
boolean formalPolicyOnly, String queryText) {
|
||||
if (snippets == null || snippets.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
@@ -638,11 +639,20 @@ public class AihrKnowledgeQueryService {
|
||||
formalSourceClause),
|
||||
(rs, rowNum) -> new Citation(
|
||||
rs.getString("space_code"), "DOCUMENT", rs.getString("doc_id"), rs.getString("title"),
|
||||
truncate(rs.getString("content"), 500), rs.getLong("fragment_id")
|
||||
rs.getString("content"), rs.getLong("fragment_id")
|
||||
), args.toArray());
|
||||
Map<Long, Citation> byId = new LinkedHashMap<>();
|
||||
rows.forEach(row -> byId.put(row.fragmentId(), row));
|
||||
return fragmentIds.stream().map(byId::get).filter(java.util.Objects::nonNull).toList();
|
||||
return fragmentIds.stream()
|
||||
.map(byId::get)
|
||||
.filter(java.util.Objects::nonNull)
|
||||
.filter(row -> !formalPolicyOnly
|
||||
|| AihrFormalPolicyClassifier.isRelevantFormalEvidence(queryText, row.snippet()))
|
||||
.map(row -> new Citation(
|
||||
row.spaceCode(), row.sourceType(), row.docId(), row.title(),
|
||||
truncate(row.snippet(), 500), row.fragmentId()
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<String> spaceCodes(String tenantId, Set<Long> spaceIds) {
|
||||
|
||||
+7
-1
@@ -2916,8 +2916,14 @@ public class AihrSopSeedService {
|
||||
category, queryText, limitValue, allowedKnowledgeIds, formalPolicyOnly);
|
||||
List<KnowledgeHit> textHits = rrf(keywordHits, fulltextHits(
|
||||
category, queryText, limitValue, allowedKnowledgeIds, formalPolicyOnly), limitValue);
|
||||
return rerankHits(queryText, rrf(textHits, vectorHits(
|
||||
List<KnowledgeHit> ranked = rerankHits(queryText, rrf(textHits, vectorHits(
|
||||
category, queryText, limitValue, allowedKnowledgeIds, formalPolicyOnly), limitValue));
|
||||
if (!formalPolicyOnly) {
|
||||
return ranked;
|
||||
}
|
||||
return ranked.stream()
|
||||
.filter(hit -> AihrFormalPolicyClassifier.isRelevantFormalEvidence(queryText, hit.content()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static String knowledgeScopeSql(String alias, Set<Long> allowedKnowledgeIds) {
|
||||
|
||||
+25
@@ -47,4 +47,29 @@ class AihrFormalPolicyClassifierTest {
|
||||
assertTrue(AihrFormalPolicyClassifier.requiresFormalSource(
|
||||
"mobile_uni_agent_formal_policy", "继续说明"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void formalEvidenceMustMatchTheControlledDomainQuestion() {
|
||||
String installmentQuestion = "住户提出分期缴费时,申请条件、审批角色和留痕要求是什么?";
|
||||
|
||||
assertFalse(AihrFormalPolicyClassifier.isRelevantFormalEvidence(
|
||||
installmentQuestion, "姿态端正,自然大方,工作手机应保持畅通。"));
|
||||
assertFalse(AihrFormalPolicyClassifier.isRelevantFormalEvidence(
|
||||
installmentQuestion, "节水型小区申报后,由相关部门组织评价。"));
|
||||
assertFalse(AihrFormalPolicyClassifier.isRelevantFormalEvidence(
|
||||
installmentQuestion, "车辆停放不得占用他人车位。"));
|
||||
assertFalse(AihrFormalPolicyClassifier.isRelevantFormalEvidence(
|
||||
installmentQuestion, "欠费住户由生活顾问定期提醒缴费。"));
|
||||
assertTrue(AihrFormalPolicyClassifier.isRelevantFormalEvidence(
|
||||
installmentQuestion, "申请分期缴费须由项目负责人审批,并保留审批记录。"));
|
||||
|
||||
assertTrue(AihrFormalPolicyClassifier.isRelevantFormalEvidence(
|
||||
"装修产生的废料该找谁、怎么约清运、能不能收费?",
|
||||
"装修垃圾应登记后预约清运,收费标准须公示。"));
|
||||
assertTrue(AihrFormalPolicyClassifier.isRelevantFormalEvidence(
|
||||
"车辆被占位或堵住时,物业怎么协调才不会升级冲突?",
|
||||
"车辆停放不得占用他人车位,发现占位应先联系车主。"));
|
||||
assertFalse(AihrFormalPolicyClassifier.isRelevantFormalEvidence(
|
||||
installmentQuestion, " "));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user