feat(aihr): bind SOP direct-answer digest to sop_answer prompt template
- Seed aihr_prompt_template with code=sop_answer (conclusion-first + numbered steps + citations appended), matching the built-in prompt - Digest prefers the enabled sop_answer template, falls back to the built-in constant, and logs prompt_version; answer/keyPoints/cautions contract unchanged
This commit is contained in:
+56
-15
@@ -92,6 +92,17 @@ public class AihrSopSeedService {
|
||||
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 String SOP_ANSWER_PROMPT_VERSION = "builtin-sop_answer-v1";
|
||||
private static final String SOP_ANSWER_SYSTEM_PROMPT = "你是物业一线员工的SOP助手。只返回 JSON,不要 markdown。字段固定为 answer, keyPoints, cautions。"
|
||||
+ "answer 必须结论先行,60字内直接告诉员工现在怎么做;keyPoints 必须是3到5条可执行步骤,每条以「一、」「二、」「三、」「四、」「五、」开头,包含责任人、动作、时限或留痕要求;"
|
||||
+ "cautions 是0到3条注意事项或红线。引用片段由系统后置展示,不要在 answer 中堆大段原文。硬约束:只能基于给定片段内容作答,片段中没有的信息不得编造;"
|
||||
+ "若片段无法回答问题,answer 固定为「" + NO_CLEAR_SOP_EVIDENCE + "」且 keyPoints 为空数组。";
|
||||
private static final String SOP_ANSWER_USER_TEMPLATE = """
|
||||
员工的问题:{{queryText}}
|
||||
|
||||
检索命中的 SOP 片段:
|
||||
{{context}}
|
||||
""";
|
||||
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
||||
|
||||
private final Map<String, SopSeed> seeds = buildSeeds();
|
||||
@@ -2035,16 +2046,19 @@ public class AihrSopSeedService {
|
||||
.append(truncate(hit.content(), 1500)).append("\n\n");
|
||||
}
|
||||
try {
|
||||
String content = callDigestModel(runtime.get(), queryText, context.toString());
|
||||
DigestModelResult result = callDigestModel(runtime.get(), queryText, context.toString());
|
||||
String content = result.content();
|
||||
JsonNode root = objectMapper.readTree(jsonObjectText(content));
|
||||
String answer = root.path("answer").asText("");
|
||||
if (isBlank(answer)) {
|
||||
return null;
|
||||
}
|
||||
log.info("sop answer prompt_version={}", result.promptVersion());
|
||||
return new SearchDigest(
|
||||
truncate(answer, 200),
|
||||
jsonStrings(root.path("keyPoints"), 6, 120),
|
||||
jsonStrings(root.path("cautions"), 3, 120)
|
||||
jsonStrings(root.path("cautions"), 3, 120),
|
||||
result.promptVersion()
|
||||
);
|
||||
} catch (Exception e) {
|
||||
log.warn("search digest failed, keep raw answer: {}", e.getMessage());
|
||||
@@ -2052,7 +2066,8 @@ public class AihrSopSeedService {
|
||||
}
|
||||
}
|
||||
|
||||
private String callDigestModel(ChatRuntime runtime, String queryText, String context) throws Exception {
|
||||
private DigestModelResult callDigestModel(ChatRuntime runtime, String queryText, String context) throws Exception {
|
||||
PromptTemplate prompt = sopAnswerPrompt();
|
||||
ObjectNode body = objectMapper.createObjectNode();
|
||||
body.put("model", runtime.modelName());
|
||||
body.put("temperature", 0);
|
||||
@@ -2061,19 +2076,11 @@ public class AihrSopSeedService {
|
||||
ArrayNode messages = body.putArray("messages");
|
||||
ObjectNode system = messages.addObject();
|
||||
system.put("role", "system");
|
||||
system.put("content", "你是物业一线员工的SOP助手。只返回 JSON,不要 markdown。字段固定为 answer, keyPoints, cautions。"
|
||||
+ "answer 是对问题的直接回答(不超过60字,含关键数字/时限);keyPoints 是3到6条可执行要点(每条一句话,含具体数字);"
|
||||
+ "cautions 是0到3条注意事项或红线。硬约束:只能基于给定片段内容作答,片段中没有的信息不得编造;"
|
||||
+ "若片段无法回答问题,answer 固定为「" + NO_CLEAR_SOP_EVIDENCE + "」且 keyPoints 为空数组。");
|
||||
system.put("content", prompt.systemPrompt());
|
||||
|
||||
ObjectNode user = messages.addObject();
|
||||
user.put("role", "user");
|
||||
user.put("content", """
|
||||
员工的问题:%s
|
||||
|
||||
检索命中的 SOP 片段:
|
||||
%s
|
||||
""".formatted(queryText, context));
|
||||
user.put("content", renderPrompt(prompt.template(), queryText, context));
|
||||
|
||||
HttpRequest.Builder builder = HttpRequest.newBuilder()
|
||||
.uri(URI.create(normalizeBaseUrl(runtime.baseUrl()) + "/chat/completions"))
|
||||
@@ -2094,7 +2101,35 @@ public class AihrSopSeedService {
|
||||
if (!choices.isArray() || choices.isEmpty()) {
|
||||
throw new IllegalStateException("digest response missing choices");
|
||||
}
|
||||
return choices.get(0).path("message").path("content").asText();
|
||||
return new DigestModelResult(choices.get(0).path("message").path("content").asText(), prompt.promptVersion());
|
||||
}
|
||||
|
||||
private PromptTemplate sopAnswerPrompt() {
|
||||
try {
|
||||
List<PromptTemplate> rows = jdbcTemplate.query("""
|
||||
SELECT system_prompt, template, version
|
||||
FROM aihr_prompt_template
|
||||
WHERE tenant_id = ? AND code = 'sop_answer' AND enabled = 1
|
||||
ORDER BY version DESC, id DESC
|
||||
LIMIT 1
|
||||
""", (rs, rowNum) -> new PromptTemplate(
|
||||
firstNonBlank(rs.getString("system_prompt"), SOP_ANSWER_SYSTEM_PROMPT),
|
||||
firstNonBlank(rs.getString("template"), SOP_ANSWER_USER_TEMPLATE),
|
||||
"sop_answer:" + firstNonBlank(rs.getString("version"), "v1")
|
||||
), TENANT_ID);
|
||||
if (!rows.isEmpty()) {
|
||||
return rows.get(0);
|
||||
}
|
||||
} catch (DataAccessException ignored) {
|
||||
// prompt table is optional in demo fallback; built-in prompt keeps SOP search usable.
|
||||
}
|
||||
return new PromptTemplate(SOP_ANSWER_SYSTEM_PROMPT, SOP_ANSWER_USER_TEMPLATE, SOP_ANSWER_PROMPT_VERSION);
|
||||
}
|
||||
|
||||
private static String renderPrompt(String template, String queryText, String context) {
|
||||
return firstNonBlank(template, SOP_ANSWER_USER_TEMPLATE)
|
||||
.replace("{{queryText}}", firstNonBlank(queryText, ""))
|
||||
.replace("{{context}}", firstNonBlank(context, ""));
|
||||
}
|
||||
|
||||
private String callSummaryCardModel(ChatRuntime runtime, String queryText, String context) throws Exception {
|
||||
@@ -2199,7 +2234,13 @@ public class AihrSopSeedService {
|
||||
return List.copyOf(values);
|
||||
}
|
||||
|
||||
private record SearchDigest(String answer, List<String> keyPoints, List<String> cautions) {
|
||||
private record SearchDigest(String answer, List<String> keyPoints, List<String> cautions, String promptVersion) {
|
||||
}
|
||||
|
||||
private record DigestModelResult(String content, String promptVersion) {
|
||||
}
|
||||
|
||||
private record PromptTemplate(String systemPrompt, String template, String promptVersion) {
|
||||
}
|
||||
|
||||
private static String dbCategory(String category) {
|
||||
|
||||
Reference in New Issue
Block a user