feat(03): Phase 3 - 对话服务 + 意图服务
- conversation-service: 会话管理、Redis上下文窗口(10轮滑动窗口)、轮次解析、消息事件监听 - intent-service: 40意图分类、规则匹配+LLM混合识别、学员画像构建(5维度)、槽位填充 - LLMIntentClient: 通义千问API封装、Prompt构建、结果解析 - 提供意图识别API、学员画像查询API
This commit is contained in:
parent
48c1dd8452
commit
8941f6f613
@ -0,0 +1,50 @@
|
|||||||
|
package com.artedu.conversation.controller;
|
||||||
|
|
||||||
|
import com.artedu.common.result.Result;
|
||||||
|
import com.artedu.conversation.entity.Conversation;
|
||||||
|
import com.artedu.conversation.service.ConversationManager;
|
||||||
|
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/conversations")
|
||||||
|
public class ConversationController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ConversationManager conversationManager;
|
||||||
|
|
||||||
|
@GetMapping("/{sessionId}/context")
|
||||||
|
public Result<Map<String, Object>> getContext(@PathVariable("sessionId") String sessionId) {
|
||||||
|
String contextStr = conversationManager.getContextString(sessionId);
|
||||||
|
var messages = conversationManager.getContextMessages(sessionId);
|
||||||
|
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("sessionId", sessionId);
|
||||||
|
result.put("context", contextStr);
|
||||||
|
result.put("messageCount", messages.size());
|
||||||
|
result.put("messages", messages);
|
||||||
|
|
||||||
|
return Result.success(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{sessionId}")
|
||||||
|
public Result<Conversation> getConversation(@PathVariable("sessionId") String sessionId) {
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{sessionId}/status")
|
||||||
|
public Result<String> updateStatus(
|
||||||
|
@PathVariable("sessionId") String sessionId,
|
||||||
|
@RequestParam("stage") String stage,
|
||||||
|
@RequestParam(value = "confidence", required = false) Double confidence,
|
||||||
|
@RequestParam(value = "summary", required = false) String summary) {
|
||||||
|
|
||||||
|
conversationManager.updateConversationStatus(sessionId, stage, confidence, summary);
|
||||||
|
return Result.success("状态更新成功");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
package com.artedu.conversation.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("conversations")
|
||||||
|
public class Conversation {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String sessionId;
|
||||||
|
private String corpId;
|
||||||
|
private String customerId;
|
||||||
|
private String customerName;
|
||||||
|
private String staffId;
|
||||||
|
private String staffName;
|
||||||
|
private String roomId;
|
||||||
|
private String status;
|
||||||
|
private String currentStage;
|
||||||
|
private Double stageConfidence;
|
||||||
|
private Integer roundCount;
|
||||||
|
private Integer intentCount;
|
||||||
|
private String lastMsgId;
|
||||||
|
private LocalDateTime lastMsgTime;
|
||||||
|
private String lastMsgContent;
|
||||||
|
private LocalDateTime startTime;
|
||||||
|
private LocalDateTime endTime;
|
||||||
|
private Integer idleDuration;
|
||||||
|
private String contextSummary;
|
||||||
|
private String keyEvents;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
package com.artedu.conversation.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("conversation_turns")
|
||||||
|
public class ConversationTurn {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String turnId;
|
||||||
|
private String sessionId;
|
||||||
|
private Integer turnNumber;
|
||||||
|
private String studentMsgId;
|
||||||
|
private String studentContent;
|
||||||
|
private String seatMsgId;
|
||||||
|
private String seatContent;
|
||||||
|
private String turnType;
|
||||||
|
private String primaryIntent;
|
||||||
|
private Double intentConfidence;
|
||||||
|
private String studentEmotion;
|
||||||
|
private String keyTopics;
|
||||||
|
private String slotChanges;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
package com.artedu.conversation.mapper;
|
||||||
|
|
||||||
|
import com.artedu.conversation.entity.Conversation;
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会话数据访问层
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface ConversationMapper extends BaseMapper<Conversation> {
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
package com.artedu.conversation.mapper;
|
||||||
|
|
||||||
|
import com.artedu.conversation.entity.ConversationTurn;
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对话轮次数据访问层
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface ConversationTurnMapper extends BaseMapper<ConversationTurn> {
|
||||||
|
}
|
||||||
@ -0,0 +1,286 @@
|
|||||||
|
package com.artedu.conversation.service;
|
||||||
|
|
||||||
|
import com.artedu.common.util.JsonUtils;
|
||||||
|
import com.artedu.conversation.entity.Conversation;
|
||||||
|
import com.artedu.conversation.entity.ConversationTurn;
|
||||||
|
import com.artedu.conversation.mapper.ConversationMapper;
|
||||||
|
import com.artedu.conversation.mapper.ConversationTurnMapper;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.amqp.rabbit.annotation.RabbitListener;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class ConversationManager {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ConversationMapper conversationMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ConversationTurnMapper conversationTurnMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RedisTemplate<String, Object> redisTemplate;
|
||||||
|
|
||||||
|
private static final String CONV_CONTEXT_KEY = "conv:context:";
|
||||||
|
private static final int MAX_CONTEXT_ROUNDS = 10;
|
||||||
|
private static final int CONTEXT_EXPIRE_MINUTES = 30;
|
||||||
|
|
||||||
|
public Conversation getOrCreateSession(String sessionId, String corpId,
|
||||||
|
String customerId, String staffId) {
|
||||||
|
Conversation conv = conversationMapper.selectOne(
|
||||||
|
new LambdaQueryWrapper<Conversation>()
|
||||||
|
.eq(Conversation::getSessionId, sessionId)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (conv == null) {
|
||||||
|
conv = new Conversation();
|
||||||
|
conv.setSessionId(sessionId);
|
||||||
|
conv.setCorpId(corpId);
|
||||||
|
conv.setCustomerId(customerId);
|
||||||
|
conv.setStaffId(staffId);
|
||||||
|
conv.setStatus("ACTIVE");
|
||||||
|
conv.setCurrentStage("开场白");
|
||||||
|
conv.setStageConfidence(0.0);
|
||||||
|
conv.setRoundCount(0);
|
||||||
|
conv.setStartTime(LocalDateTime.now());
|
||||||
|
conversationMapper.insert(conv);
|
||||||
|
log.info("创建新会话: sessionId={}", sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return conv;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateConversationStatus(String sessionId, String stage,
|
||||||
|
Double confidence, String summary) {
|
||||||
|
Conversation conv = conversationMapper.selectOne(
|
||||||
|
new LambdaQueryWrapper<Conversation>()
|
||||||
|
.eq(Conversation::getSessionId, sessionId)
|
||||||
|
);
|
||||||
|
if (conv != null) {
|
||||||
|
conv.setCurrentStage(stage);
|
||||||
|
if (confidence != null) {
|
||||||
|
conv.setStageConfidence(confidence);
|
||||||
|
}
|
||||||
|
if (summary != null) {
|
||||||
|
conv.setContextSummary(summary);
|
||||||
|
}
|
||||||
|
conv.setUpdatedAt(LocalDateTime.now());
|
||||||
|
conversationMapper.updateById(conv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addMessageToContext(String sessionId, String role, String content, Long msgTime) {
|
||||||
|
String key = CONV_CONTEXT_KEY + sessionId;
|
||||||
|
ContextMessage message = new ContextMessage(role, content, msgTime);
|
||||||
|
String messageJson = JsonUtils.toJson(message);
|
||||||
|
redisTemplate.opsForList().rightPush(key, messageJson);
|
||||||
|
Long size = redisTemplate.opsForList().size(key);
|
||||||
|
if (size != null && size > MAX_CONTEXT_ROUNDS * 2) {
|
||||||
|
redisTemplate.opsForList().trim(key, -MAX_CONTEXT_ROUNDS * 2, -1);
|
||||||
|
}
|
||||||
|
redisTemplate.expire(key, CONTEXT_EXPIRE_MINUTES, TimeUnit.MINUTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ContextMessage> getContextMessages(String sessionId) {
|
||||||
|
String key = CONV_CONTEXT_KEY + sessionId;
|
||||||
|
List<Object> jsonList = redisTemplate.opsForList().range(key, 0, -1);
|
||||||
|
if (jsonList == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
return jsonList.stream()
|
||||||
|
.map(obj -> JsonUtils.fromJson((String) obj, ContextMessage.class))
|
||||||
|
.filter(msg -> msg != null)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getContextString(String sessionId) {
|
||||||
|
List<ContextMessage> messages = getContextMessages(sessionId);
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (ContextMessage msg : messages) {
|
||||||
|
String roleName = "student".equals(msg.getRole()) ? "学员" : "顾问";
|
||||||
|
sb.append(roleName).append(": ").append(msg.getContent()).append("\n");
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearContext(String sessionId) {
|
||||||
|
String key = CONV_CONTEXT_KEY + sessionId;
|
||||||
|
redisTemplate.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ConversationTurn createTurn(String sessionId, int turnNumber,
|
||||||
|
String studentMsgId, String studentContent,
|
||||||
|
String turnType) {
|
||||||
|
ConversationTurn turn = new ConversationTurn();
|
||||||
|
turn.setTurnId(sessionId + "_" + turnNumber);
|
||||||
|
turn.setSessionId(sessionId);
|
||||||
|
turn.setTurnNumber(turnNumber);
|
||||||
|
turn.setStudentMsgId(studentMsgId);
|
||||||
|
turn.setStudentContent(studentContent);
|
||||||
|
turn.setTurnType(turnType != null ? turnType : "QUERY");
|
||||||
|
turn.setCreatedAt(LocalDateTime.now());
|
||||||
|
conversationTurnMapper.insert(turn);
|
||||||
|
|
||||||
|
Conversation conv = conversationMapper.selectOne(
|
||||||
|
new LambdaQueryWrapper<Conversation>()
|
||||||
|
.eq(Conversation::getSessionId, sessionId)
|
||||||
|
);
|
||||||
|
if (conv != null) {
|
||||||
|
conv.setRoundCount(turnNumber);
|
||||||
|
conv.setUpdatedAt(LocalDateTime.now());
|
||||||
|
conversationMapper.updateById(conv);
|
||||||
|
}
|
||||||
|
|
||||||
|
return turn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateSeatReply(String turnId, String seatMsgId, String seatContent) {
|
||||||
|
ConversationTurn turn = conversationTurnMapper.selectOne(
|
||||||
|
new LambdaQueryWrapper<ConversationTurn>()
|
||||||
|
.eq(ConversationTurn::getTurnId, turnId)
|
||||||
|
);
|
||||||
|
if (turn != null) {
|
||||||
|
turn.setSeatMsgId(seatMsgId);
|
||||||
|
turn.setSeatContent(seatContent);
|
||||||
|
conversationTurnMapper.updateById(turn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RabbitListener(queues = "archive.new_message.queue")
|
||||||
|
public void handleNewMessage(String messageJson) {
|
||||||
|
try {
|
||||||
|
ArchiveMessageEvent event = JsonUtils.fromJson(messageJson, ArchiveMessageEvent.class);
|
||||||
|
if (event == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("收到新消息事件: sessionId={}, msgType={}",
|
||||||
|
event.getSessionId(), event.getMsgtype());
|
||||||
|
|
||||||
|
Conversation conv = getOrCreateSession(
|
||||||
|
event.getSessionId(),
|
||||||
|
event.getCorpId(),
|
||||||
|
event.getFromUser(),
|
||||||
|
event.getToUser()
|
||||||
|
);
|
||||||
|
|
||||||
|
String role = "EXTERNAL".equals(event.getFromRole()) ? "student" : "seat";
|
||||||
|
addMessageToContext(event.getSessionId(), role, event.getContent(), event.getMsgtime());
|
||||||
|
|
||||||
|
conv.setLastMsgId(event.getMsgid());
|
||||||
|
conv.setLastMsgTime(LocalDateTime.now());
|
||||||
|
conv.setLastMsgContent(truncateContent(event.getContent(), 200));
|
||||||
|
conv.setIdleDuration(0);
|
||||||
|
conversationMapper.updateById(conv);
|
||||||
|
|
||||||
|
if ("student".equals(role)) {
|
||||||
|
String turnType = detectTurnType(event.getContent());
|
||||||
|
createTurn(event.getSessionId(), conv.getRoundCount() + 1,
|
||||||
|
event.getMsgid(), event.getContent(), turnType);
|
||||||
|
} else {
|
||||||
|
updateLastTurn(conv.getSessionId(), conv.getRoundCount(),
|
||||||
|
event.getMsgid(), event.getContent());
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("处理新消息事件失败: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String detectTurnType(String content) {
|
||||||
|
if (content == null || content.isEmpty()) {
|
||||||
|
return "QUERY";
|
||||||
|
}
|
||||||
|
String lower = content.toLowerCase();
|
||||||
|
if (lower.contains("贵") || lower.contains("考虑") || lower.contains("担心")) {
|
||||||
|
return "OBJECTION";
|
||||||
|
}
|
||||||
|
if (lower.contains("报名") || lower.contains("决定") || lower.contains("确定")) {
|
||||||
|
return "INTEREST";
|
||||||
|
}
|
||||||
|
if (lower.contains("?") || lower.contains("?") || lower.contains("吗")) {
|
||||||
|
return "QUERY";
|
||||||
|
}
|
||||||
|
return "QUERY";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateLastTurn(String sessionId, int roundNumber, String seatMsgId, String content) {
|
||||||
|
ConversationTurn turn = conversationTurnMapper.selectOne(
|
||||||
|
new LambdaQueryWrapper<ConversationTurn>()
|
||||||
|
.eq(ConversationTurn::getSessionId, sessionId)
|
||||||
|
.eq(ConversationTurn::getTurnNumber, roundNumber)
|
||||||
|
);
|
||||||
|
if (turn != null) {
|
||||||
|
turn.setSeatMsgId(seatMsgId);
|
||||||
|
turn.setSeatContent(content);
|
||||||
|
conversationTurnMapper.updateById(turn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String truncateContent(String content, int maxLength) {
|
||||||
|
if (content == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return content.length() > maxLength ? content.substring(0, maxLength) + "..." : content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ArchiveMessageEvent {
|
||||||
|
private String msgid;
|
||||||
|
private String corpId;
|
||||||
|
private String sessionId;
|
||||||
|
private String fromUser;
|
||||||
|
private String fromRole;
|
||||||
|
private String toUser;
|
||||||
|
private String msgtype;
|
||||||
|
private String content;
|
||||||
|
private Long msgtime;
|
||||||
|
|
||||||
|
public String getMsgid() { return msgid; }
|
||||||
|
public void setMsgid(String msgid) { this.msgid = msgid; }
|
||||||
|
public String getCorpId() { return corpId; }
|
||||||
|
public void setCorpId(String corpId) { this.corpId = corpId; }
|
||||||
|
public String getSessionId() { return sessionId; }
|
||||||
|
public void setSessionId(String sessionId) { this.sessionId = sessionId; }
|
||||||
|
public String getFromUser() { return fromUser; }
|
||||||
|
public void setFromUser(String fromUser) { this.fromUser = fromUser; }
|
||||||
|
public String getFromRole() { return fromRole; }
|
||||||
|
public void setFromRole(String fromRole) { this.fromRole = fromRole; }
|
||||||
|
public String getToUser() { return toUser; }
|
||||||
|
public void setToUser(String toUser) { this.toUser = toUser; }
|
||||||
|
public String getMsgtype() { return msgtype; }
|
||||||
|
public void setMsgtype(String msgtype) { this.msgtype = msgtype; }
|
||||||
|
public String getContent() { return content; }
|
||||||
|
public void setContent(String content) { this.content = content; }
|
||||||
|
public Long getMsgtime() { return msgtime; }
|
||||||
|
public void setMsgtime(Long msgtime) { this.msgtime = msgtime; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ContextMessage {
|
||||||
|
private String role;
|
||||||
|
private String content;
|
||||||
|
private Long msgTime;
|
||||||
|
|
||||||
|
public ContextMessage() {}
|
||||||
|
|
||||||
|
public ContextMessage(String role, String content, Long msgTime) {
|
||||||
|
this.role = role;
|
||||||
|
this.content = content;
|
||||||
|
this.msgTime = msgTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRole() { return role; }
|
||||||
|
public void setRole(String role) { this.role = role; }
|
||||||
|
public String getContent() { return content; }
|
||||||
|
public void setContent(String content) { this.content = content; }
|
||||||
|
public Long getMsgTime() { return msgTime; }
|
||||||
|
public void setMsgTime(Long msgTime) { this.msgTime = msgTime; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
package com.artedu.intent.controller;
|
||||||
|
|
||||||
|
import com.artedu.common.result.Result;
|
||||||
|
import com.artedu.intent.dto.IntentResult;
|
||||||
|
import com.artedu.intent.entity.CustomerProfile;
|
||||||
|
import com.artedu.intent.entity.IntentCategory;
|
||||||
|
import com.artedu.intent.mapper.CustomerProfileMapper;
|
||||||
|
import com.artedu.intent.mapper.IntentCategoryMapper;
|
||||||
|
import com.artedu.intent.service.IntentRecognitionService;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/intent")
|
||||||
|
public class IntentController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IntentRecognitionService intentRecognitionService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IntentCategoryMapper intentCategoryMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private CustomerProfileMapper customerProfileMapper;
|
||||||
|
|
||||||
|
@PostMapping("/recognize")
|
||||||
|
public Result<IntentResult> recognize(
|
||||||
|
@RequestParam("message") String message,
|
||||||
|
@RequestParam("customerId") String customerId,
|
||||||
|
@RequestParam("corpId") String corpId,
|
||||||
|
@RequestParam("staffId") String staffId,
|
||||||
|
@RequestParam(value = "contextSummary", required = false) String contextSummary) {
|
||||||
|
|
||||||
|
IntentResult result = intentRecognitionService.recognize(message, contextSummary, customerId, corpId, staffId);
|
||||||
|
return Result.success(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/categories")
|
||||||
|
public Result<List<IntentCategory>> listCategories() {
|
||||||
|
List<IntentCategory> list = intentCategoryMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<IntentCategory>()
|
||||||
|
.eq(IntentCategory::getStatus, 1)
|
||||||
|
);
|
||||||
|
return Result.success(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/profile/{customerId}")
|
||||||
|
public Result<CustomerProfile> getProfile(
|
||||||
|
@PathVariable("customerId") String customerId,
|
||||||
|
@RequestParam("corpId") String corpId) {
|
||||||
|
|
||||||
|
CustomerProfile profile = customerProfileMapper.selectOne(
|
||||||
|
new LambdaQueryWrapper<CustomerProfile>()
|
||||||
|
.eq(CustomerProfile::getCustomerId, customerId)
|
||||||
|
.eq(CustomerProfile::getCorpId, corpId)
|
||||||
|
);
|
||||||
|
return Result.success(profile);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
package com.artedu.intent.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class IntentResult {
|
||||||
|
|
||||||
|
private IntentInfo primaryIntent;
|
||||||
|
private List<IntentInfo> secondaryIntents;
|
||||||
|
private Double confidence;
|
||||||
|
private String sentiment;
|
||||||
|
private Double sentimentConfidence;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class IntentInfo {
|
||||||
|
private String code;
|
||||||
|
private String name;
|
||||||
|
private String domain;
|
||||||
|
private Double confidence;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
package com.artedu.intent.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("customer_profiles")
|
||||||
|
public class CustomerProfile {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String customerId;
|
||||||
|
private String corpId;
|
||||||
|
private String studentType;
|
||||||
|
private Double studentTypeConfidence;
|
||||||
|
private String skillLevel;
|
||||||
|
private Double skillLevelConfidence;
|
||||||
|
private String intentLevel;
|
||||||
|
private Double intentScore;
|
||||||
|
private String concernFocus;
|
||||||
|
private Double concernFocusConfidence;
|
||||||
|
private String decisionStage;
|
||||||
|
private Double decisionStageConfidence;
|
||||||
|
private String interestedCourses;
|
||||||
|
private String budgetHint;
|
||||||
|
private String preferredCity;
|
||||||
|
private Integer age;
|
||||||
|
private String education;
|
||||||
|
private String currentOccupation;
|
||||||
|
private String slotData;
|
||||||
|
private Integer conversationCount;
|
||||||
|
private LocalDateTime lastConversationTime;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
package com.artedu.intent.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("intent_categories")
|
||||||
|
public class IntentCategory {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String code;
|
||||||
|
private String name;
|
||||||
|
private String domain;
|
||||||
|
private String description;
|
||||||
|
private String priority;
|
||||||
|
private String triggerKeywords;
|
||||||
|
private String sampleExpressions;
|
||||||
|
private Integer status;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
package com.artedu.intent.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("intent_recognitions")
|
||||||
|
public class IntentRecognition {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String sessionId;
|
||||||
|
private String turnId;
|
||||||
|
private String msgId;
|
||||||
|
private String customerId;
|
||||||
|
private String staffId;
|
||||||
|
private String messageContent;
|
||||||
|
private String primaryIntentCode;
|
||||||
|
private Double primaryIntentConfidence;
|
||||||
|
private String secondaryIntents;
|
||||||
|
private String sentiment;
|
||||||
|
private Double sentimentConfidence;
|
||||||
|
private String recognitionMethod;
|
||||||
|
private String llmModel;
|
||||||
|
private Integer promptTokens;
|
||||||
|
private Integer completionTokens;
|
||||||
|
private Integer elapsedMs;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
package com.artedu.intent.mapper;
|
||||||
|
|
||||||
|
import com.artedu.intent.entity.CustomerProfile;
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface CustomerProfileMapper extends BaseMapper<CustomerProfile> {
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
package com.artedu.intent.mapper;
|
||||||
|
|
||||||
|
import com.artedu.intent.entity.IntentCategory;
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface IntentCategoryMapper extends BaseMapper<IntentCategory> {
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
package com.artedu.intent.mapper;
|
||||||
|
|
||||||
|
import com.artedu.intent.entity.IntentRecognition;
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface IntentRecognitionMapper extends BaseMapper<IntentRecognition> {
|
||||||
|
}
|
||||||
@ -0,0 +1,291 @@
|
|||||||
|
package com.artedu.intent.service;
|
||||||
|
|
||||||
|
import com.artedu.common.config.RabbitConfig;
|
||||||
|
import com.artedu.common.util.JsonUtils;
|
||||||
|
import com.artedu.intent.dto.IntentResult;
|
||||||
|
import com.artedu.intent.entity.CustomerProfile;
|
||||||
|
import com.artedu.intent.entity.IntentCategory;
|
||||||
|
import com.artedu.intent.entity.IntentRecognition;
|
||||||
|
import com.artedu.intent.mapper.CustomerProfileMapper;
|
||||||
|
import com.artedu.intent.mapper.IntentCategoryMapper;
|
||||||
|
import com.artedu.intent.mapper.IntentRecognitionMapper;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.amqp.rabbit.annotation.RabbitListener;
|
||||||
|
import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class IntentRecognitionService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IntentCategoryMapper intentCategoryMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IntentRecognitionMapper intentRecognitionMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private CustomerProfileMapper customerProfileMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private LLMIntentClient llmIntentClient;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RedisTemplate<String, Object> redisTemplate;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RabbitTemplate rabbitTemplate;
|
||||||
|
|
||||||
|
private static final String INTENT_CACHE_KEY = "intent:";
|
||||||
|
|
||||||
|
public IntentResult recognize(String message, String contextSummary,
|
||||||
|
String customerId, String corpId, String staffId) {
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
String cacheKey = INTENT_CACHE_KEY + customerId + ":" + message.hashCode();
|
||||||
|
IntentResult cached = (IntentResult) redisTemplate.opsForValue().get(cacheKey);
|
||||||
|
if (cached != null) {
|
||||||
|
log.debug("意图识别命中缓存: customerId={}", customerId);
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
IntentResult ruleResult = ruleMatch(message);
|
||||||
|
if (ruleResult != null && ruleResult.getConfidence() > 0.85) {
|
||||||
|
log.debug("意图识别规则匹配命中: intent={}, confidence={}",
|
||||||
|
ruleResult.getPrimaryIntent().getCode(), ruleResult.getConfidence());
|
||||||
|
|
||||||
|
saveRecognition(message, contextSummary, customerId, corpId, staffId, ruleResult, "RULE", startTime);
|
||||||
|
updateProfile(customerId, corpId, ruleResult);
|
||||||
|
redisTemplate.opsForValue().set(cacheKey, ruleResult, 5, TimeUnit.MINUTES);
|
||||||
|
|
||||||
|
return ruleResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
IntentResult llmResult = llmIntentClient.recognize(message, contextSummary, getAllCategories());
|
||||||
|
|
||||||
|
if (ruleResult != null && llmResult != null) {
|
||||||
|
if (ruleResult.getPrimaryIntent().getCode()
|
||||||
|
.equals(llmResult.getPrimaryIntent().getCode())) {
|
||||||
|
llmResult.setConfidence(Math.min(1.0, llmResult.getConfidence() + 0.05));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveRecognition(message, contextSummary, customerId, corpId, staffId, llmResult, "HYBRID", startTime);
|
||||||
|
updateProfile(customerId, corpId, llmResult);
|
||||||
|
redisTemplate.opsForValue().set(cacheKey, llmResult, 5, TimeUnit.MINUTES);
|
||||||
|
sendRecommendEvent(customerId, corpId, staffId, llmResult, contextSummary);
|
||||||
|
|
||||||
|
log.info("意图识别完成: intent={}, confidence={}, elapsed={}ms",
|
||||||
|
llmResult.getPrimaryIntent().getCode(),
|
||||||
|
llmResult.getConfidence(),
|
||||||
|
System.currentTimeMillis() - startTime);
|
||||||
|
|
||||||
|
return llmResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IntentResult ruleMatch(String message) {
|
||||||
|
if (message == null || message.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String lowerMsg = message.toLowerCase();
|
||||||
|
|
||||||
|
List<IntentCategory> categories = intentCategoryMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<IntentCategory>()
|
||||||
|
.eq(IntentCategory::getStatus, 1)
|
||||||
|
);
|
||||||
|
|
||||||
|
IntentCategory bestMatch = null;
|
||||||
|
double bestScore = 0;
|
||||||
|
|
||||||
|
for (IntentCategory category : categories) {
|
||||||
|
List<String> keywords = JsonUtils.fromJsonList(
|
||||||
|
category.getTriggerKeywords(), String.class);
|
||||||
|
if (keywords == null || keywords.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int matchCount = 0;
|
||||||
|
for (String keyword : keywords) {
|
||||||
|
if (lowerMsg.contains(keyword.toLowerCase())) {
|
||||||
|
matchCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchCount > 0) {
|
||||||
|
double score = (double) matchCount / keywords.size();
|
||||||
|
if ("P1".equals(category.getPriority())) {
|
||||||
|
score *= 1.1;
|
||||||
|
}
|
||||||
|
if (score > bestScore) {
|
||||||
|
bestScore = score;
|
||||||
|
bestMatch = category;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestMatch != null && bestScore > 0.3) {
|
||||||
|
IntentResult result = new IntentResult();
|
||||||
|
IntentResult.IntentInfo intentInfo = new IntentResult.IntentInfo();
|
||||||
|
intentInfo.setCode(bestMatch.getCode());
|
||||||
|
intentInfo.setName(bestMatch.getName());
|
||||||
|
intentInfo.setDomain(bestMatch.getDomain());
|
||||||
|
intentInfo.setConfidence(Math.min(1.0, bestScore));
|
||||||
|
result.setPrimaryIntent(intentInfo);
|
||||||
|
result.setConfidence(bestScore);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveRecognition(String message, String contextSummary, String customerId,
|
||||||
|
String corpId, String staffId, IntentResult result,
|
||||||
|
String method, long startTime) {
|
||||||
|
try {
|
||||||
|
IntentRecognition record = new IntentRecognition();
|
||||||
|
record.setSessionId(contextSummary);
|
||||||
|
record.setMsgId(UUID.randomUUID().toString());
|
||||||
|
record.setCustomerId(customerId);
|
||||||
|
record.setStaffId(staffId);
|
||||||
|
record.setMessageContent(message.length() > 200 ? message.substring(0, 200) : message);
|
||||||
|
record.setPrimaryIntentCode(result.getPrimaryIntent().getCode());
|
||||||
|
record.setPrimaryIntentConfidence(result.getPrimaryIntent().getConfidence());
|
||||||
|
record.setSecondaryIntents(JsonUtils.toJson(result.getSecondaryIntents()));
|
||||||
|
record.setSentiment(result.getSentiment());
|
||||||
|
record.setRecognitionMethod(method);
|
||||||
|
record.setElapsedMs((int) (System.currentTimeMillis() - startTime));
|
||||||
|
record.setCreatedAt(LocalDateTime.now());
|
||||||
|
|
||||||
|
intentRecognitionMapper.insert(record);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("保存意图识别记录失败: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateProfile(String customerId, String corpId, IntentResult result) {
|
||||||
|
try {
|
||||||
|
CustomerProfile profile = customerProfileMapper.selectOne(
|
||||||
|
new LambdaQueryWrapper<CustomerProfile>()
|
||||||
|
.eq(CustomerProfile::getCustomerId, customerId)
|
||||||
|
.eq(CustomerProfile::getCorpId, corpId)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (profile == null) {
|
||||||
|
profile = new CustomerProfile();
|
||||||
|
profile.setCustomerId(customerId);
|
||||||
|
profile.setCorpId(corpId);
|
||||||
|
profile.setIntentLevel("低");
|
||||||
|
profile.setIntentScore(0.0);
|
||||||
|
profile.setDecisionStage("信息了解");
|
||||||
|
profile.setConversationCount(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
String intentCode = result.getPrimaryIntent().getCode();
|
||||||
|
double score = result.getPrimaryIntent().getConfidence();
|
||||||
|
|
||||||
|
updateIntentScore(profile, intentCode, score);
|
||||||
|
updateConcernFocus(profile, intentCode);
|
||||||
|
updateDecisionStage(profile, intentCode);
|
||||||
|
|
||||||
|
profile.setLastConversationTime(LocalDateTime.now());
|
||||||
|
profile.setUpdatedAt(LocalDateTime.now());
|
||||||
|
|
||||||
|
if (profile.getId() == null) {
|
||||||
|
customerProfileMapper.insert(profile);
|
||||||
|
} else {
|
||||||
|
customerProfileMapper.updateById(profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("更新学员画像失败: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateIntentScore(CustomerProfile profile, String intentCode, double score) {
|
||||||
|
double currentScore = profile.getIntentScore() != null ? profile.getIntentScore() : 0.0;
|
||||||
|
if (intentCode.startsWith("INT-PRICE-") || intentCode.startsWith("INT-JOB-")) {
|
||||||
|
profile.setIntentScore(Math.min(100.0, currentScore + score * 10));
|
||||||
|
} else if (intentCode.startsWith("INT-COURSE-")) {
|
||||||
|
profile.setIntentScore(Math.min(100.0, currentScore + score * 5));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profile.getIntentScore() >= 70) {
|
||||||
|
profile.setIntentLevel("高");
|
||||||
|
} else if (profile.getIntentScore() >= 40) {
|
||||||
|
profile.setIntentLevel("中");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateConcernFocus(CustomerProfile profile, String intentCode) {
|
||||||
|
if (intentCode.startsWith("INT-PRICE-")) {
|
||||||
|
profile.setConcernFocus("价格敏感型");
|
||||||
|
} else if (intentCode.startsWith("INT-JOB-")) {
|
||||||
|
profile.setConcernFocus("就业导向型");
|
||||||
|
} else if (intentCode.startsWith("INT-TEACH-")) {
|
||||||
|
profile.setConcernFocus("师资看重型");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateDecisionStage(CustomerProfile profile, String intentCode) {
|
||||||
|
if (intentCode.startsWith("INT-PRICE-03") || intentCode.startsWith("INT-PRICE-04")) {
|
||||||
|
profile.setDecisionStage("购买决策");
|
||||||
|
} else if (intentCode.startsWith("INT-COURSE-04") || intentCode.startsWith("INT-COURSE-02")) {
|
||||||
|
profile.setDecisionStage("方案比较");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendRecommendEvent(String customerId, String corpId, String staffId,
|
||||||
|
IntentResult result, String contextSummary) {
|
||||||
|
try {
|
||||||
|
Map<String, Object> event = Map.of(
|
||||||
|
"customerId", customerId,
|
||||||
|
"corpId", corpId,
|
||||||
|
"staffId", staffId,
|
||||||
|
"primaryIntent", result.getPrimaryIntent().getCode(),
|
||||||
|
"sessionId", contextSummary
|
||||||
|
);
|
||||||
|
rabbitTemplate.convertAndSend(
|
||||||
|
RabbitConfig.CONVERSATION_EXCHANGE,
|
||||||
|
RabbitConfig.ROUTING_KEY_RECOMMEND,
|
||||||
|
JsonUtils.toJson(event)
|
||||||
|
);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("发送推荐事件失败: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<IntentCategory> getAllCategories() {
|
||||||
|
return intentCategoryMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<IntentCategory>()
|
||||||
|
.eq(IntentCategory::getStatus, 1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@RabbitListener(queues = "conversation.intent_analyze.queue")
|
||||||
|
public void handleIntentAnalyze(String messageJson) {
|
||||||
|
try {
|
||||||
|
Map<String, Object> event = JsonUtils.fromJsonMap(messageJson);
|
||||||
|
if (event == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String customerId = (String) event.get("customerId");
|
||||||
|
String corpId = (String) event.get("corpId");
|
||||||
|
String staffId = (String) event.get("staffId");
|
||||||
|
String message = (String) event.get("message");
|
||||||
|
String contextSummary = (String) event.get("contextSummary");
|
||||||
|
|
||||||
|
recognize(message, contextSummary, customerId, corpId, staffId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("处理意图分析事件失败: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,139 @@
|
|||||||
|
package com.artedu.intent.service;
|
||||||
|
|
||||||
|
import com.artedu.common.util.JsonUtils;
|
||||||
|
import com.artedu.intent.dto.IntentResult;
|
||||||
|
import com.artedu.intent.entity.IntentCategory;
|
||||||
|
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.*;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class LLMIntentClient {
|
||||||
|
|
||||||
|
@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 IntentResult recognize(String message, String contextSummary, List<IntentCategory> categories) {
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
try {
|
||||||
|
String prompt = buildPrompt(message, contextSummary, categories);
|
||||||
|
|
||||||
|
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");
|
||||||
|
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);
|
||||||
|
return parseIntentResult(text, categories);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.error("LLM意图识别失败: status={}", response.getStatusCode());
|
||||||
|
return createDefaultResult();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("LLM意图识别异常: {}", e.getMessage(), e);
|
||||||
|
return createDefaultResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildPrompt(String message, String contextSummary, List<IntentCategory> categories) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("你是一个专业的教育培训机构客服意图识别助手。请根据学员的咨询消息,判断其意图。\n\n");
|
||||||
|
sb.append("可选意图列表:\n");
|
||||||
|
for (IntentCategory cat : categories) {
|
||||||
|
sb.append("- ").append(cat.getCode()).append(": ").append(cat.getName())
|
||||||
|
.append(" (").append(cat.getDomain()).append(")\n");
|
||||||
|
}
|
||||||
|
sb.append("\n对话上下文:\n").append(contextSummary != null ? contextSummary : "无");
|
||||||
|
sb.append("\n学员最新消息:").append(message);
|
||||||
|
sb.append("\n\n请输出JSON格式结果,包含:primaryIntentCode, confidence(0-1), sentiment(POSITIVE/NEUTRAL/NEGATIVE)");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 IntentResult parseIntentResult(String text, List<IntentCategory> categories) {
|
||||||
|
IntentResult result = new IntentResult();
|
||||||
|
try {
|
||||||
|
String jsonStr = text.substring(text.indexOf("{"), text.lastIndexOf("}") + 1);
|
||||||
|
Map<String, Object> map = JsonUtils.fromJsonMap(jsonStr);
|
||||||
|
|
||||||
|
String code = (String) map.get("primaryIntentCode");
|
||||||
|
double confidence = Double.parseDouble(String.valueOf(map.getOrDefault("confidence", 0.8)));
|
||||||
|
String sentiment = (String) map.getOrDefault("sentiment", "NEUTRAL");
|
||||||
|
|
||||||
|
IntentCategory matched = categories.stream()
|
||||||
|
.filter(c -> c.getCode().equals(code))
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
|
IntentResult.IntentInfo intentInfo = new IntentResult.IntentInfo();
|
||||||
|
if (matched != null) {
|
||||||
|
intentInfo.setCode(matched.getCode());
|
||||||
|
intentInfo.setName(matched.getName());
|
||||||
|
intentInfo.setDomain(matched.getDomain());
|
||||||
|
} else {
|
||||||
|
intentInfo.setCode(code);
|
||||||
|
intentInfo.setName("未知意图");
|
||||||
|
intentInfo.setDomain("其他");
|
||||||
|
}
|
||||||
|
intentInfo.setConfidence(confidence);
|
||||||
|
|
||||||
|
result.setPrimaryIntent(intentInfo);
|
||||||
|
result.setConfidence(confidence);
|
||||||
|
result.setSentiment(sentiment);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("解析LLM意图结果失败,使用默认值: {}", e.getMessage());
|
||||||
|
return createDefaultResult();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IntentResult createDefaultResult() {
|
||||||
|
IntentResult result = new IntentResult();
|
||||||
|
IntentResult.IntentInfo intentInfo = new IntentResult.IntentInfo();
|
||||||
|
intentInfo.setCode("INT-COURSE-01");
|
||||||
|
intentInfo.setName("课程内容咨询");
|
||||||
|
intentInfo.setDomain("课程相关");
|
||||||
|
intentInfo.setConfidence(0.5);
|
||||||
|
result.setPrimaryIntent(intentInfo);
|
||||||
|
result.setConfidence(0.5);
|
||||||
|
result.setSentiment("NEUTRAL");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user