refactor: 抽离特殊聊天模式处理逻辑

- 将工作流、人机交互恢复、思考模式处理逻辑抽离为独立方法
- 新增 handleSpecialChatModes 方法统一处理特殊模式
- 新增 handleThinkingMode 方法专门处理思考模式
- 简化 sseChat 方法结构,提高代码可读性

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ageerle
2026-03-20 10:38:54 +08:00
parent c84d6247b0
commit 27ad00ac3a
6 changed files with 167 additions and 324 deletions

View File

@@ -1,38 +0,0 @@
package org.ruoyi.agent;
import dev.langchain4j.agentic.Agent;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.V;
/**
* User Name Retrieval Agent
* A simple assistant that retrieves user names using the get_name tool.
*/
public interface GetNameInfo {
@SystemMessage("""
You are a user identity assistant. You MUST always use tools to get information.
MANDATORY REQUIREMENTS:
- You MUST call the get_user_name_by_id tool for ANY question about names or identity
- NEVER respond without calling the get_user_name_by_id tool first
- Return ONLY the exact string returned by the get_user_name_by_id tool
- Do not make up names like "John Doe" or any other default names
- Do not use your knowledge to answer - ALWAYS use the tool
Your workflow:
1. Extract userId from the query (if mentioned), or use "1" as default
2. ALWAYS call the get_user_name_by_id tool with the userId parameter
3. Return the exact result as plain text with no additions
CRITICAL: If you don't call the get_user_name_by_id tool, your response is wrong.
""")
@UserMessage("""
Get the user name using the get_user_name_by_id tool. Query: {{query}}
IMPORTANT: Return only the exact result from the tool.
""")
@Agent("User identity assistant that returns user name from get_name tool")
String search(@V("query") String query);
}

View File

@@ -1,42 +0,0 @@
package org.ruoyi.agent;
import dev.langchain4j.agentic.Agent;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.V;
public interface McpAgent extends Agent {
/**
* 系统提示词:通用工具调用智能体
* 不限定具体工具类型,让 LangChain4j 自动传递工具描述给 LLM
*/
@SystemMessage("""
你是一个AI助手可以通过调用各种工具来帮助用户完成不同的任务。
【工具使用规则】
1. 根据用户的请求,判断需要使用哪些工具
2. 仔细阅读每个工具的描述,确保理解工具的功能和参数要求
3. 使用正确的参数调用工具
4. 如果工具执行失败,向用户友好地说明错误原因,并尝试提供替代方案
5. 对于复杂任务,可以分步骤使用多个工具完成
6. 将工具执行结果以清晰易懂的方式呈现给用户
【响应格式】
- 直接回答用户的问题
- 如果使用了工具,说明使用了什么工具以及结果
- 如果遇到错误,提供友好的错误信息和解决建议
""")
@UserMessage("""
{{query}}
""")
@Agent("通用工具调用智能体")
/**
* 智能体对外调用入口
* @param query 用户的自然语言请求
* @return 处理结果
*/
String callMcpTool(@V("query") String query);
}

View File

