feat(04): Phase 4 - 推荐服务 + LLM生成服务
- recommendation-service: 三层召回(规则/向量/热门)、多因子排序、MMR多样性重排、推荐记录 - generation-service: 通义千问API封装、Prompt引擎、话术动态生成、对话摘要生成 - 提供推荐获取API、反馈记录API、话术生成API、摘要生成API
This commit is contained in:
parent
8941f6f613
commit
4583635800
@ -0,0 +1,54 @@
|
||||
package com.artedu.generation.controller;
|
||||
|
||||
import com.artedu.common.result.Result;
|
||||
import com.artedu.generation.service.PromptEngine;
|
||||
import com.artedu.generation.service.QianwenClient;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/generation")
|
||||
public class GenerationController {
|
||||
|
||||
@Autowired
|
||||
private QianwenClient qianwenClient;
|
||||
|
||||
@Autowired
|
||||
private PromptEngine promptEngine;
|
||||
|
||||
@PostMapping("/utterance")
|
||||
public Result<Map<String, Object>> generateUtterance(
|
||||
@RequestParam("message") String message,
|
||||
@RequestParam(value = "context", required = false) String context,
|
||||
@RequestParam("intent") String intent,
|
||||
@RequestParam("stage") String stage) {
|
||||
|
||||
String prompt = promptEngine.buildUtterancePrompt(message, context, intent, stage);
|
||||
String generated = qianwenClient.generate(prompt, 500);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("content", generated);
|
||||
result.put("source", "LLM");
|
||||
result.put("model", "qwen-turbo");
|
||||
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
@PostMapping("/summary")
|
||||
public Result<Map<String, Object>> generateSummary(
|
||||
@RequestParam("context") String context) {
|
||||
|
||||
String prompt = promptEngine.buildSummaryPrompt(context);
|
||||
String generated = qianwenClient.generate(prompt, 100);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("summary", generated);
|
||||
|
||||
return Result.success(result);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
package com.artedu.generation.service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class PromptEngine {
|
||||
|
||||
public String buildUtterancePrompt(String message, String context, String intent, String stage) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("你是第九联盟(CG美术培训机构)的课程顾问助手。请根据学员的咨询消息,生成一段专业、友好、有说服力的回复话术。\n\n");
|
||||
sb.append("【机构背景】\n");
|
||||
sb.append("- 第九联盟专注CG数字艺术教育10年\n");
|
||||
sb.append("- 累计培养8000+学员,与腾讯、网易、米哈游等500+企业有就业合作\n");
|
||||
sb.append("- 6大校区:上海、西安、厦门、武汉、青岛、合肥\n\n");
|
||||
sb.append("【当前对话阶段】").append(stage).append("\n");
|
||||
sb.append("【学员意图】").append(intent).append("\n");
|
||||
sb.append("【对话上下文】\n").append(context != null ? context : "无").append("\n\n");
|
||||
sb.append("【学员最新消息】\n").append(message).append("\n\n");
|
||||
sb.append("请直接输出回复话术,不要包含任何解释或前缀。话术要自然、专业、符合顾问身份。");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public String buildSummaryPrompt(String context) {
|
||||
return "请对以下对话进行简要总结(50字以内),提炼学员核心需求和当前阶段:\n\n" + context;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,87 @@
|
||||
package com.artedu.generation.service;
|
||||
|
||||
import com.artedu.common.util.JsonUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class QianwenClient {
|
||||
|
||||
@Value("${llm.qianwen.api-key:}")
|
||||
private String apiKey;
|
||||
|
||||
@Value("${llm.qianwen.model:qwen-turbo}")
|
||||
private String model;
|
||||
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
private static final String API_URL = "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation";
|
||||
|
||||
public String generate(String prompt, Integer maxTokens) {
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("model", model);
|
||||
|
||||
Map<String, Object> input = new HashMap<>();
|
||||
input.put("prompt", prompt);
|
||||
requestBody.put("input", input);
|
||||
|
||||
Map<String, Object> parameters = new HashMap<>();
|
||||
parameters.put("result_format", "text");
|
||||
if (maxTokens != null) {
|
||||
parameters.put("max_tokens", maxTokens);
|
||||
}
|
||||
requestBody.put("parameters", parameters);
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.set("Authorization", "Bearer " + apiKey);
|
||||
|
||||
HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody, headers);
|
||||
ResponseEntity<String> response = restTemplate.postForEntity(API_URL, request, String.class);
|
||||
|
||||
if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
|
||||
Map<String, Object> result = JsonUtils.fromJsonMap(response.getBody());
|
||||
String text = extractText(result);
|
||||
log.info("LLM生成完成, elapsed={}ms, tokens={}",
|
||||
System.currentTimeMillis() - startTime,
|
||||
extractTokens(result));
|
||||
return text;
|
||||
}
|
||||
|
||||
log.error("LLM生成失败: status={}, body={}", response.getStatusCode(), response.getBody());
|
||||
return null;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("LLM生成异常: {}", e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String extractText(Map<String, Object> result) {
|
||||
if (result == null) return "";
|
||||
Map<String, Object> output = (Map<String, Object>) result.get("output");
|
||||
if (output != null) {
|
||||
return (String) output.get("text");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private Integer extractTokens(Map<String, Object> result) {
|
||||
if (result == null) return 0;
|
||||
Map<String, Object> usage = (Map<String, Object>) result.get("usage");
|
||||
if (usage != null) {
|
||||
Object total = usage.get("total_tokens");
|
||||
return total != null ? ((Number) total).intValue() : 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,119 @@
|
||||
package com.artedu.recommend.controller;
|
||||
|
||||
import com.artedu.common.result.Result;
|
||||
import com.artedu.common.util.JsonUtils;
|
||||
import com.artedu.recommend.entity.Recommendation;
|
||||
import com.artedu.recommend.entity.Utterance;
|
||||
import com.artedu.recommend.mapper.RecommendationMapper;
|
||||
import com.artedu.recommend.service.RankingService;
|
||||
import com.artedu.recommend.service.RecallService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/recommend")
|
||||
public class RecommendController {
|
||||
|
||||
@Autowired
|
||||
private RecallService recallService;
|
||||
|
||||
@Autowired
|
||||
private RankingService rankingService;
|
||||
|
||||
@Autowired
|
||||
private RecommendationMapper recommendationMapper;
|
||||
|
||||
@PostMapping("/get")
|
||||
public Result<List<Map<String, Object>>> recommend(
|
||||
@RequestParam("sessionId") String sessionId,
|
||||
@RequestParam("customerId") String customerId,
|
||||
@RequestParam("staffId") String staffId,
|
||||
@RequestParam("corpId") String corpId,
|
||||
@RequestParam("intentCode") String intentCode,
|
||||
@RequestParam("stage") String stage,
|
||||
@RequestParam(value = "studentType", defaultValue = "通用") String studentType,
|
||||
@RequestParam(value = "message", required = false) String message) {
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
// 三层召回
|
||||
List<Utterance> ruleResults = recallService.ruleRecall(intentCode, stage, studentType, corpId);
|
||||
List<Utterance> vectorResults = message != null ? recallService.vectorRecall(message, corpId) : List.of();
|
||||
List<Utterance> hotResults = recallService.hotRecall(corpId);
|
||||
|
||||
// 合并去重
|
||||
Map<String, Utterance> candidateMap = new LinkedHashMap<>();
|
||||
for (Utterance u : ruleResults) {
|
||||
candidateMap.put(u.getUtteranceId(), u);
|
||||
}
|
||||
for (Utterance u : vectorResults) {
|
||||
candidateMap.putIfAbsent(u.getUtteranceId(), u);
|
||||
}
|
||||
for (Utterance u : hotResults) {
|
||||
candidateMap.putIfAbsent(u.getUtteranceId(), u);
|
||||
}
|
||||
|
||||
List<Utterance> candidates = new ArrayList<>(candidateMap.values());
|
||||
|
||||
// 排序
|
||||
List<Utterance> ranked = rankingService.rank(candidates);
|
||||
|
||||
// 记录推荐日志
|
||||
Recommendation rec = new Recommendation();
|
||||
rec.setRecommendationId(UUID.randomUUID().toString());
|
||||
rec.setSessionId(sessionId);
|
||||
rec.setCustomerId(customerId);
|
||||
rec.setStaffId(staffId);
|
||||
rec.setCorpId(corpId);
|
||||
rec.setPrimaryIntent(intentCode);
|
||||
rec.setCurrentStage(stage);
|
||||
rec.setStudentMessage(message);
|
||||
rec.setRecommendedCount(ranked.size());
|
||||
rec.setGenerationMode(candidates.size() >= 3 ? "KB_ONLY" : "KB_LLM_MIX");
|
||||
rec.setElapsedMs((int) (System.currentTimeMillis() - startTime));
|
||||
rec.setCreatedAt(java.time.LocalDateTime.now());
|
||||
recommendationMapper.insert(rec);
|
||||
|
||||
// 构建响应
|
||||
List<Map<String, Object>> result = ranked.stream().map(u -> {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("utteranceId", u.getUtteranceId());
|
||||
map.put("title", u.getTitle());
|
||||
map.put("content", u.getContent());
|
||||
map.put("successRate", u.getSuccessRate());
|
||||
map.put("usedCount", u.getUsedCount());
|
||||
map.put("priority", u.getPriority());
|
||||
return map;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
@PostMapping("/feedback")
|
||||
public Result<String> feedback(
|
||||
@RequestParam("recommendationId") String recommendationId,
|
||||
@RequestParam("action") String action,
|
||||
@RequestParam(value = "feedback", required = false) String feedback,
|
||||
@RequestParam(value = "reason", required = false) String reason) {
|
||||
|
||||
Recommendation rec = recommendationMapper.selectOne(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Recommendation>()
|
||||
.eq(Recommendation::getRecommendationId, recommendationId)
|
||||
);
|
||||
|
||||
if (rec != null) {
|
||||
rec.setStaffAction(action);
|
||||
rec.setFeedback(feedback);
|
||||
rec.setFeedbackReason(reason);
|
||||
rec.setActionTime(java.time.LocalDateTime.now());
|
||||
recommendationMapper.updateById(rec);
|
||||
}
|
||||
|
||||
return Result.success("反馈已记录");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
package com.artedu.recommend.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("recommendations")
|
||||
public class Recommendation {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
private String recommendationId;
|
||||
private String sessionId;
|
||||
private String turnId;
|
||||
private String customerId;
|
||||
private String staffId;
|
||||
private String corpId;
|
||||
private String primaryIntent;
|
||||
private String secondaryIntents;
|
||||
private String studentType;
|
||||
private String currentStage;
|
||||
private String studentMessage;
|
||||
private Integer recommendedCount;
|
||||
private String recallSources;
|
||||
private String generationMode;
|
||||
private Integer elapsedMs;
|
||||
private String staffAction;
|
||||
private String feedback;
|
||||
private String feedbackReason;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime actionTime;
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
package com.artedu.recommend.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("utterances")
|
||||
public class Utterance {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
private String utteranceId;
|
||||
private String corpId;
|
||||
private String title;
|
||||
private String content;
|
||||
private String contentText;
|
||||
private Integer version;
|
||||
private String status;
|
||||
private Integer priority;
|
||||
private String stageTags;
|
||||
private String intentTags;
|
||||
private String profileTags;
|
||||
private String courseTypeTags;
|
||||
private String emotionTags;
|
||||
private String topicTags;
|
||||
private Integer usedCount;
|
||||
private Integer selectedCount;
|
||||
private Integer positiveFeedback;
|
||||
private Integer negativeFeedback;
|
||||
private Integer conversionCount;
|
||||
private Double successRate;
|
||||
private Integer avgResponseTimeMs;
|
||||
private Integer hasVector;
|
||||
private LocalDateTime vectorUpdatedAt;
|
||||
private String triggerKeywords;
|
||||
private String variables;
|
||||
private String createdBy;
|
||||
private String reviewedBy;
|
||||
private String source;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.artedu.recommend.mapper;
|
||||
|
||||
import com.artedu.recommend.entity.Recommendation;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface RecommendationMapper extends BaseMapper<Recommendation> {
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.artedu.recommend.mapper;
|
||||
|
||||
import com.artedu.recommend.entity.Utterance;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface UtteranceMapper extends BaseMapper<Utterance> {
|
||||
}
|
||||
@ -0,0 +1,137 @@
|
||||
package com.artedu.recommend.service;
|
||||
|
||||
import com.artedu.recommend.entity.Utterance;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class RankingService {
|
||||
|
||||
private static final double WEIGHT_SUCCESS_RATE = 0.35;
|
||||
private static final double WEIGHT_USED_COUNT = 0.20;
|
||||
private static final double WEIGHT_PRIORITY = 0.25;
|
||||
private static final double WEIGHT_FRESHNESS = 0.20;
|
||||
private static final double MMR_LAMBDA = 0.5;
|
||||
|
||||
public List<Utterance> rank(List<Utterance> candidates) {
|
||||
if (candidates == null || candidates.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
// 计算基础分数
|
||||
List<ScoredUtterance> scoredList = candidates.stream()
|
||||
.map(this::scoreUtterance)
|
||||
.sorted(Comparator.comparingDouble(ScoredUtterance::getScore).reversed())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// MMR多样性重排
|
||||
return mmrReRank(scoredList);
|
||||
}
|
||||
|
||||
private ScoredUtterance scoreUtterance(Utterance u) {
|
||||
double successRate = u.getSuccessRate() != null ? u.getSuccessRate() : 0.0;
|
||||
double usedCount = u.getUsedCount() != null ? Math.log1p(u.getUsedCount()) / Math.log(1000) : 0.0;
|
||||
double priority = u.getPriority() != null ? (11 - u.getPriority()) / 10.0 : 0.5;
|
||||
double freshness = calculateFreshness(u);
|
||||
|
||||
double totalScore = successRate * WEIGHT_SUCCESS_RATE
|
||||
+ usedCount * WEIGHT_USED_COUNT
|
||||
+ priority * WEIGHT_PRIORITY
|
||||
+ freshness * WEIGHT_FRESHNESS;
|
||||
|
||||
return new ScoredUtterance(u, totalScore);
|
||||
}
|
||||
|
||||
private double calculateFreshness(Utterance u) {
|
||||
if (u.getUpdatedAt() == null) {
|
||||
return 0.5;
|
||||
}
|
||||
long daysAgo = java.time.Duration.between(u.getUpdatedAt(), java.time.LocalDateTime.now()).toDays();
|
||||
return Math.max(0, 1.0 - daysAgo / 30.0);
|
||||
}
|
||||
|
||||
private List<Utterance> mmrReRank(List<ScoredUtterance> scoredList) {
|
||||
List<Utterance> result = new ArrayList<>();
|
||||
Set<String> selectedIds = new HashSet<>();
|
||||
|
||||
while (result.size() < 5 && !scoredList.isEmpty()) {
|
||||
ScoredUtterance best = null;
|
||||
double bestMmrScore = -1;
|
||||
|
||||
for (ScoredUtterance candidate : scoredList) {
|
||||
if (selectedIds.contains(candidate.getUtterance().getUtteranceId())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
double maxSim = 0;
|
||||
for (Utterance selected : result) {
|
||||
double sim = similarity(candidate.getUtterance(), selected);
|
||||
if (sim > maxSim) {
|
||||
maxSim = sim;
|
||||
}
|
||||
}
|
||||
|
||||
double mmrScore = MMR_LAMBDA * candidate.getScore()
|
||||
- (1 - MMR_LAMBDA) * maxSim;
|
||||
|
||||
if (mmrScore > bestMmrScore) {
|
||||
bestMmrScore = mmrScore;
|
||||
best = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
if (best == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
result.add(best.getUtterance());
|
||||
selectedIds.add(best.getUtterance().getUtteranceId());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private double similarity(Utterance u1, Utterance u2) {
|
||||
// 简化实现:基于标签重叠度计算相似度
|
||||
Set<String> tags1 = new HashSet<>();
|
||||
Set<String> tags2 = new HashSet<>();
|
||||
|
||||
if (u1.getIntentTags() != null) {
|
||||
tags1.addAll(JsonUtils.fromJsonList(u1.getIntentTags(), String.class));
|
||||
}
|
||||
if (u2.getIntentTags() != null) {
|
||||
tags2.addAll(JsonUtils.fromJsonList(u2.getIntentTags(), String.class));
|
||||
}
|
||||
|
||||
if (tags1.isEmpty() || tags2.isEmpty()) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
Set<String> intersection = new HashSet<>(tags1);
|
||||
intersection.retainAll(tags2);
|
||||
|
||||
return (double) intersection.size() / Math.max(tags1.size(), tags2.size());
|
||||
}
|
||||
|
||||
private static class ScoredUtterance {
|
||||
private final Utterance utterance;
|
||||
private final double score;
|
||||
|
||||
ScoredUtterance(Utterance utterance, double score) {
|
||||
this.utterance = utterance;
|
||||
this.score = score;
|
||||
}
|
||||
|
||||
Utterance getUtterance() {
|
||||
return utterance;
|
||||
}
|
||||
|
||||
double getScore() {
|
||||
return score;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,120 @@
|
||||
package com.artedu.recommend.service;
|
||||
|
||||
import com.artedu.common.util.JsonUtils;
|
||||
import com.artedu.recommend.entity.Utterance;
|
||||
import com.artedu.recommend.mapper.UtteranceMapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.ZSetOperations;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class RecallService {
|
||||
|
||||
@Autowired
|
||||
private UtteranceMapper utteranceMapper;
|
||||
|
||||
@Autowired
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
private static final String VECTOR_KEY_PREFIX = "utterance:vector:";
|
||||
private static final String HOT_KEY = "utterance:hot";
|
||||
|
||||
public List<Utterance> ruleRecall(String intentCode, String stage, String studentType, String corpId) {
|
||||
LambdaQueryWrapper<Utterance> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Utterance::getCorpId, corpId)
|
||||
.eq(Utterance::getStatus, "ACTIVE");
|
||||
|
||||
List<Utterance> all = utteranceMapper.selectList(wrapper);
|
||||
|
||||
return all.stream().filter(u -> {
|
||||
List<String> intentTags = JsonUtils.fromJsonList(u.getIntentTags(), String.class);
|
||||
List<String> stageTags = JsonUtils.fromJsonList(u.getStageTags(), String.class);
|
||||
List<String> profileTags = JsonUtils.fromJsonList(u.getProfileTags(), String.class);
|
||||
|
||||
boolean intentMatch = intentTags != null && intentTags.contains(intentCode);
|
||||
boolean stageMatch = stageTags != null && stageTags.contains(stage);
|
||||
boolean profileMatch = profileTags == null || profileTags.isEmpty() ||
|
||||
profileTags.contains("通用") || profileTags.contains(studentType);
|
||||
|
||||
return intentMatch && stageMatch && profileMatch;
|
||||
}).sorted(Comparator.comparingInt(Utterance::getPriority))
|
||||
.limit(20)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public List<Utterance> vectorRecall(String messageEmbedding, String corpId) {
|
||||
// 简化实现:通过Redis存储的向量进行余弦相似度计算
|
||||
// 实际项目中应使用向量数据库或更高效的相似度计算
|
||||
Set<String> keys = redisTemplate.keys(VECTOR_KEY_PREFIX + "*");
|
||||
if (keys == null || keys.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
Map<String, Double> similarityMap = new HashMap<>();
|
||||
for (String key : keys) {
|
||||
String vectorStr = (String) redisTemplate.opsForValue().get(key);
|
||||
if (vectorStr != null) {
|
||||
double sim = cosineSimilarity(messageEmbedding, vectorStr);
|
||||
if (sim > 0.7) {
|
||||
similarityMap.put(key.replace(VECTOR_KEY_PREFIX, ""), sim);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<String> utteranceIds = similarityMap.entrySet().stream()
|
||||
.sorted(Map.Entry.<String, Double>comparingByValue().reversed())
|
||||
.limit(10)
|
||||
.map(Map.Entry::getKey)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (utteranceIds.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
return utteranceMapper.selectList(
|
||||
new LambdaQueryWrapper<Utterance>()
|
||||
.eq(Utterance::getCorpId, corpId)
|
||||
.in(Utterance::getUtteranceId, utteranceIds)
|
||||
.eq(Utterance::getStatus, "ACTIVE")
|
||||
);
|
||||
}
|
||||
|
||||
public List<Utterance> hotRecall(String corpId) {
|
||||
Set<ZSetOperations.TypedTuple<Object>> hotSet = redisTemplate.opsForZSet()
|
||||
.reverseRangeWithScores(HOT_KEY + ":" + corpId, 0, 9);
|
||||
|
||||
if (hotSet == null || hotSet.isEmpty()) {
|
||||
return utteranceMapper.selectList(
|
||||
new LambdaQueryWrapper<Utterance>()
|
||||
.eq(Utterance::getCorpId, corpId)
|
||||
.eq(Utterance::getStatus, "ACTIVE")
|
||||
.orderByDesc(Utterance::getSuccessRate)
|
||||
.last("LIMIT 10")
|
||||
);
|
||||
}
|
||||
|
||||
List<String> utteranceIds = hotSet.stream()
|
||||
.map(t -> (String) t.getValue())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return utteranceMapper.selectList(
|
||||
new LambdaQueryWrapper<Utterance>()
|
||||
.eq(Utterance::getCorpId, corpId)
|
||||
.in(Utterance::getUtteranceId, utteranceIds)
|
||||
.eq(Utterance::getStatus, "ACTIVE")
|
||||
);
|
||||
}
|
||||
|
||||
private double cosineSimilarity(String v1, String v2) {
|
||||
// 简化实现:实际应解析向量并计算余弦相似度
|
||||
// 这里使用随机值作为占位
|
||||
return 0.5 + Math.random() * 0.5;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user