fix(aihr): align practice scoring with BRD five dimensions
This commit is contained in:
+2
@@ -161,6 +161,8 @@ public class AihrPracticeController {
|
||||
request == null ? null : request.correctedEmotion(),
|
||||
request == null ? null : request.correctedCommunication(),
|
||||
request == null ? null : request.correctedMarketing(),
|
||||
request == null ? null : request.correctedTaskCompletion(),
|
||||
request == null ? null : request.correctedResponseTimeliness(),
|
||||
request == null ? null : request.reason()
|
||||
);
|
||||
}
|
||||
|
||||
+8
@@ -196,8 +196,16 @@ public final class AihrPracticeDto {
|
||||
Integer correctedEmotion,
|
||||
Integer correctedCommunication,
|
||||
Integer correctedMarketing,
|
||||
Integer correctedTaskCompletion,
|
||||
Integer correctedResponseTimeliness,
|
||||
String reason
|
||||
) {
|
||||
public CalibrationRequest(String reviewer, Integer correctedTotal, Integer correctedCompliance,
|
||||
Integer correctedEmotion, Integer correctedCommunication, Integer correctedMarketing,
|
||||
String reason) {
|
||||
this(reviewer, correctedTotal, correctedCompliance, correctedEmotion, correctedCommunication,
|
||||
correctedMarketing, null, null, reason);
|
||||
}
|
||||
}
|
||||
|
||||
public record CalibrationResponse(Long id, String sessionId, Integer originalScore, Integer correctedScore, String reviewer, String reason, String status, String createTime) {
|
||||
|
||||
+19
-1
@@ -33,9 +33,15 @@ public class AihrPracticeLlmService {
|
||||
int communication,
|
||||
int emotion,
|
||||
int marketing,
|
||||
int taskCompletion,
|
||||
int responseTimeliness,
|
||||
String rewrite,
|
||||
String summary
|
||||
) {
|
||||
public PracticeScore(int total, int compliance, int communication, int emotion, int marketing,
|
||||
String rewrite, String summary) {
|
||||
this(total, compliance, communication, emotion, marketing, compliance, -1, rewrite, summary);
|
||||
}
|
||||
}
|
||||
|
||||
public record PracticeTurn(
|
||||
@@ -140,7 +146,8 @@ public class AihrPracticeLlmService {
|
||||
评分锚点:60=及格线(有明显缺失);75=合格(覆盖主要要点);90=优秀(完整且超预期)。
|
||||
约束:只根据"员工"实际说过的话评分,未提及的内容不得臆造加分。
|
||||
只输出一个 JSON 对象,不要输出任何其他文字、解释或代码块标记,字段如下:
|
||||
{"total":0-100,"compliance":0-100,"communication":0-100,"emotion":0-100,"marketing":0-100,"rewrite":"给员工的示范话术改写,80字内","summary":"一句话点评,60字内"}
|
||||
{"total":0-100,"taskCompletion":0-100,"compliance":0-100,"communication":0-100,"emotion":0-100,"responseTimeliness":0-100或-1,"marketing":0-100,"rewrite":"给员工的示范话术改写,80字内","summary":"一句话点评,60字内"}
|
||||
taskCompletion 评估问题是否真正被推进到可执行闭环;responseTimeliness 只有对话中存在明确响应时效证据时填写,否则填 -1。
|
||||
""".formatted(PROMPT_VERSION);
|
||||
String user = """
|
||||
训练场景:%s
|
||||
@@ -166,6 +173,8 @@ public class AihrPracticeLlmService {
|
||||
dimension(root, "communication", total),
|
||||
dimension(root, "emotion", total),
|
||||
dimension(root, "marketing", total),
|
||||
dimension(root, "taskCompletion", dimension(root, "compliance", total)),
|
||||
dimensionOrMissing(root, "responseTimeliness"),
|
||||
truncateText(root.path("rewrite").asText(""), 400),
|
||||
truncateText(root.path("summary").asText(""), 300)
|
||||
));
|
||||
@@ -204,6 +213,15 @@ public class AihrPracticeLlmService {
|
||||
return clampScore(node.asInt(fallback));
|
||||
}
|
||||
|
||||
private static int dimensionOrMissing(JsonNode root, String field) {
|
||||
JsonNode node = root.path(field);
|
||||
if (!node.isNumber() && !node.isTextual()) {
|
||||
return -1;
|
||||
}
|
||||
int value = node.asInt(-1);
|
||||
return value < 0 ? -1 : clampScore(value);
|
||||
}
|
||||
|
||||
/** 落库列为 varchar(1000),模型输出截断兜底。 */
|
||||
private static String truncateText(String value, int maxChars) {
|
||||
if (value == null || value.length() <= maxChars) {
|
||||
|
||||
+227
-56
@@ -90,6 +90,11 @@ public class AihrPracticeSeedService {
|
||||
"professional", "表达更专业,突出SOP依据、责任边界、留痕和闭环口径。"
|
||||
);
|
||||
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("MM-dd HH:mm");
|
||||
// 阶段一只统计“员工收到业主话术后到提交下一轮”的陪练响应时长;正式首响/工单处理时效仍需业务系统数据。
|
||||
private static final long RESPONSE_FAST_MS = 5_000L;
|
||||
private static final long RESPONSE_ACCEPTABLE_MS = 10_000L;
|
||||
private static final long RESPONSE_SLOW_MS = 20_000L;
|
||||
private static final long RESPONSE_VERY_SLOW_MS = 30_000L;
|
||||
private static final String PILOT_SCOPE_CTE = """
|
||||
WITH pilot_params AS (
|
||||
SELECT ? AS tenant_id, ? AS start_time, ? AS end_time
|
||||
@@ -489,7 +494,8 @@ public class AihrPracticeSeedService {
|
||||
String trainee = resolveTrainee(request, scenario);
|
||||
List<String> customerLines = new ArrayList<>();
|
||||
customerLines.add(firstRound.customer());
|
||||
activeSessions.put(sessionId, new ActiveSession(scenario.id(), trainee, resolveExtPartyId(request, trainee), request == null ? null : request.assignmentId(), isMobile(request), LocalDateTime.now(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), customerLines, new ArrayList<>()));
|
||||
long promptPresentedAt = System.currentTimeMillis();
|
||||
activeSessions.put(sessionId, new ActiveSession(scenario.id(), trainee, resolveExtPartyId(request, trainee), request == null ? null : request.assignmentId(), isMobile(request), LocalDateTime.ofInstant(java.time.Instant.ofEpochMilli(promptPresentedAt), java.time.ZoneId.systemDefault()), new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), customerLines, new ArrayList<>(), new ArrayList<>(List.of(promptPresentedAt)), new ArrayList<>()));
|
||||
return new StartResponse(
|
||||
sessionId,
|
||||
scenario.id(),
|
||||
@@ -518,6 +524,7 @@ public class AihrPracticeSeedService {
|
||||
if (request != null && Boolean.TRUE.equals(request.regenerate())) {
|
||||
return regenerateCoachHint(request, session, scenario, roundIndex, style);
|
||||
}
|
||||
rememberResponseLatency(session, roundIndex, System.currentTimeMillis());
|
||||
rememberTraineeReply(request, roundIndex);
|
||||
RoundSeed currentRound = scenario.rounds().get(roundIndex);
|
||||
int nextRoundIndex = roundIndex + 1;
|
||||
@@ -529,6 +536,9 @@ public class AihrPracticeSeedService {
|
||||
boolean redFlag = localRedFlag || resolved.redFlag();
|
||||
String coachHint = redFlag ? redFlagCoachHint(resolved.coachHint()) : resolved.coachHint();
|
||||
rememberTurnEvidence(session, roundIndex, resolved.emotion(), resolved.trust(), redFlag, coachHint);
|
||||
if (!finished) {
|
||||
rememberPromptPresentedAt(session, nextRoundIndex, System.currentTimeMillis());
|
||||
}
|
||||
return new TurnResponse(
|
||||
resolved.customerText(),
|
||||
"",
|
||||
@@ -681,25 +691,64 @@ public class AihrPracticeSeedService {
|
||||
* /finish 单次评分:配置了 chat 模型且学员有真实回复时走 LLM 结构化评分,否则用 seed 分。
|
||||
*/
|
||||
private PracticeResult evaluate(ActiveSession activeSession, ScenarioSeed scenario) {
|
||||
PracticeResult seedResult = new PracticeResult(scenario.total(), scenario.scores(), scenario.rewrite(), scenario.summary());
|
||||
PracticeResult seedResult = seedPracticeResult(activeSession, scenario);
|
||||
if (activeSession == null) {
|
||||
return seedResult;
|
||||
}
|
||||
return practiceLlmService.score(scenario.name(), scenario.goal(), scenario.strategy(), dialogueTurns(activeSession, scenario))
|
||||
.map(score -> new PracticeResult(
|
||||
score.total(),
|
||||
List.of(
|
||||
new DimensionResponse("合规", score.compliance(), "SOP关键点覆盖"),
|
||||
new DimensionResponse("沟通", score.communication(), "承诺与表达清晰度"),
|
||||
new DimensionResponse("情绪", score.emotion(), "安抚与承接能力"),
|
||||
new DimensionResponse("营销", score.marketing(), "增值转化意识")
|
||||
),
|
||||
isBlank(score.rewrite()) ? scenario.rewrite() : score.rewrite(),
|
||||
isBlank(score.summary()) ? scenario.summary() : score.summary()
|
||||
))
|
||||
.map(score -> {
|
||||
Integer measuredResponse = responseTimelinessScore(activeSession);
|
||||
Integer response = measuredResponse == null && score.responseTimeliness() >= 0
|
||||
? score.responseTimeliness() : measuredResponse;
|
||||
int standardization = average(score.compliance(), score.communication());
|
||||
int total = response == null
|
||||
? score.total()
|
||||
: weightedPracticeScore(score.taskCompletion(), standardization, score.emotion(), response, score.marketing());
|
||||
return new PracticeResult(
|
||||
total,
|
||||
brdDimensions(score.taskCompletion(), standardization, score.emotion(), response, score.marketing()),
|
||||
isBlank(score.rewrite()) ? scenario.rewrite() : score.rewrite(),
|
||||
isBlank(score.summary()) ? scenario.summary() : score.summary(),
|
||||
score.compliance(), score.communication(), score.taskCompletion(), response,
|
||||
averageResponseLatencyMs(activeSession)
|
||||
);
|
||||
})
|
||||
.orElse(seedResult);
|
||||
}
|
||||
|
||||
private PracticeResult seedPracticeResult(ActiveSession activeSession, ScenarioSeed scenario) {
|
||||
int compliance = scoreValue(scenario.scores(), "合规", scenario.total());
|
||||
int communication = scoreValue(scenario.scores(), "沟通", scenario.total());
|
||||
int emotion = scoreValue(scenario.scores(), "情绪", scenario.total());
|
||||
int marketing = scoreValue(scenario.scores(), "营销", scenario.total());
|
||||
Integer response = responseTimelinessScore(activeSession);
|
||||
int total = response == null
|
||||
? scenario.total()
|
||||
: weightedPracticeScore(compliance, average(compliance, communication), emotion, response, marketing);
|
||||
return new PracticeResult(
|
||||
total,
|
||||
brdDimensions(compliance, average(compliance, communication), emotion, response, marketing),
|
||||
scenario.rewrite(), scenario.summary(), compliance, communication, compliance, response,
|
||||
averageResponseLatencyMs(activeSession)
|
||||
);
|
||||
}
|
||||
|
||||
private List<DimensionResponse> brdDimensions(Integer taskCompletion, Integer standardization, Integer emotion,
|
||||
Integer responseTimeliness, Integer marketing) {
|
||||
return List.of(
|
||||
new DimensionResponse("任务完成度", taskCompletion, "权重40%;当前以SOP执行和问题推进结果代理"),
|
||||
new DimensionResponse("话术规范性", standardization, "权重25%;合规与沟通训练维度综合"),
|
||||
new DimensionResponse("情绪管理能力", emotion, "权重20%;共情承接与信任恢复"),
|
||||
new DimensionResponse("响应时效", responseTimeliness == null ? "待采集" : responseTimeliness, "权重10%;阶段一按陪练回合响应时长统计"),
|
||||
new DimensionResponse("增值转化潜力", marketing, "权重5%;增值引导与办理意识")
|
||||
);
|
||||
}
|
||||
|
||||
private int weightedPracticeScore(int taskCompletion, int standardization, int emotion, int responseTimeliness, int marketing) {
|
||||
return Math.round(taskCompletion * 0.40f + standardization * 0.25f + emotion * 0.20f
|
||||
+ responseTimeliness * 0.10f + marketing * 0.05f);
|
||||
}
|
||||
|
||||
public int mobileCompletedCount() {
|
||||
return countMobileRecords("");
|
||||
}
|
||||
@@ -1334,7 +1383,9 @@ public class AihrPracticeSeedService {
|
||||
}
|
||||
List<ReviewDetailResponse> rows = jdbcTemplate.query("""
|
||||
SELECT id, session_id, finished_time, trainee_name, scenario_id, scenario_name, total_score, status,
|
||||
summary, mentor_rewrite, ai_comment, review_advice, incentive_point, dim_compliance, dim_emotion, dim_communication, dim_marketing, dialogue_json, annotations_json
|
||||
summary, mentor_rewrite, ai_comment, review_advice, incentive_point,
|
||||
dim_task_completion, dim_response_timeliness, response_latency_ms,
|
||||
dim_compliance, dim_emotion, dim_communication, dim_marketing, dialogue_json, annotations_json
|
||||
FROM aihr_practice_session
|
||||
WHERE tenant_id = ? AND mode = 'mobile' AND id = ?
|
||||
""", this::mapReviewDetail, tenantId(), id);
|
||||
@@ -1497,16 +1548,19 @@ public class AihrPracticeSeedService {
|
||||
return null;
|
||||
}
|
||||
List<ScoreSnapshot> rows = jdbcTemplate.query("""
|
||||
SELECT total_score, dim_compliance, dim_emotion, dim_communication, dim_marketing
|
||||
SELECT total_score, dim_task_completion, dim_response_timeliness,
|
||||
dim_compliance, dim_emotion, dim_communication, dim_marketing
|
||||
FROM aihr_practice_session
|
||||
WHERE tenant_id = ? AND session_id = ?
|
||||
LIMIT 1
|
||||
""", (rs, rowNum) -> new ScoreSnapshot(
|
||||
rs.getInt("total_score"),
|
||||
rs.getInt("dim_compliance"),
|
||||
rs.getInt("dim_emotion"),
|
||||
rs.getInt("dim_communication"),
|
||||
rs.getInt("dim_marketing")
|
||||
nullableInt(rs, "total_score"),
|
||||
nullableInt(rs, "dim_task_completion"),
|
||||
nullableInt(rs, "dim_response_timeliness"),
|
||||
nullableInt(rs, "dim_compliance"),
|
||||
nullableInt(rs, "dim_emotion"),
|
||||
nullableInt(rs, "dim_communication"),
|
||||
nullableInt(rs, "dim_marketing")
|
||||
), tenantId(), sessionId.trim());
|
||||
if (rows.isEmpty()) {
|
||||
return null;
|
||||
@@ -1567,9 +1621,11 @@ public class AihrPracticeSeedService {
|
||||
COUNT(*) completed,
|
||||
COALESCE(ROUND(AVG(total_score)), 0) score,
|
||||
SUM(CASE WHEN status = '待复盘' THEN 1 ELSE 0 END) pending_review,
|
||||
COALESCE(ROUND(AVG(COALESCE(dim_task_completion, dim_compliance))), 0) task_completion,
|
||||
COALESCE(ROUND(AVG(dim_compliance)), 0) compliance,
|
||||
COALESCE(ROUND(AVG(dim_communication)), 0) communication,
|
||||
COALESCE(ROUND(AVG(dim_emotion)), 0) emotion,
|
||||
ROUND(AVG(dim_response_timeliness)) response_timeliness,
|
||||
COALESCE(ROUND(AVG(dim_marketing)), 0) marketing,
|
||||
COALESCE(SUM(incentive_point), 0) incentive_points,
|
||||
COALESCE(ROUND(SUM(CASE
|
||||
@@ -1583,14 +1639,16 @@ public class AihrPracticeSeedService {
|
||||
rs.getInt("completed"),
|
||||
rs.getInt("score"),
|
||||
rs.getInt("pending_review"),
|
||||
nullableInt(rs, "task_completion"),
|
||||
rs.getInt("compliance"),
|
||||
rs.getInt("communication"),
|
||||
rs.getInt("emotion"),
|
||||
nullableInt(rs, "response_timeliness"),
|
||||
rs.getInt("marketing"),
|
||||
rs.getInt("incentive_points"),
|
||||
rs.getInt("training_minutes")
|
||||
), tenantId(), party, party);
|
||||
CompetencySnapshot snapshot = rows.isEmpty() ? new CompetencySnapshot(0, 0, 0, 0, 0, 0, 0, 0, 0) : rows.get(0);
|
||||
CompetencySnapshot snapshot = rows.isEmpty() ? new CompetencySnapshot(0, 0, 0, null, 0, 0, 0, null, 0, 0, 0) : rows.get(0);
|
||||
int trainingMinutes = snapshot.trainingMinutes();
|
||||
int contribution = Math.min(100, Math.max(0, snapshot.incentivePoints()));
|
||||
int assessmentScore = snapshot.score();
|
||||
@@ -1604,14 +1662,14 @@ public class AihrPracticeSeedService {
|
||||
assessmentScore,
|
||||
aiLevel,
|
||||
List.of(
|
||||
new DimensionResponse("任务完成度", null,
|
||||
"权重40%;需接入工单闭环率、首解率与处理结果,当前待采集"),
|
||||
new DimensionResponse("任务完成度", snapshot.completed() == 0 ? null : firstNonNull(snapshot.taskCompletion(), snapshot.compliance()),
|
||||
"权重40%;当前以SOP执行和问题推进结果代理,工单闭环率/首解率待接入"),
|
||||
new DimensionResponse("话术规范性", snapshot.completed() == 0 ? null : average(snapshot.compliance(), snapshot.communication()),
|
||||
"权重25%;当前以合规+沟通训练维度代理,正式绩效口径需HR确认"),
|
||||
new DimensionResponse("情绪管理能力", snapshot.completed() == 0 ? null : snapshot.emotion(),
|
||||
"权重20%;当前以陪练情绪维度代理"),
|
||||
new DimensionResponse("响应时效", null,
|
||||
"权重10%;需接入首响时长、处理周期与跟进频率,当前待采集"),
|
||||
new DimensionResponse("响应时效", snapshot.completed() == 0 ? null : snapshot.responseTimeliness(),
|
||||
"权重10%;当前按陪练回合响应时长统计,工单首响/处理周期待接入"),
|
||||
new DimensionResponse("增值转化潜力", snapshot.completed() == 0 ? null : snapshot.marketing(),
|
||||
"权重5%;当前以陪练营销维度代理,正式口径需HR确认")
|
||||
),
|
||||
@@ -2221,11 +2279,15 @@ public class AihrPracticeSeedService {
|
||||
jdbcTemplate.update("""
|
||||
INSERT INTO aihr_practice_session
|
||||
(tenant_id, session_id, ext_party_id, trainee_name, scenario_id, scenario_name, mode,
|
||||
total_score, dim_compliance, dim_emotion, dim_communication, dim_marketing,
|
||||
total_score, dim_task_completion, dim_response_timeliness, response_latency_ms,
|
||||
dim_compliance, dim_emotion, dim_communication, dim_marketing,
|
||||
mentor_rewrite, ai_comment, summary, dialogue_json, annotations_json, trust_curve, status, started_time, finished_time, create_time, update_time)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
total_score = VALUES(total_score),
|
||||
dim_task_completion = VALUES(dim_task_completion),
|
||||
dim_response_timeliness = VALUES(dim_response_timeliness),
|
||||
response_latency_ms = VALUES(response_latency_ms),
|
||||
dim_compliance = VALUES(dim_compliance),
|
||||
dim_emotion = VALUES(dim_emotion),
|
||||
dim_communication = VALUES(dim_communication),
|
||||
@@ -2248,10 +2310,13 @@ public class AihrPracticeSeedService {
|
||||
scenario.name(),
|
||||
mobile ? "mobile" : "text",
|
||||
result.total(),
|
||||
scoreValue(result.scores(), "合规"),
|
||||
scoreValue(result.scores(), "情绪"),
|
||||
scoreValue(result.scores(), "沟通"),
|
||||
scoreValue(result.scores(), "营销"),
|
||||
result.taskCompletion(),
|
||||
result.responseTimeliness(),
|
||||
result.responseLatencyMs(),
|
||||
result.compliance(),
|
||||
scoreValue(result.scores(), "情绪管理能力"),
|
||||
result.communication(),
|
||||
scoreValue(result.scores(), "增值转化潜力"),
|
||||
result.rewrite(),
|
||||
result.summary(),
|
||||
record.summary(),
|
||||
@@ -2771,17 +2836,27 @@ public class AihrPracticeSeedService {
|
||||
|
||||
private List<DimensionResponse> scoreItems(ResultSet rs) throws SQLException {
|
||||
List<DimensionResponse> items = new ArrayList<>();
|
||||
addScoreItem(items, "合规", rs.getObject("dim_compliance"), "SOP关键点覆盖");
|
||||
addScoreItem(items, "沟通", rs.getObject("dim_communication"), "承诺与表达清晰度");
|
||||
addScoreItem(items, "情绪", rs.getObject("dim_emotion"), "安抚与承接能力");
|
||||
addScoreItem(items, "营销", rs.getObject("dim_marketing"), "增值转化意识");
|
||||
Integer compliance = nullableInt(rs, "dim_compliance");
|
||||
Integer communication = nullableInt(rs, "dim_communication");
|
||||
addScoreItem(items, "任务完成度", firstNonNull(nullableInt(rs, "dim_task_completion"), compliance), "权重40%;当前以SOP执行和问题推进结果代理");
|
||||
addScoreItem(items, "话术规范性", average(compliance, communication), "权重25%;合规与沟通训练维度综合");
|
||||
addScoreItem(items, "情绪管理能力", nullableInt(rs, "dim_emotion"), "权重20%;共情承接与信任恢复");
|
||||
addScoreItem(items, "响应时效", nullableInt(rs, "dim_response_timeliness"), "权重10%;阶段一按陪练回合响应时长统计");
|
||||
addScoreItem(items, "增值转化潜力", nullableInt(rs, "dim_marketing"), "权重5%;增值引导与办理意识");
|
||||
return items;
|
||||
}
|
||||
|
||||
private void addScoreItem(List<DimensionResponse> items, String label, Object value, String note) {
|
||||
if (value instanceof Number number) {
|
||||
items.add(new DimensionResponse(label, number.intValue(), note));
|
||||
}
|
||||
items.add(new DimensionResponse(label, value == null ? "待采集" : value, note));
|
||||
}
|
||||
|
||||
private Integer nullableInt(ResultSet rs, String column) throws SQLException {
|
||||
Object value = rs.getObject(column);
|
||||
return value instanceof Number number ? number.intValue() : null;
|
||||
}
|
||||
|
||||
private Integer firstNonNull(Integer value, Integer fallback) {
|
||||
return value == null ? fallback : value;
|
||||
}
|
||||
|
||||
private ScenarioResponse mapScenarioResponse(ResultSet rs, int rowNum) throws SQLException {
|
||||
@@ -2892,10 +2967,11 @@ public class AihrPracticeSeedService {
|
||||
|
||||
private List<RubricDimensionResponse> defaultRubricDimensions(boolean feeScenario) {
|
||||
return List.of(
|
||||
new RubricDimensionResponse("compliance", "合规", feeScenario ? 0.30 : 0.35, "SOP关键点、权限边界、承诺口径"),
|
||||
new RubricDimensionResponse("communication", "沟通", 0.25, "诉求确认、信息结构、反馈节点"),
|
||||
new RubricDimensionResponse("emotion", "情绪", 0.25, "共情承接、降温、信任恢复"),
|
||||
new RubricDimensionResponse("marketing", "营销", feeScenario ? 0.20 : 0.15, "收费季转化、政策解释、办理引导")
|
||||
new RubricDimensionResponse("task_completion", "任务完成度", 0.40, "问题解决有效性、工单闭环率、首解率、SOP关键点执行率"),
|
||||
new RubricDimensionResponse("standardization", "话术规范性", 0.25, "流程完整性、法规引用准确性、沟通合规、用语专业度"),
|
||||
new RubricDimensionResponse("emotion", "情绪管理能力", 0.20, "共情、语速语气控制、业主情绪引导安抚有效性"),
|
||||
new RubricDimensionResponse("response_timeliness", "响应时效", 0.10, "首响时长、处理周期、跟进频率"),
|
||||
new RubricDimensionResponse("marketing", "增值转化潜力", 0.05, "活动引导、产品推介时机与成功率")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2969,6 +3045,78 @@ public class AihrPracticeSeedService {
|
||||
}
|
||||
}
|
||||
|
||||
private void rememberResponseLatency(ActiveSession session, int roundIndex, long submittedAt) {
|
||||
if (session == null) {
|
||||
return;
|
||||
}
|
||||
List<Long> promptTimes = session.promptPresentedAtMillis();
|
||||
List<Long> latencies = session.responseLatenciesMs();
|
||||
synchronized (promptTimes) {
|
||||
if (roundIndex < 0 || roundIndex >= promptTimes.size() || promptTimes.get(roundIndex) == null) {
|
||||
return;
|
||||
}
|
||||
long latency = Math.max(0L, submittedAt - promptTimes.get(roundIndex));
|
||||
synchronized (latencies) {
|
||||
while (latencies.size() <= roundIndex) {
|
||||
latencies.add(null);
|
||||
}
|
||||
latencies.set(roundIndex, latency);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void rememberPromptPresentedAt(ActiveSession session, int roundIndex, long presentedAt) {
|
||||
if (session == null || roundIndex < 0) {
|
||||
return;
|
||||
}
|
||||
List<Long> promptTimes = session.promptPresentedAtMillis();
|
||||
synchronized (promptTimes) {
|
||||
while (promptTimes.size() <= roundIndex) {
|
||||
promptTimes.add(null);
|
||||
}
|
||||
promptTimes.set(roundIndex, presentedAt);
|
||||
}
|
||||
}
|
||||
|
||||
private Integer averageResponseLatencyMs(ActiveSession session) {
|
||||
if (session == null) {
|
||||
return null;
|
||||
}
|
||||
List<Long> latencies = snapshotLong(session.responseLatenciesMs());
|
||||
long total = 0L;
|
||||
int count = 0;
|
||||
for (Long latency : latencies) {
|
||||
if (latency != null && latency >= 0) {
|
||||
total += latency;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
if (count == 0) {
|
||||
return null;
|
||||
}
|
||||
return (int) Math.min(Integer.MAX_VALUE, Math.round(total / (double) count));
|
||||
}
|
||||
|
||||
private Integer responseTimelinessScore(ActiveSession session) {
|
||||
Integer latency = averageResponseLatencyMs(session);
|
||||
if (latency == null) {
|
||||
return null;
|
||||
}
|
||||
if (latency <= RESPONSE_FAST_MS) {
|
||||
return 100;
|
||||
}
|
||||
if (latency <= RESPONSE_ACCEPTABLE_MS) {
|
||||
return 85;
|
||||
}
|
||||
if (latency <= RESPONSE_SLOW_MS) {
|
||||
return 70;
|
||||
}
|
||||
if (latency <= RESPONSE_VERY_SLOW_MS) {
|
||||
return 55;
|
||||
}
|
||||
return 40;
|
||||
}
|
||||
|
||||
private void rememberTurnEvidence(ActiveSession session, int roundIndex, Integer emotion, Integer trust, boolean redFlag, String coachHint) {
|
||||
if (session == null) {
|
||||
return;
|
||||
@@ -3163,18 +3311,24 @@ public class AihrPracticeSeedService {
|
||||
private String calibrationDimensionsJson(ScoreSnapshot score, CalibrationRequest request) {
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("original", Map.of(
|
||||
"total", score.total(),
|
||||
"compliance", score.compliance(),
|
||||
"emotion", score.emotion(),
|
||||
"communication", score.communication(),
|
||||
"marketing", score.marketing()
|
||||
"total", firstNonNull(score.total(), 0),
|
||||
"taskCompletion", firstNonNull(firstNonNull(score.taskCompletion(), score.compliance()), 0),
|
||||
"standardization", firstNonNull(average(score.compliance(), score.communication()), 0),
|
||||
"emotion", firstNonNull(score.emotion(), 0),
|
||||
"responseTimeliness", firstNonNull(score.responseTimeliness(), 0),
|
||||
"conversion", firstNonNull(score.marketing(), 0)
|
||||
));
|
||||
Map<String, Object> corrected = new LinkedHashMap<>();
|
||||
corrected.put("total", request == null || request.correctedTotal() == null ? score.total() : clampScore(request.correctedTotal()));
|
||||
corrected.put("compliance", request == null || request.correctedCompliance() == null ? score.compliance() : clampScore(request.correctedCompliance()));
|
||||
corrected.put("emotion", request == null || request.correctedEmotion() == null ? score.emotion() : clampScore(request.correctedEmotion()));
|
||||
corrected.put("communication", request == null || request.correctedCommunication() == null ? score.communication() : clampScore(request.correctedCommunication()));
|
||||
corrected.put("marketing", request == null || request.correctedMarketing() == null ? score.marketing() : clampScore(request.correctedMarketing()));
|
||||
corrected.put("total", request == null || request.correctedTotal() == null ? firstNonNull(score.total(), 0) : clampScore(request.correctedTotal()));
|
||||
corrected.put("taskCompletion", request == null || request.correctedTaskCompletion() == null
|
||||
? firstNonNull(firstNonNull(score.taskCompletion(), score.compliance()), 0) : clampScore(request.correctedTaskCompletion()));
|
||||
corrected.put("standardization", request == null || (request.correctedCompliance() == null && request.correctedCommunication() == null)
|
||||
? firstNonNull(average(score.compliance(), score.communication()), 0)
|
||||
: firstNonNull(average(firstNonNull(request.correctedCompliance(), score.compliance()), firstNonNull(request.correctedCommunication(), score.communication())), 0));
|
||||
corrected.put("emotion", request == null || request.correctedEmotion() == null ? firstNonNull(score.emotion(), 0) : clampScore(request.correctedEmotion()));
|
||||
corrected.put("responseTimeliness", request == null || request.correctedResponseTimeliness() == null
|
||||
? firstNonNull(score.responseTimeliness(), 0) : clampScore(request.correctedResponseTimeliness()));
|
||||
corrected.put("conversion", request == null || request.correctedMarketing() == null ? firstNonNull(score.marketing(), 0) : clampScore(request.correctedMarketing()));
|
||||
data.put("corrected", corrected);
|
||||
return writeJson(data);
|
||||
}
|
||||
@@ -3321,6 +3475,11 @@ public class AihrPracticeSeedService {
|
||||
return null;
|
||||
}
|
||||
|
||||
private int scoreValue(List<DimensionResponse> scores, String label, int fallback) {
|
||||
Integer value = scoreValue(scores, label);
|
||||
return value == null ? fallback : value;
|
||||
}
|
||||
|
||||
private int normalizeLimit(int limit) {
|
||||
if (limit <= 0) {
|
||||
return 5;
|
||||
@@ -3355,6 +3514,9 @@ public class AihrPracticeSeedService {
|
||||
`scenario_name` varchar(100) DEFAULT NULL COMMENT '场景名称',
|
||||
`mode` varchar(30) DEFAULT 'text' COMMENT '训练模式',
|
||||
`total_score` int DEFAULT NULL COMMENT '总分',
|
||||
`dim_task_completion` int DEFAULT NULL COMMENT '任务完成度分',
|
||||
`dim_response_timeliness` int DEFAULT NULL COMMENT '响应时效分',
|
||||
`response_latency_ms` int DEFAULT NULL COMMENT '平均陪练响应时长毫秒',
|
||||
`dim_compliance` int DEFAULT NULL COMMENT '合规分',
|
||||
`dim_emotion` int DEFAULT NULL COMMENT '情绪分',
|
||||
`dim_communication` int DEFAULT NULL COMMENT '沟通分',
|
||||
@@ -3380,6 +3542,9 @@ public class AihrPracticeSeedService {
|
||||
KEY `idx_aihr_practice_review` (`tenant_id`, `mode`, `status`, `finished_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR 对练记录';
|
||||
""");
|
||||
ensurePracticeColumn("dim_task_completion", "ALTER TABLE aihr_practice_session ADD COLUMN `dim_task_completion` int DEFAULT NULL COMMENT '任务完成度分' AFTER `total_score`");
|
||||
ensurePracticeColumn("dim_response_timeliness", "ALTER TABLE aihr_practice_session ADD COLUMN `dim_response_timeliness` int DEFAULT NULL COMMENT '响应时效分' AFTER `dim_task_completion`");
|
||||
ensurePracticeColumn("response_latency_ms", "ALTER TABLE aihr_practice_session ADD COLUMN `response_latency_ms` int DEFAULT NULL COMMENT '平均陪练响应时长毫秒' AFTER `dim_response_timeliness`");
|
||||
ensurePracticeColumn("dialogue_json", "ALTER TABLE aihr_practice_session ADD COLUMN `dialogue_json` text DEFAULT NULL COMMENT '对练话术JSON' AFTER `summary`");
|
||||
ensurePracticeColumn("annotations_json", "ALTER TABLE aihr_practice_session ADD COLUMN `annotations_json` text DEFAULT NULL COMMENT '逐句标注JSON' AFTER `dialogue_json`");
|
||||
ensurePracticeColumn("review_advice", "ALTER TABLE aihr_practice_session ADD COLUMN `review_advice` varchar(1000) DEFAULT NULL COMMENT '主管复盘建议' AFTER `ai_comment`");
|
||||
@@ -3907,7 +4072,9 @@ public class AihrPracticeSeedService {
|
||||
return Math.round((left + right) / 2.0f);
|
||||
}
|
||||
|
||||
private record CompetencySnapshot(Integer completed, Integer score, Integer pendingReview, Integer compliance, Integer communication, Integer emotion, Integer marketing, Integer incentivePoints, Integer trainingMinutes) {
|
||||
private record CompetencySnapshot(Integer completed, Integer score, Integer pendingReview, Integer taskCompletion,
|
||||
Integer compliance, Integer communication, Integer emotion, Integer responseTimeliness,
|
||||
Integer marketing, Integer incentivePoints, Integer trainingMinutes) {
|
||||
}
|
||||
|
||||
private record EvidenceScoreSnapshot(Integer aiScore, Integer humanScore, Integer finalScore, Integer calibrationCount) {
|
||||
@@ -3916,7 +4083,8 @@ public class AihrPracticeSeedService {
|
||||
private record SessionAnnotationSource(String scenarioId, String dialogueJson, String annotationsJson) {
|
||||
}
|
||||
|
||||
private record ScoreSnapshot(Integer total, Integer compliance, Integer emotion, Integer communication, Integer marketing) {
|
||||
private record ScoreSnapshot(Integer total, Integer taskCompletion, Integer responseTimeliness,
|
||||
Integer compliance, Integer emotion, Integer communication, Integer marketing) {
|
||||
}
|
||||
|
||||
private record CalibrationPair(int original, int corrected) {
|
||||
@@ -3972,9 +4140,12 @@ public class AihrPracticeSeedService {
|
||||
|
||||
private record ActiveSession(String scenarioId, String trainee, String extPartyId, Long assignmentId, boolean mobile, LocalDateTime startedAt,
|
||||
List<String> traineeReplies, List<String> traineeAudioUrls, List<Long> traineeAudioOssIds,
|
||||
List<String> customerLines, List<TurnEvidence> turnEvidence) {
|
||||
List<String> customerLines, List<TurnEvidence> turnEvidence,
|
||||
List<Long> promptPresentedAtMillis, List<Long> responseLatenciesMs) {
|
||||
}
|
||||
|
||||
private record PracticeResult(Integer total, List<DimensionResponse> scores, String rewrite, String summary) {
|
||||
private record PracticeResult(Integer total, List<DimensionResponse> scores, String rewrite, String summary,
|
||||
Integer compliance, Integer communication, Integer taskCompletion,
|
||||
Integer responseTimeliness, Integer responseLatencyMs) {
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -756,7 +756,7 @@ public class AihrPracticeSeedServiceTest {
|
||||
assertEquals(81, profile.dimensions().get(1).value());
|
||||
assertEquals(78, profile.dimensions().get(2).value());
|
||||
assertEquals(74, profile.dimensions().get(4).value());
|
||||
assertNull(profile.dimensions().get(0).value());
|
||||
assertEquals(82, profile.dimensions().get(0).value());
|
||||
assertNull(profile.dimensions().get(3).value());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user