@@ -1,132 +0,0 @@
//package org.ruoyi.service.chat.handler;
//
//import dev.langchain4j.agentic.AgenticServices;
//import dev.langchain4j.community.model.dashscope.QwenChatModel;
//import dev.langchain4j.service.tool.ToolProvider;
//import lombok.RequiredArgsConstructor;
//import lombok.extern.slf4j.Slf4j;
//import org.ruoyi.agent.McpAgent;
//import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
//import org.ruoyi.common.chat.entity.chat.ChatContext;
//import org.ruoyi.common.chat.service.chatMessage.IChatMessageService;
//import org.ruoyi.common.sse.utils.SseMessageUtils;
//import org.ruoyi.mcp.service.core.ToolProviderFactory;
//import org.springframework.core.annotation.Order;
//import org.springframework.stereotype.Component;
//import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
//
//import java.util.ArrayList;
//import java.util.List;
//
///**
// * Agent 深度思考处理器
// * <p>
// * 处理 enableThinking=true 的场景,使用 Agent 进行深度思考和工具调用
// *
// * @author ageerle@163.com
// * @date 2025/12/13
// */
//@Slf4j
//@Component
//@Order(3)
//@RequiredArgsConstructor
//public class AgentChatHandler implements ChatHandler {
//
// private final ToolProviderFactory toolProviderFactory;
//
// @Override
// public boolean supports(ChatContext context) {
// Boolean enableThinking = context.getChatRequest().getEnableThinking();
// return enableThinking != null && enableThinking;
// }
//
// @Override
// public SseEmitter handle(ChatContext context) {
// log.info("处理 Agent 深度思考,用户: {}", context.getUserId());
//
// Long userId = context.getUserId();
// String tokenValue = context.getTokenValue();
// ChatModelVo chatModelVo = context.getChatModelVo();
//
// try {
// // 1. 保存用户消息
// String content = extractUserContent(context);
//// saveChatMessage(context.getChatRequest(), userId, content,
//// RoleType.USER.getName(), chatModelVo);
//
// // 2. 执行 Agent 任务
// String result = doAgent(content, chatModelVo);
//
// // 3. 发送结果并保存
// SseMessageUtils.sendMessage(userId, result);
// SseMessageUtils.completeConnection(userId, tokenValue);
//
//// saveChatMessage(context.getChatRequest(), userId, result,
//// RoleType.ASSISTANT.getName(), chatModelVo);
// // todo 保存消息
// } catch (Exception e) {
// log.error("Agent 执行失败: {}", e.getMessage(), e);
// SseMessageUtils.sendMessage(userId, "Agent 执行失败:" + e.getMessage());
// SseMessageUtils.completeConnection(userId, tokenValue);
// }
//
// return context.getEmitter();
// }
//
// /**
// * 执行 Agent 任务
// */
// private String doAgent(String userMessage, ChatModelVo chatModelVo) {
// log.info("执行 Agent 任务,消息: {}", userMessage);
//
// try {
// // 1. 加载 LLM 模型
// QwenChatModel qwenChatModel = QwenChatModel.builder()
// .apiKey(chatModelVo.getApiKey())
// .modelName(chatModelVo.getModelName())
// .build();
//
// // 2. 获取内置工具
// List<Object> builtinTools = toolProviderFactory.getAllBuiltinToolObjects();
// List<Object> allTools = new ArrayList<>(builtinTools);
// log.debug("加载 {} 个内置工具", builtinTools.size());
//
// // 3. 获取 MCP 工具提供者
// ToolProvider mcpToolProvider = toolProviderFactory.getAllEnabledMcpToolsProvider();
//
// // 4. 创建 MCP Agent
// var agentBuilder = AgenticServices.agentBuilder(McpAgent.class)
// .chatModel(qwenChatModel);
//
// if (!allTools.isEmpty()) {
// agentBuilder.tools(allTools.toArray(new Object[0]));
// }
// if (mcpToolProvider != null) {
// agentBuilder.toolProvider(mcpToolProvider);
// }
//
// McpAgent mcpAgent = agentBuilder.build();
//
// // 5. 调用 Agent
// String result = mcpAgent.callMcpTool(userMessage);
// log.info("Agent 执行完成,结果长度: {}", result.length());
// return result;
//
// } catch (Exception e) {
// log.error("Agent 模式执行失败: {}", e.getMessage(), e);
// return "Agent 执行失败: " + e.getMessage();
// }
// }
//
// /**
// * 提取用户消息内容
// */
// private String extractUserContent(ChatContext context) {
// var messages = context.getChatRequest().getMessages();
// if (messages != null && !messages.isEmpty()) {
// return messages.get(0).getContent();
// }
// return "";
// }
//
//}

View File

