feat(oa): 优化 AI服务并添加缓存支持
- 添加 Redis 缓存支持,优化 AI 配置和对话历史的获取性能 - 实现 AI回复缓存,减少重复请求的处理时间 - 新增缓存清除接口,支持对话缓存和 AI 配置缓存的清除 - 重构 AI 服务调用流程,增加缓存检查和更新逻辑
This commit is contained in:
@@ -200,9 +200,29 @@ public class SysOaAiController extends BaseController {
|
|||||||
*/
|
*/
|
||||||
@PostMapping("/conversation/{conversationId}/end")
|
@PostMapping("/conversation/{conversationId}/end")
|
||||||
public R<Void> endConversation(@PathVariable Long conversationId) {
|
public R<Void> endConversation(@PathVariable Long conversationId) {
|
||||||
|
// 清除对话缓存
|
||||||
|
aiServiceUtil.clearConversationCache(conversationId);
|
||||||
return toAjax(conversationService.endConversation(conversationId));
|
return toAjax(conversationService.endConversation(conversationId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除对话缓存
|
||||||
|
*/
|
||||||
|
@DeleteMapping("/conversation/{conversationId}/cache")
|
||||||
|
public R<Void> clearConversationCache(@PathVariable Long conversationId) {
|
||||||
|
aiServiceUtil.clearConversationCache(conversationId);
|
||||||
|
return R.ok("缓存已清除");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除AI配置缓存
|
||||||
|
*/
|
||||||
|
@DeleteMapping("/cache/config")
|
||||||
|
public R<Void> clearAiConfigCache() {
|
||||||
|
aiServiceUtil.clearAiConfigCache();
|
||||||
|
return R.ok("AI配置缓存已清除");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发送消息给AI
|
* 发送消息给AI
|
||||||
*/
|
*/
|
||||||
@@ -212,8 +232,14 @@ public class SysOaAiController extends BaseController {
|
|||||||
// 1. 保存用户消息
|
// 1. 保存用户消息
|
||||||
messageService.addUserMessage(conversationId, message);
|
messageService.addUserMessage(conversationId, message);
|
||||||
|
|
||||||
// 2. 获取对话历史
|
// 2. 获取对话历史(优先从Redis缓存获取)
|
||||||
List<SysOaAiMessageVo> conversationHistory = messageService.queryByConversationId(conversationId);
|
List<SysOaAiMessageVo> conversationHistory = aiServiceUtil.getCachedConversationHistory(conversationId);
|
||||||
|
if (conversationHistory == null) {
|
||||||
|
// 缓存中没有,从数据库获取
|
||||||
|
conversationHistory = messageService.queryByConversationId(conversationId);
|
||||||
|
// 缓存对话历史
|
||||||
|
aiServiceUtil.cacheConversationHistory(conversationId, conversationHistory);
|
||||||
|
}
|
||||||
|
|
||||||
// 3. 调用AI服务获取回复(传递对话历史)
|
// 3. 调用AI服务获取回复(传递对话历史)
|
||||||
String aiResponse = callAiServiceWithHistory(message, conversationHistory);
|
String aiResponse = callAiServiceWithHistory(message, conversationHistory);
|
||||||
@@ -221,6 +247,10 @@ public class SysOaAiController extends BaseController {
|
|||||||
// 4. 保存AI回复
|
// 4. 保存AI回复
|
||||||
messageService.addAiMessage(conversationId, aiResponse, 0, java.math.BigDecimal.ZERO);
|
messageService.addAiMessage(conversationId, aiResponse, 0, java.math.BigDecimal.ZERO);
|
||||||
|
|
||||||
|
// 5. 更新对话历史缓存
|
||||||
|
List<SysOaAiMessageVo> updatedHistory = messageService.queryByConversationId(conversationId);
|
||||||
|
aiServiceUtil.cacheConversationHistory(conversationId, updatedHistory);
|
||||||
|
|
||||||
Map<String, Object> result = new HashMap<>();
|
Map<String, Object> result = new HashMap<>();
|
||||||
result.put("conversationId", conversationId);
|
result.put("conversationId", conversationId);
|
||||||
result.put("response", aiResponse);
|
result.put("response", aiResponse);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.ruoyi.oa.utils;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.ruoyi.common.utils.redis.RedisUtils;
|
||||||
import com.ruoyi.oa.domain.vo.SysOaAiConfigVo;
|
import com.ruoyi.oa.domain.vo.SysOaAiConfigVo;
|
||||||
import com.ruoyi.oa.domain.vo.SysOaAiMessageVo;
|
import com.ruoyi.oa.domain.vo.SysOaAiMessageVo;
|
||||||
import com.ruoyi.oa.service.ISysOaAiConfigService;
|
import com.ruoyi.oa.service.ISysOaAiConfigService;
|
||||||
@@ -12,6 +13,7 @@ import org.springframework.stereotype.Component;
|
|||||||
import org.springframework.web.client.RestTemplate;
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.time.Duration;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,6 +31,11 @@ public class AiServiceUtil {
|
|||||||
private final RestTemplate restTemplate;
|
private final RestTemplate restTemplate;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
// Redis缓存key前缀
|
||||||
|
private static final String AI_CONFIG_CACHE_KEY = "ai:config:active";
|
||||||
|
private static final String AI_CONVERSATION_CACHE_KEY = "ai:conversation:";
|
||||||
|
private static final String AI_RESPONSE_CACHE_KEY = "ai:response:";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 调用DeepSeek AI服务(单次对话)
|
* 调用DeepSeek AI服务(单次对话)
|
||||||
*
|
*
|
||||||
@@ -48,18 +55,26 @@ public class AiServiceUtil {
|
|||||||
*/
|
*/
|
||||||
public String callDeepSeekWithHistory(String message, List<SysOaAiMessageVo> conversationHistory) {
|
public String callDeepSeekWithHistory(String message, List<SysOaAiMessageVo> conversationHistory) {
|
||||||
try {
|
try {
|
||||||
// 获取AI配置
|
// 1. 检查缓存中是否有相同的请求
|
||||||
SysOaAiConfigVo config = configService.getActiveConfig();
|
String cacheKey = generateCacheKey(message, conversationHistory);
|
||||||
|
String cachedResponse = RedisUtils.getCacheObject(cacheKey);
|
||||||
|
if (cachedResponse != null) {
|
||||||
|
log.info("从Redis缓存获取AI回复");
|
||||||
|
return cachedResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 获取AI配置(优先从缓存获取)
|
||||||
|
SysOaAiConfigVo config = getCachedAiConfig();
|
||||||
if (config == null) {
|
if (config == null) {
|
||||||
throw new RuntimeException("未找到可用的AI配置");
|
throw new RuntimeException("未找到可用的AI配置");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建请求头
|
// 3. 构建请求头
|
||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
headers.set("Authorization", "Bearer " + config.getApiKey());
|
headers.set("Authorization", "Bearer " + config.getApiKey());
|
||||||
|
|
||||||
// 构建请求体
|
// 4. 构建请求体
|
||||||
Map<String, Object> requestBody = new HashMap<>();
|
Map<String, Object> requestBody = new HashMap<>();
|
||||||
requestBody.put("model", config.getModelName());
|
requestBody.put("model", config.getModelName());
|
||||||
requestBody.put("temperature", config.getTemperature());
|
requestBody.put("temperature", config.getTemperature());
|
||||||
@@ -67,7 +82,7 @@ public class AiServiceUtil {
|
|||||||
|
|
||||||
List<Map<String, String>> messages = new ArrayList<>();
|
List<Map<String, String>> messages = new ArrayList<>();
|
||||||
|
|
||||||
// 添加对话历史
|
// 5. 添加对话历史
|
||||||
if (conversationHistory != null && !conversationHistory.isEmpty()) {
|
if (conversationHistory != null && !conversationHistory.isEmpty()) {
|
||||||
for (SysOaAiMessageVo historyMessage : conversationHistory) {
|
for (SysOaAiMessageVo historyMessage : conversationHistory) {
|
||||||
Map<String, String> historyMsg = new HashMap<>();
|
Map<String, String> historyMsg = new HashMap<>();
|
||||||
@@ -77,7 +92,7 @@ public class AiServiceUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加当前用户消息
|
// 6. 添加当前用户消息
|
||||||
Map<String, String> userMessage = new HashMap<>();
|
Map<String, String> userMessage = new HashMap<>();
|
||||||
userMessage.put("role", "user");
|
userMessage.put("role", "user");
|
||||||
userMessage.put("content", message);
|
userMessage.put("content", message);
|
||||||
@@ -85,7 +100,7 @@ public class AiServiceUtil {
|
|||||||
|
|
||||||
requestBody.put("messages", messages);
|
requestBody.put("messages", messages);
|
||||||
|
|
||||||
// 发送请求
|
// 7. 发送请求
|
||||||
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
|
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
|
||||||
String url = config.getBaseUrl() + "/chat/completions";
|
String url = config.getBaseUrl() + "/chat/completions";
|
||||||
|
|
||||||
@@ -96,10 +111,13 @@ public class AiServiceUtil {
|
|||||||
JsonNode jsonNode = objectMapper.readTree(response.getBody());
|
JsonNode jsonNode = objectMapper.readTree(response.getBody());
|
||||||
String aiResponse = jsonNode.path("choices").path(0).path("message").path("content").asText();
|
String aiResponse = jsonNode.path("choices").path(0).path("message").path("content").asText();
|
||||||
|
|
||||||
// 记录token使用情况
|
// 8. 记录token使用情况
|
||||||
int totalTokens = jsonNode.path("usage").path("total_tokens").asInt();
|
int totalTokens = jsonNode.path("usage").path("total_tokens").asInt();
|
||||||
log.info("AI回复成功,消耗token: {}", totalTokens);
|
log.info("AI回复成功,消耗token: {}", totalTokens);
|
||||||
|
|
||||||
|
// 9. 缓存AI回复(缓存5分钟)
|
||||||
|
RedisUtils.setCacheObject(cacheKey, aiResponse, Duration.ofDays(1));
|
||||||
|
|
||||||
return aiResponse;
|
return aiResponse;
|
||||||
} else {
|
} else {
|
||||||
log.error("DeepSeek API调用失败: {}", response.getStatusCode());
|
log.error("DeepSeek API调用失败: {}", response.getStatusCode());
|
||||||
@@ -111,6 +129,82 @@ public class AiServiceUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取缓存的AI配置
|
||||||
|
*/
|
||||||
|
private SysOaAiConfigVo getCachedAiConfig() {
|
||||||
|
// 先从Redis缓存获取
|
||||||
|
SysOaAiConfigVo cachedConfig = RedisUtils.getCacheObject(AI_CONFIG_CACHE_KEY);
|
||||||
|
if (cachedConfig != null) {
|
||||||
|
log.debug("从Redis缓存获取AI配置");
|
||||||
|
return cachedConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓存中没有,从数据库获取
|
||||||
|
SysOaAiConfigVo config = configService.getActiveConfig();
|
||||||
|
if (config != null) {
|
||||||
|
// 缓存配置(缓存30分钟)
|
||||||
|
RedisUtils.setCacheObject(AI_CONFIG_CACHE_KEY, config, Duration.ofMinutes(30));
|
||||||
|
log.debug("AI配置已缓存到Redis");
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成缓存key
|
||||||
|
*/
|
||||||
|
private String generateCacheKey(String message, List<SysOaAiMessageVo> conversationHistory) {
|
||||||
|
StringBuilder keyBuilder = new StringBuilder(AI_RESPONSE_CACHE_KEY);
|
||||||
|
keyBuilder.append(message.hashCode());
|
||||||
|
|
||||||
|
if (conversationHistory != null && !conversationHistory.isEmpty()) {
|
||||||
|
for (SysOaAiMessageVo history : conversationHistory) {
|
||||||
|
keyBuilder.append(":").append(history.getContent().hashCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyBuilder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存对话历史
|
||||||
|
*/
|
||||||
|
public void cacheConversationHistory(Long conversationId, List<SysOaAiMessageVo> messages) {
|
||||||
|
String cacheKey = AI_CONVERSATION_CACHE_KEY + conversationId;
|
||||||
|
RedisUtils.setCacheObject(cacheKey, messages, Duration.ofMinutes(10));
|
||||||
|
log.debug("对话历史已缓存到Redis, conversationId: {}", conversationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取缓存的对话历史
|
||||||
|
*/
|
||||||
|
public List<SysOaAiMessageVo> getCachedConversationHistory(Long conversationId) {
|
||||||
|
String cacheKey = AI_CONVERSATION_CACHE_KEY + conversationId;
|
||||||
|
List<SysOaAiMessageVo> cachedMessages = RedisUtils.getCacheObject(cacheKey);
|
||||||
|
if (cachedMessages != null) {
|
||||||
|
log.debug("从Redis缓存获取对话历史, conversationId: {}", conversationId);
|
||||||
|
}
|
||||||
|
return cachedMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除对话历史缓存
|
||||||
|
*/
|
||||||
|
public void clearConversationCache(Long conversationId) {
|
||||||
|
String cacheKey = AI_CONVERSATION_CACHE_KEY + conversationId;
|
||||||
|
RedisUtils.deleteObject(cacheKey);
|
||||||
|
log.debug("已清除对话历史缓存, conversationId: {}", conversationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除AI配置缓存
|
||||||
|
*/
|
||||||
|
public void clearAiConfigCache() {
|
||||||
|
RedisUtils.deleteObject(AI_CONFIG_CACHE_KEY);
|
||||||
|
log.debug("已清除AI配置缓存");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 计算费用(示例:每1000个token 0.002美元)
|
* 计算费用(示例:每1000个token 0.002美元)
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user