feat(aihr): return explicit no-evidence SOP answers and log knowledge gaps
- Empty/unanswerable SOP queries now return the fixed 「未在已入库 SOP 中找到明确依据」 response instead of falling back to seed answers (no fake answers) - Record knowledge_gap (question/category/position/source) whenever search yields no evidence — feeds the phase-two open question board - Extract NO_CLEAR_SOP_EVIDENCE constant shared by digest prompt and checks
This commit is contained in:
+44
-19
@@ -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<String, SopSeed> 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<String> 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<KnowledgeConfig> rows = jdbcTemplate.query("""
|
||||
select id, coalesce(text_block_size, 800) as block_size, coalesce(overlap_char, 120) as overlap_char
|
||||
|
||||
Reference in New Issue
Block a user