@@ -1,53 +0,0 @@
//package org.ruoyi.service.chat.handler;
//
//import lombok.RequiredArgsConstructor;
//import lombok.extern.slf4j.Slf4j;
//import org.ruoyi.common.chat.domain.dto.request.ReSumeRunner;
//import org.ruoyi.common.chat.entity.chat.ChatContext;
//import org.ruoyi.common.chat.service.workFlow.IWorkFlowStarterService;
//import org.ruoyi.common.core.utils.ObjectUtils;
//import org.springframework.core.annotation.Order;
//import org.springframework.stereotype.Component;
//import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
//
///**
// * 人机交互恢复处理器
// * <p>
// * 处理 isResume=true 的场景,恢复工作流的人机交互
// *
// * @author ageerle@163.com
// * @date 2025/12/13
// */
//@Slf4j
//@Component
//@Order(1)
//@RequiredArgsConstructor
//public class ResumeChatHandler implements ChatHandler {
//
// private final IWorkFlowStarterService workFlowStarterService;
//
// @Override
// public boolean supports(ChatContext context) {
// Boolean isResume = context.getChatRequest().getIsResume();
// return isResume != null && isResume;
// }
//
// @Override
// public SseEmitter handle(ChatContext context) {
// log.info("处理人机交互恢复,用户: {}", context.getUserId());
//
// ReSumeRunner reSumeRunner = context.getChatRequest().getReSumeRunner();
// if (ObjectUtils.isEmpty(reSumeRunner)) {
// log.warn("人机交互恢复参数为空");
// return context.getEmitter();
// }
//
// workFlowStarterService.resumeFlow(
// reSumeRunner.getRuntimeUuid(),
// reSumeRunner.getFeedbackContent(),
// context.getEmitter()
// );
//
// return context.getEmitter();
// }
//}

View File

@@ -1,54 +0,0 @@
//package org.ruoyi.service.chat.handler;
//
//import lombok.RequiredArgsConstructor;
//import lombok.extern.slf4j.Slf4j;
//import org.ruoyi.common.chat.base.ThreadContext;
//import org.ruoyi.common.chat.domain.dto.request.WorkFlowRunner;
//import org.ruoyi.common.chat.entity.chat.ChatContext;
//import org.ruoyi.common.chat.service.workFlow.IWorkFlowStarterService;
//import org.ruoyi.common.core.utils.ObjectUtils;
//import org.springframework.core.annotation.Order;
//import org.springframework.stereotype.Component;
//import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
//
///**
// * 工作流对话处理器
// * <p>
// * 处理 enableWorkFlow=true 的场景,启动工作流对话
// *
// * @author ageerle@163.com
// * @date 2025/12/13
// */
//@Slf4j
//@Component
//@Order(2)
//@RequiredArgsConstructor
//public class WorkflowChatHandler implements ChatHandler {
//
// private final IWorkFlowStarterService workFlowStarterService;
//
// @Override
// public boolean supports(ChatContext context) {
// Boolean enableWorkFlow = context.getChatRequest().getEnableWorkFlow();
// return enableWorkFlow != null && enableWorkFlow;
// }
//
// @Override
// public SseEmitter handle(ChatContext context) {
// log.info("处理工作流对话,用户: {}, 会话: {}",
// context.getUserId(), context.getChatRequest().getSessionId());
//
// WorkFlowRunner runner = context.getChatRequest().getWorkFlowRunner();
// if (ObjectUtils.isEmpty(runner)) {
// log.warn("工作流参数为空");
// return context.getEmitter();
// }
//
// return workFlowStarterService.streaming(
// ThreadContext.getCurrentUser(),
// runner.getUuid(),
// runner.getInputs(),
// context.getChatRequest().getSessionId()
// );
// }
//}

View File

