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_MODEL = "local-hash-v1";
|
||||||
private static final String LOCAL_EMBEDDING_BASE_URL = "local";
|
private static final String LOCAL_EMBEDDING_BASE_URL = "local";
|
||||||
private static final int LOCAL_EMBEDDING_DIMENSION = 1536;
|
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 static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
||||||
|
|
||||||
private final Map<String, SopSeed> seeds = buildSeeds();
|
private final Map<String, SopSeed> seeds = buildSeeds();
|
||||||
@@ -120,25 +121,19 @@ public class AihrSopSeedService {
|
|||||||
String category = isBlank(requestedCategory) ? seed.category() : requestedCategory;
|
String category = isBlank(requestedCategory) ? seed.category() : requestedCategory;
|
||||||
String position = firstNonBlank(request == null ? null : request.position(), "生活顾问");
|
String position = firstNonBlank(request == null ? null : request.position(), "生活顾问");
|
||||||
String source = normalizeSearchSource(request == null ? null : request.source());
|
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());
|
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);
|
return withReviewId(dbResponse, source);
|
||||||
}
|
}
|
||||||
recordKnowledgeGap(queryText, category, position, source);
|
recordKnowledgeGap(queryText, category, position, source);
|
||||||
return withReviewId(new SearchResponse(
|
return withReviewId(noEvidenceResponse(queryText, category), source);
|
||||||
queryText,
|
|
||||||
seed.category(),
|
|
||||||
seed.answer(),
|
|
||||||
seed.reference(),
|
|
||||||
seed.docs(),
|
|
||||||
seed.snippets(),
|
|
||||||
seed.training(),
|
|
||||||
records(),
|
|
||||||
List.of(),
|
|
||||||
List.of(),
|
|
||||||
null
|
|
||||||
), source);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public SopReviewResponse reviewSearch(Long id, SopReviewRequest request) {
|
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"));
|
docs.putIfAbsent(hit.title(), new DocResponse(hit.title(), hit.description(), "命中 " + scoreText(hit.score()), "pill-success"));
|
||||||
}
|
}
|
||||||
SearchDigest digest = searchDigest(queryText, hits);
|
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(
|
return new SearchResponse(
|
||||||
queryText,
|
queryText,
|
||||||
responseCategory,
|
responseCategory,
|
||||||
digest != null ? digest.answer() : "根据已入库 SOP:" + first.content(),
|
digest.answer(),
|
||||||
"引用 " + docs.size() + " 份文档 / " + snippets.size() + " 个片段",
|
"引用 " + docs.size() + " 份文档 / " + snippets.size() + " 个片段",
|
||||||
List.copyOf(docs.values()),
|
List.copyOf(docs.values()),
|
||||||
snippets,
|
snippets,
|
||||||
List.of("题 1:请按命中 SOP 复述处理步骤。", "题 2:哪些信息必须留痕并可追溯?"),
|
List.of("题 1:请按命中 SOP 复述处理步骤。", "题 2:哪些信息必须留痕并可追溯?"),
|
||||||
records(),
|
records(),
|
||||||
digest != null ? digest.keyPoints() : List.of(),
|
digest.keyPoints(),
|
||||||
digest != null ? digest.cautions() : List.of(),
|
digest.cautions(),
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2063,7 +2064,7 @@ public class AihrSopSeedService {
|
|||||||
system.put("content", "你是物业一线员工的SOP助手。只返回 JSON,不要 markdown。字段固定为 answer, keyPoints, cautions。"
|
system.put("content", "你是物业一线员工的SOP助手。只返回 JSON,不要 markdown。字段固定为 answer, keyPoints, cautions。"
|
||||||
+ "answer 是对问题的直接回答(不超过60字,含关键数字/时限);keyPoints 是3到6条可执行要点(每条一句话,含具体数字);"
|
+ "answer 是对问题的直接回答(不超过60字,含关键数字/时限);keyPoints 是3到6条可执行要点(每条一句话,含具体数字);"
|
||||||
+ "cautions 是0到3条注意事项或红线。硬约束:只能基于给定片段内容作答,片段中没有的信息不得编造;"
|
+ "cautions 是0到3条注意事项或红线。硬约束:只能基于给定片段内容作答,片段中没有的信息不得编造;"
|
||||||
+ "若片段无法回答问题,answer 固定为「未在已入库 SOP 中找到明确依据」且 keyPoints 为空数组。");
|
+ "若片段无法回答问题,answer 固定为「" + NO_CLEAR_SOP_EVIDENCE + "」且 keyPoints 为空数组。");
|
||||||
|
|
||||||
ObjectNode user = messages.addObject();
|
ObjectNode user = messages.addObject();
|
||||||
user.put("role", "user");
|
user.put("role", "user");
|
||||||
@@ -2247,6 +2248,30 @@ public class AihrSopSeedService {
|
|||||||
return score <= 0 ? "片段" : String.format("%.2f", score);
|
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) {
|
private KnowledgeConfig knowledgeConfig(String category) {
|
||||||
List<KnowledgeConfig> rows = jdbcTemplate.query("""
|
List<KnowledgeConfig> rows = jdbcTemplate.query("""
|
||||||
select id, coalesce(text_block_size, 800) as block_size, coalesce(overlap_char, 120) as overlap_char
|
select id, coalesce(text_block_size, 800) as block_size, coalesce(overlap_char, 120) as overlap_char
|
||||||
|
|||||||
Reference in New Issue
Block a user