@@ -1,25 +1,48 @@
package org.ruoyi.service.chat.impl;
import cn.dev33.satoken.stp.StpUtil;
import dev.langchain4j.agentic.AgenticServices;
import dev.langchain4j.agentic.supervisor.SupervisorAgent;
import dev.langchain4j.agentic.supervisor.SupervisorResponseStrategy;
import dev.langchain4j.community.model.dashscope.QwenChatModel;
import dev.langchain4j.data.message.*;
import dev.langchain4j.mcp.McpToolProvider;
import dev.langchain4j.mcp.client.DefaultMcpClient;
import dev.langchain4j.mcp.client.McpClient;
import dev.langchain4j.mcp.client.transport.McpTransport;
import dev.langchain4j.mcp.client.transport.stdio.StdioMcpTransport;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.StreamingChatModel;
import dev.langchain4j.model.chat.response.ChatResponse;
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.tool.ToolProvider;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.agent.ChartGenerationAgent;
import org.ruoyi.agent.SqlAgent;
import org.ruoyi.agent.WebSearchAgent;
import org.ruoyi.agent.tool.ExecuteSqlQueryTool;
import org.ruoyi.agent.tool.QueryAllTablesTool;
import org.ruoyi.agent.tool.QueryTableSchemaTool;
import org.ruoyi.common.chat.base.ThreadContext;
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
import org.ruoyi.common.chat.domain.dto.request.ReSumeRunner;
import org.ruoyi.common.chat.domain.dto.request.WorkFlowRunner;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.common.chat.enums.RoleType;
import org.ruoyi.common.chat.service.chat.IChatModelService;
import org.ruoyi.common.chat.service.chat.IChatService;
import org.ruoyi.common.chat.service.workFlow.IWorkFlowStarterService;
import org.ruoyi.common.core.utils.ObjectUtils;
import org.ruoyi.common.satoken.utils.LoginHelper;
import org.ruoyi.common.sse.core.SseEmitterManager;
import org.ruoyi.common.sse.utils.SseMessageUtils;
import org.ruoyi.domain.bo.vector.QueryVectorBo;
import org.ruoyi.domain.vo.knowledge.KnowledgeInfoVo;
import org.ruoyi.factory.ChatServiceFactory;
import org.ruoyi.mcp.service.core.ToolProviderFactory;
import org.ruoyi.service.chat.AbstractChatService;
import org.ruoyi.service.chat.IChatMessageService;
import org.ruoyi.service.chat.impl.memory.PersistentChatMemoryStore;
@@ -62,12 +85,18 @@ public class ChatServiceFacade implements IChatService {
private final IChatMessageService chatMessageService;
private final IWorkFlowStarterService workFlowStarterService;
private final ToolProviderFactory toolProviderFactory;
/**
* 内存实例缓存,避免同一会话重复创建
* Key: sessionId, Value: MessageWindowChatMemory实例
*/
private static final Map<Object, MessageWindowChatMemory> memoryCache = new ConcurrentHashMap<>();
/**
* 统一聊天入口 - SSE流式响应
*
@@ -76,6 +105,11 @@ public class ChatServiceFacade implements IChatService {
*/
public SseEmitter sseChat(ChatRequest chatRequest) {
// 4. 具体的服务实现
Long userId = LoginHelper.getUserId();
String tokenValue = StpUtil.getTokenValue();
SseEmitter emitter = sseEmitterManager.connect(userId, tokenValue);
// 1. 根据模型名称查询完整配置
ChatModelVo chatModelVo = chatModelService.selectModelByName(chatRequest.getModel());
if (chatModelVo == null) {
@@ -85,14 +119,17 @@ public class ChatServiceFacade implements IChatService {
// 2. 构建上下文消息列表
List<ChatMessage> contextMessages = buildContextMessages(chatRequest);
// 3. 路由服务提供商
// 3. 处理特殊聊天模式(工作流、人机交互恢复、思考模式)
SseEmitter specialResult = handleSpecialChatModes(chatRequest, contextMessages, chatModelVo, emitter);
if (specialResult != null) {
return specialResult;
}
// 4. 路由服务提供商
String providerCode = chatModelVo.getProviderCode();
log.info("路由到服务提供商: {}, 模型: {}", providerCode, chatRequest.getModel());
AbstractChatService chatService = chatServiceFactory.getOriginalService(providerCode);
// 4. 具体的服务实现
Long userId = LoginHelper.getUserId();
String tokenValue = StpUtil.getTokenValue();
SseEmitter emitter = sseEmitterManager.connect(userId, tokenValue);
StreamingChatResponseHandler handler = createResponseHandler(userId, tokenValue,chatRequest);
@@ -105,6 +142,131 @@ public class ChatServiceFacade implements IChatService {
return emitter;
}
/**
* 处理特殊聊天模式(工作流、人机交互恢复、思考模式)
*
* @param chatRequest 聊天请求
* @param contextMessages 上下文消息列表(可能被修改)
* @param chatModelVo 聊天模型配置
* @param emitter SSE发射器
* @return 如果需要提前返回则返回SseEmitter否则返回null
*/
private SseEmitter handleSpecialChatModes(ChatRequest chatRequest, List<ChatMessage> contextMessages,
ChatModelVo chatModelVo, SseEmitter emitter) {
// 处理工作流对话
if (chatRequest.getEnableWorkFlow()) {
log.info("处理工作流对话,会话: {}", chatRequest.getSessionId());
WorkFlowRunner runner = chatRequest.getWorkFlowRunner();
if (ObjectUtils.isEmpty(runner)) {
log.warn("工作流参数为空");
}
return workFlowStarterService.streaming(
ThreadContext.getCurrentUser(),
runner.getUuid(),
runner.getInputs(),
chatRequest.getSessionId()
);
}
// 处理人机交互恢复
if (chatRequest.getIsResume()) {
log.info("处理人机交互恢复");
ReSumeRunner reSumeRunner = chatRequest.getReSumeRunner();
if (ObjectUtils.isEmpty(reSumeRunner)) {
log.warn("人机交互恢复参数为空");
return emitter;
}
workFlowStarterService.resumeFlow(
reSumeRunner.getRuntimeUuid(),
reSumeRunner.getFeedbackContent(),
emitter
);
return emitter;
}
// 处理思考模式
if (chatRequest.getEnableThinking()) {
handleThinkingMode(chatRequest, contextMessages, chatModelVo);
}
return null;
}
/**
* 处理思考模式
*
* @param chatRequest 聊天请求
* @param contextMessages 上下文消息列表
* @param chatModelVo 聊天模型配置
*/
private void handleThinkingMode(ChatRequest chatRequest, List<ChatMessage> contextMessages, ChatModelVo chatModelVo) {
// 步骤1: 配置MCP传输层 - 连接到bing-cn-mcp服务器
McpTransport transport = new StdioMcpTransport.Builder()
.command(List.of("C:\\Program Files\\nodejs\\npx.cmd", "-y", "bing-cn-mcp"))
.logEvents(true)
.build();
McpClient mcpClient = new DefaultMcpClient.Builder()
.transport(transport)
.build();
ToolProvider toolProvider = McpToolProvider.builder()
.mcpClients(List.of(mcpClient))
.build();
// 配置echarts MCP
McpTransport transport1 = new StdioMcpTransport.Builder()
.command(List.of("C:\\Program Files\\nodejs\\npx.cmd", "-y", "mcp-echarts"))
.logEvents(true)
.build();
McpClient mcpClient1 = new DefaultMcpClient.Builder()
.transport(transport1)
.build();
ToolProvider toolProvider1 = McpToolProvider.builder()
.mcpClients(List.of(mcpClient1))
.build();
// 配置模型
OpenAiChatModel plannerModel = OpenAiChatModel.builder()
.baseUrl(chatModelVo.getApiHost())
.apiKey(chatModelVo.getApiKey())
.modelName(chatModelVo.getModelName())
.build();
// 构建各Agent
SqlAgent sqlAgent = AgenticServices.agentBuilder(SqlAgent.class)
.chatModel(plannerModel)
.tools(new QueryAllTablesTool(), new QueryTableSchemaTool(), new ExecuteSqlQueryTool())
.build();
WebSearchAgent searchAgent = AgenticServices.agentBuilder(WebSearchAgent.class)
.chatModel(plannerModel)
.toolProvider(toolProvider)
.build();
ChartGenerationAgent chartGenerationAgent = AgenticServices.agentBuilder(ChartGenerationAgent.class)
.chatModel(plannerModel)
.toolProvider(toolProvider1)
.build();
// 构建监督者Agent
SupervisorAgent supervisor = AgenticServices.supervisorBuilder()
.chatModel(plannerModel)
.subAgents(sqlAgent, chartGenerationAgent)
.responseStrategy(SupervisorResponseStrategy.LAST)
.build();
String invoke = supervisor.invoke(chatRequest.getContent());
contextMessages.add(AiMessage.from(invoke));
}
/**
* 支持外部 handler 的对话接口(跨模块调用)
* 同时发送到 SSE 和外部 handler