package com.wok.supportbot.app; import com.wok.supportbot.advisor.ContentSafetyAdvisor; import com.wok.supportbot.advisor.MyLoggerAdvisor; import com.wok.supportbot.advisor.ReReadingAdvisor; import com.wok.supportbot.chatmemory.DatabaseChatMemory; import com.wok.supportbot.config.ChatModelFactory; import com.wok.supportbot.rag.preretrieval.CompressionQueryRewriter; import com.wok.supportbot.rag.preretrieval.MultiQueryExpanderRewriter; import com.wok.supportbot.rag.preretrieval.RewriteQueryRewriter; import com.wok.supportbot.rag.preretrieval.TranslationQueryRewriter; import com.wok.supportbot.service.FaqMatchEngine; import com.wok.supportbot.mcp.McpToolCallback; import com.wok.supportbot.mcp.McpToolCallbackAdapter; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; import org.springframework.ai.chat.client.advisor.api.Advisor; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.document.Document; import org.springframework.ai.rag.advisor.RetrievalAugmentationAdvisor; import org.springframework.ai.rag.generation.augmentation.ContextualQueryAugmenter; import org.springframework.ai.rag.retrieval.search.VectorStoreDocumentRetriever; import org.springframework.ai.tool.ToolCallback; import org.springframework.ai.vectorstore.SearchRequest; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.ai.vectorstore.filter.Filter; import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import reactor.core.publisher.Flux; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import static org.springframework.ai.chat.memory.ChatMemory.CONVERSATION_ID; /** * @Classname AssistantApp * @Description * @Version 1.0.0 * @Date 2025/06/27 14:11 * @Author lyx */ @Component @Slf4j public class AssistantApp { @Resource private VectorStore pgVectorVectorStore; @Resource private ContentSafetyAdvisor contentSafetyAdvisor; @Resource private FaqMatchEngine faqMatchEngine; @Resource private McpToolCallbackAdapter mcpToolCallbackAdapter; /** MCP 工具开关,默认启用,可通过 application.yml 的 chat.mcp.enabled 关闭 */ @Value("${chat.mcp.enabled:true}") private boolean enableMcpTools; private final ChatModelFactory chatModelFactory; private final DatabaseChatMemory chatMemory; private final ConcurrentHashMap chatClientCache = new ConcurrentHashMap<>(); private static final String SYSTEM_PROMPT = "";//""\u4f60\u662f\u83dc\u5927\u738b\u5185\u90e8\u77e5\u8bc6\u52a9\u624b\uff0c\u4e3b\u8981\u670d\u52a1\u5bf9\u8c61\u662f\u5ba2\u670d\u3001\u8d22\u52a1\u3001\u884c\u653f\u7b49\u5185\u90e8\u4eba\u5458\uff0c\u4e0d\u662f\u76f4\u63a5\u9762\u5411\u7ec8\u7aef\u5ba2\u6237\u7684\u673a\u5668\u4eba\u3002\n\n\u9ed8\u8ba4\u4e1a\u52a1\u4e0a\u4e0b\u6587\uff1a\n- \u7528\u6237\u672a\u7279\u522b\u8bf4\u660e\u65f6\uff0c\u5e73\u53f0\u3001APP\u3001\u540e\u53f0\u3001\u8ba2\u5355\u3001\u5ba2\u6237\u3001\u4f9b\u5e94\u5546\u5747\u9ed8\u8ba4\u6307\u83dc\u5927\u738b\u76f8\u5173\u7cfb\u7edf\u548c\u4e1a\u52a1\u3002\n- \u4e0d\u8981\u53cd\u95ee\u201c\u54ea\u4e2a\u5e73\u53f0/\u7cfb\u7edf\u201d\uff0c\u9664\u975e\u7528\u6237\u660e\u786e\u63d0\u5230\u5916\u90e8\u5e73\u53f0\u3001\u591a\u4e1a\u52a1\u7ebf\u51b2\u7a81\uff0c\u6216\u95ee\u9898\u672c\u8eab\u5fc5\u987b\u533a\u5206\u7cfb\u7edf\u3002\n\n\u56de\u7b54\u65b9\u5f0f\uff1a\n- \u5185\u90e8\u4eba\u5458\u8be2\u95ee\u5ba2\u6237\u54a8\u8be2\u7c7b\u95ee\u9898\u65f6\uff0c\u5148\u7ed9\u201c\u53ef\u8f6c\u8ff0\u7ed9\u5ba2\u6237\u7684\u7b54\u590d\u53e3\u5f84\u201d\uff0c\u518d\u7ed9\u5185\u90e8\u5904\u7406\u8981\u70b9\u3002\n- \u5bf9\u6d41\u7a0b\u7c7b\u95ee\u9898\uff0c\u4f18\u5148\u76f4\u63a5\u7ed9\u5165\u53e3\u3001\u6b65\u9aa4\u3001\u6761\u4ef6\u548c\u5f02\u5e38\u5904\u7406\uff0c\u4e0d\u8981\u4e00\u5f00\u59cb\u53ea\u8ffd\u95ee\u4fe1\u606f\u3002\n- \u53ea\u6709\u5728\u7f3a\u5c11\u8ba2\u5355\u53f7\u3001\u8ba2\u5355\u72b6\u6001\u3001\u53d1\u7968\u7c7b\u578b\u3001\u8d26\u53f7\u6743\u9650\u7b49\u4f1a\u5f71\u54cd\u529e\u7406\u7ed3\u679c\u7684\u5173\u952e\u4fe1\u606f\u65f6\uff0c\u624d\u8865\u5145\u9700\u8981\u786e\u8ba4\u7684\u4e8b\u9879\u3002\n- \u6d89\u53ca\u8d22\u52a1\u3001\u884c\u653f\u5236\u5ea6\u6216\u654f\u611f\u5185\u90e8\u4fe1\u606f\u65f6\uff0c\u53ea\u8f93\u51fa\u5f53\u524d\u89d2\u8272\u548c\u77e5\u8bc6\u5e93\u5141\u8bb8\u4f7f\u7528\u7684\u5185\u5bb9\uff1b\u8d44\u6599\u4e0d\u8db3\u65f6\u5982\u5b9e\u8bf4\u660e\uff0c\u4e0d\u8981\u7f16\u9020\u3002\n\n\u4fdd\u6301\u4e13\u4e1a\u3001\u7b80\u6d01\u3001\u53ef\u6267\u884c\u3002"; /** * 初始化 ChatClient * * @param dashscopeChatModel */ public AssistantApp(ChatModelFactory chatModelFactory, DatabaseChatMemory chatMemory) { this.chatModelFactory = chatModelFactory; this.chatMemory = chatMemory; } private ChatClient getChatClient(String appType) { return getChatClient(appType, enableMcpTools ? java.util.List.of("*") : null); } /** * 获取 ChatClient(支持按角色过滤 MCP 工具) * 缓存 key 包含工具权限信息,不同角色权限缓存不同的 ChatClient 实例。 * * @param appType 应用类型(CHAT / RAG_REWRITE 等) * @param allowedMcpTools 允许的 MCP 工具名列表;null 或空 = 允许所有(默认行为);["*"] = 全部 */ private ChatClient getChatClient(String appType, List allowedMcpTools) { // 空列表降级为 ["*"](允许所有工具),与无参数版本行为一致 final List effectiveTools; if (enableMcpTools && (allowedMcpTools == null || allowedMcpTools.isEmpty())) { effectiveTools = java.util.List.of("*"); } else { effectiveTools = allowedMcpTools; } String cacheKey = appType + ":" + (effectiveTools == null ? "none" : String.join(",", effectiveTools)); return chatClientCache.computeIfAbsent(cacheKey, type -> { ChatModel chatModel = chatModelFactory.getChatModel(appType); var builder = ChatClient.builder(chatModel); // 仅在系统提示词非空时设置,避免 Spring AI 的 Assert.hasText 校验抛异常 if (StringUtils.hasText(SYSTEM_PROMPT)) { builder.defaultSystem(SYSTEM_PROMPT); } // 注册 MCP 工具(按角色权限过滤) if (enableMcpTools && effectiveTools != null && !effectiveTools.isEmpty()) { ToolCallback[] mcpTools = mcpToolCallbackAdapter.getFilteredToolCallbacks(effectiveTools); if (mcpTools.length > 0) { builder.defaultToolCallbacks(mcpTools); log.info("✅ 已注册 {} 个 MCP 工具到 ChatClient [{}]", mcpTools.length, cacheKey); for (ToolCallback tc : mcpTools) { log.info(" 🔧 工具: {} — {}", tc.getToolDefinition().name(), tc.getToolDefinition().description()); } } else { log.warn("⚠️ MCP 工具已启用但无可注册的工具 [{}],请检查:", cacheKey); log.warn(" 1. 是否在「MCP 服务管理」页面添加了 MCP Server 配置?"); log.warn(" 2. 配置是否已启用(is_active=true)?"); log.warn(" 3. MCP Server 是否连接成功(点击「测试连接」验证)?"); log.warn(" 4. MCP Server 是否暴露了工具(listTools 返回非空)?"); } } else { log.info("ℹ️ MCP 工具未启用 [enableMcpTools={}, effectiveTools={}]", enableMcpTools, effectiveTools); } return builder .defaultAdvisors( contentSafetyAdvisor, MessageChatMemoryAdvisor.builder(chatMemory).build(), new MyLoggerAdvisor() ) .build(); }); } public void clearCache() { chatClientCache.clear(); log.info("AssistantApp ChatClient cache cleared"); } /** * AI 基础对话(支持多轮对话记忆) * * @param message * @param chatId * @return */ public String doChat(String message, String chatId) { return doChat(message, chatId, null); } /** * AI 基础对话(支持多轮对话记忆 + 角色系统提示词) * * @param message 用户消息(保持原样,不做包装,避免污染会话记忆) * @param chatId 会话ID * @param systemPrompt 角色人设/风格,作为系统提示词叠加在基础提示词之上;为空则仅用基础提示词 * @return AI 回答 */ public String doChat(String message, String chatId, String systemPrompt) { return doChat(message, chatId, systemPrompt, null); } /** * AI 基础对话(支持多轮对话记忆 + 角色系统提示词 + MCP 工具权限) * * @param message 用户消息 * @param chatId 会话ID * @param systemPrompt 角色人设/风格 * @param allowedMcpTools 允许的 MCP 工具列表;null=不注册工具 * @return AI 回答 */ public String doChat(String message, String chatId, String systemPrompt, List allowedMcpTools) { ChatClient.ChatClientRequestSpec spec = getChatClient("CHAT", allowedMcpTools) .prompt() .user(message) .advisors(s -> s.param(CONVERSATION_ID, chatId)); if (StringUtils.hasText(systemPrompt)) { spec = spec.system(effectiveSystem(systemPrompt)); } ChatResponse chatResponse = spec.call().chatResponse(); return chatResponse.getResult().getOutput().getText(); } /** * 组合系统提示词:基础客服提示词 + 角色人设。 * 角色人设作为附加段落叠加,既保留客服基线约束,又让角色风格生效。 */ private String effectiveSystem(String rolePrompt) { if (!StringUtils.hasText(rolePrompt)) { return SYSTEM_PROMPT; } return SYSTEM_PROMPT + "\n\n【当前角色设定】\n" + rolePrompt; } /** * AI 基础对话(支持多轮对话记忆,SSE 流式传输) * * @param message * @param chatId * @return */ public Flux doChatByStream(String message, String chatId) { return doChatByStream(message, chatId, null); } /** * AI 基础对话(多轮记忆 + 角色系统提示词,SSE 流式传输) * * @param message 用户消息(保持原样) * @param chatId 会话ID * @param systemPrompt 角色人设/风格,作为系统提示词叠加;为空则仅用基础提示词 * @return 流式回答 */ public Flux doChatByStream(String message, String chatId, String systemPrompt) { ChatClient.ChatClientRequestSpec spec = getChatClient("CHAT") .prompt() .user(message) .advisors(s -> s.param(CONVERSATION_ID, chatId)); if (StringUtils.hasText(systemPrompt)) { spec = spec.system(effectiveSystem(systemPrompt)); } return spec.stream().content(); } /** * AI 基础对话(多轮记忆 + 角色系统提示词 + MCP 工具权限,SSE 流式传输) * * @param message 用户消息 * @param chatId 会话ID * @param systemPrompt 角色人设/风格 * @param allowedMcpTools 允许的 MCP 工具列表;null=不注册工具 * @return 流式回答 */ public Flux doChatByStream(String message, String chatId, String systemPrompt, List allowedMcpTools) { // 重置 MCP 工具调用状态(事件收集器 + 轮次计数器) McpToolCallback.resetEvents(); McpToolCallback.resetCallRounds(); ChatClient.ChatClientRequestSpec spec = getChatClient("CHAT", allowedMcpTools) .prompt() .user(message) .advisors(s -> s.param(CONVERSATION_ID, chatId)); if (StringUtils.hasText(systemPrompt)) { spec = spec.system(effectiveSystem(systemPrompt)); } return appendMcpToolEvents(spec.stream().content()); } /** * 在 SSE 文本流末尾追加 MCP 工具调用事件 * 前端通过解析 "event: tool_call_start" / "event: tool_call_result" 行来展示工具调用过程。 * 事件格式遵循 SSE 标准,与 readSSEStreamWithEvents 的解析逻辑匹配。 */ private Flux appendMcpToolEvents(Flux contentFlux) { return contentFlux.concatMap(chunk -> Flux.just(chunk)) .concatWith(Flux.defer(() -> { java.util.List events = McpToolCallback.drainEvents(); if (events.isEmpty()) { return Flux.empty(); } log.info("本次对话触发了 {} 个 MCP 工具调用", events.size()); List sseLines = new ArrayList<>(); for (McpToolCallback.ToolCallEvent event : events) { sseLines.add("event:tool_call_result"); String json = String.format("{\"tool\":\"%s\",\"result\":\"%s\",\"latencyMs\":%d}", escapeSse(event.tool()), escapeSse(event.result()), event.latencyMs()); sseLines.add("data:" + json); sseLines.add(""); // SSE 事件分隔空行 } return Flux.fromIterable(sseLines); })); } /** 转义 SSE 数据中的特殊字符 */ private static String escapeSse(String s) { if (s == null) return ""; return s.replace("\\", "\\\\").replace("\"", "\\\"") .replace("\n", "\\n").replace("\r", "\\r"); } // ==================== FAQ 优先匹配 ==================== /** * 尝试 FAQ 三级匹配(精确→关键词→语义),命中则返回标准答案。 * 在 RAG 对话入口处优先调用,避免不必要的知识库检索开销。 * * @param message 用户消息 * @return FAQ 标准答案(未命中时返回 empty) */ private Optional tryFaqMatch(String message) { try { return faqMatchEngine.match(message) .map(result -> result.getFaq().getAnswer()); } catch (Exception e) { log.warn("FAQ 匹配异常,降级到 RAG: {}", e.getMessage()); return Optional.empty(); } } // AI 恋爱知识库问答功能 @Resource RewriteQueryRewriter rewriteQueryRewriter; @Resource CompressionQueryRewriter compressionQueryRewriter; @Resource MultiQueryExpanderRewriter multiQueryExpanderRewriter; @Resource TranslationQueryRewriter translationQueryRewriter; /** * 和 RAG 知识库进行对话 * * @param message * @param chatId * @return */ public String doChatWithRag(String message, String chatId) { // 在预检索阶段,系统接收用户的原始查询,通过查询转换和查询扩展等方法对其进行优化,输出增强的用户查询。 // String rewrittenMessage = translationQueryRewriter.doQueryRewrite(message); String rewrittenMessage = rewriteQueryRewriter.doQueryRewrite(message); ChatResponse chatResponse = getChatClient("CHAT") .prompt() .user(rewrittenMessage) .advisors(spec -> spec.param(CONVERSATION_ID, chatId)) // 应用 RAG 知识库问答 .advisors(buildRetrievalAdvisor(4, Collections.emptyList())) .call() .chatResponse(); return chatResponse.getResult().getOutput().getText(); } /** * 和 RAG 知识库进行对话(支持动态选择查询重写策略) * * @param message 用户消息 * @param chatId 会话ID * @param strategy 查询重写策略:NONE/REWRITE/TRANSLATION/COMPRESSION/MULTI_QUERY * @return AI 回答 */ public String doChatWithRagStrategy(String message, String chatId, String strategy) { return doChatWithRagStrategy(message, chatId, strategy, Collections.emptyList()); } public String doChatWithRagStrategy(String message, String chatId, String strategy, Long categoryId) { List categoryIds = categoryId != null ? List.of(categoryId) : Collections.emptyList(); return doChatWithRagStrategy(message, chatId, strategy, categoryIds); } public String doChatWithRagStrategy(String message, String chatId, String strategy, List categoryIds) { return doChatWithRagStrategy(message, chatId, strategy, categoryIds, null); } public String doChatWithRagStrategy(String message, String chatId, String strategy, List categoryIds, String systemPrompt) { // FAQ 优先匹配:三级匹配(精确→关键词→语义)命中则直接返回标准答案 Optional faqAnswer = tryFaqMatch(message); if (faqAnswer.isPresent()) { log.info("FAQ 命中,直接返回标准答案: chatId={}", chatId); return faqAnswer.get(); } // 对于 MULTI_QUERY 策略,需要使用特殊的处理方式 if ("MULTI_QUERY".equalsIgnoreCase(strategy)) { return doChatWithMultiQueryRag(message, chatId, categoryIds, systemPrompt); } // 其他策略:单查询处理 String rewrittenMessage = rewriteQuery(message, chatId, strategy); ChatClient.ChatClientRequestSpec spec = getChatClient("CHAT") .prompt() .user(rewrittenMessage) .advisors(s -> s.param(CONVERSATION_ID, chatId)) // 应用 RAG 知识库问答 .advisors(buildRetrievalAdvisor(4, categoryIds)); if (StringUtils.hasText(systemPrompt)) { spec = spec.system(effectiveSystem(systemPrompt)); } ChatResponse chatResponse = spec.call().chatResponse(); return chatResponse.getResult().getOutput().getText(); } /** * RAG 知识库对话(支持查询重写策略 + MCP 工具权限) * * @param message 用户消息 * @param chatId 会话ID * @param strategy 查询重写策略 * @param categoryIds 知识库分类过滤 * @param systemPrompt 角色人设/风格 * @param allowedMcpTools 允许的 MCP 工具列表;null=不注册工具 * @return AI 回答 */ public String doChatWithRagStrategy(String message, String chatId, String strategy, List categoryIds, String systemPrompt, List allowedMcpTools) { // FAQ 优先匹配 Optional faqAnswer = tryFaqMatch(message); if (faqAnswer.isPresent()) { log.info("FAQ 命中,直接返回标准答案: chatId={}", chatId); return faqAnswer.get(); } if ("MULTI_QUERY".equalsIgnoreCase(strategy)) { return doChatWithMultiQueryRag(message, chatId, categoryIds, systemPrompt, allowedMcpTools); } String rewrittenMessage = rewriteQuery(message, chatId, strategy); ChatClient.ChatClientRequestSpec spec = getChatClient("CHAT", allowedMcpTools) .prompt() .user(rewrittenMessage) .advisors(s -> s.param(CONVERSATION_ID, chatId)) .advisors(buildRetrievalAdvisor(4, categoryIds)); if (StringUtils.hasText(systemPrompt)) { spec = spec.system(effectiveSystem(systemPrompt)); } ChatResponse chatResponse = spec.call().chatResponse(); return chatResponse.getResult().getOutput().getText(); } /** * 根据策略对查询做预检索改写(MULTI_QUERY 不走此方法)。 */ private String rewriteQuery(String message, String chatId, String strategy) { if (strategy == null || strategy.isEmpty()) { return message; } try { switch (strategy.toUpperCase()) { case "REWRITE": return rewriteQueryRewriter.doQueryRewrite(message); case "TRANSLATION": return translationQueryRewriter.doQueryRewrite(message); case "COMPRESSION": // 查询压缩需要对话历史,从会话记忆中取最近若干条传入, // 利用多轮上下文把指代不清的追问补全为独立查询 List history = chatMemory.get(chatId, 10); return compressionQueryRewriter.doQueryRewrite(message, history); case "NONE": default: return message; } } catch (Exception e) { // 查询重写失败时降级为原始查询,避免 RAG_REWRITE 配置异常(如 InvalidApiKey)导致整次对话不可用 log.warn("查询重写失败 [strategy={}, chatId={}],降级使用原始查询: {}", strategy, chatId, e.getMessage()); return message; } } /** * 和 RAG 知识库进行对话(支持查询重写策略,SSE 流式传输) * * @param message 用户消息 * @param chatId 会话ID * @param strategy 查询重写策略:NONE/REWRITE/TRANSLATION/COMPRESSION/MULTI_QUERY * @param categoryIds 知识库分类过滤 * @param systemPrompt 角色人设/风格 * @return 流式回答 */ public Flux doChatWithRagStrategyByStream(String message, String chatId, String strategy, List categoryIds, String systemPrompt) { // FAQ 优先匹配:命中则直接以流式形式返回标准答案 Optional faqAnswer = tryFaqMatch(message); if (faqAnswer.isPresent()) { log.info("FAQ 命中(流式),直接返回标准答案: chatId={}", chatId); return Flux.just(faqAnswer.get()); } // 对于 MULTI_QUERY 策略,需要先手动检索合并再流式生成 if ("MULTI_QUERY".equalsIgnoreCase(strategy)) { return doChatWithMultiQueryRagByStream(message, chatId, categoryIds, systemPrompt); } String rewrittenMessage = rewriteQuery(message, chatId, strategy); ChatClient.ChatClientRequestSpec spec = getChatClient("CHAT") .prompt() .user(rewrittenMessage) .advisors(s -> s.param(CONVERSATION_ID, chatId)) .advisors(buildRetrievalAdvisor(4, categoryIds)); if (StringUtils.hasText(systemPrompt)) { spec = spec.system(effectiveSystem(systemPrompt)); } return spec.stream().content(); } /** * 和 RAG 知识库进行对话(支持查询重写策略 + MCP 工具权限,SSE 流式传输) * * @param message 用户消息 * @param chatId 会话ID * @param strategy 查询重写策略 * @param categoryIds 知识库分类过滤 * @param systemPrompt 角色人设/风格 * @param allowedMcpTools 允许的 MCP 工具列表;null=不注册工具 * @return 流式回答 */ public Flux doChatWithRagStrategyByStream(String message, String chatId, String strategy, List categoryIds, String systemPrompt, List allowedMcpTools) { // 重置 MCP 工具调用状态 McpToolCallback.resetEvents(); McpToolCallback.resetCallRounds(); // FAQ 优先匹配:命中则直接以流式形式返回标准答案 Optional faqAnswer = tryFaqMatch(message); if (faqAnswer.isPresent()) { log.info("FAQ 命中(流式),直接返回标准答案: chatId={}", chatId); return Flux.just(faqAnswer.get()); } // 对于 MULTI_QUERY 策略,需要先手动检索合并再流式生成 if ("MULTI_QUERY".equalsIgnoreCase(strategy)) { return doChatWithMultiQueryRagByStream(message, chatId, categoryIds, systemPrompt, allowedMcpTools); } String rewrittenMessage = rewriteQuery(message, chatId, strategy); ChatClient.ChatClientRequestSpec spec = getChatClient("CHAT", allowedMcpTools) .prompt() .user(rewrittenMessage) .advisors(s -> s.param(CONVERSATION_ID, chatId)) .advisors(buildRetrievalAdvisor(4, categoryIds)); if (StringUtils.hasText(systemPrompt)) { spec = spec.system(effectiveSystem(systemPrompt)); } return appendMcpToolEvents(spec.stream().content()); } /** * 使用多路查询扩展的 RAG 对话 * 将原始查询扩展为多个语义不同的查询,分别检索后按文档ID去重合并, * 再把合并后的资料注入 system 提示词(不污染用户消息与会话记忆)。 * * @param message 用户消息(保持原样) * @param chatId 会话ID * @param categoryIds 知识库分类过滤 * @param systemPrompt 角色人设/风格 * @return AI 回答 */ private String doChatWithMultiQueryRag(String message, String chatId, List categoryIds, String systemPrompt) { return doChatWithMultiQueryRag(message, chatId, categoryIds, systemPrompt, null); } private String doChatWithMultiQueryRag(String message, String chatId, List categoryIds, String systemPrompt, List allowedMcpTools) { String ragSystem = buildMultiQueryRagSystem(message, categoryIds, systemPrompt); ChatResponse chatResponse = getChatClient("CHAT", allowedMcpTools) .prompt() .system(ragSystem) .user(message) .advisors(s -> s.param(CONVERSATION_ID, chatId)) .call() .chatResponse(); return chatResponse.getResult().getOutput().getText(); } private Flux doChatWithMultiQueryRagByStream(String message, String chatId, List categoryIds, String systemPrompt) { return doChatWithMultiQueryRagByStream(message, chatId, categoryIds, systemPrompt, null); } private Flux doChatWithMultiQueryRagByStream(String message, String chatId, List categoryIds, String systemPrompt, List allowedMcpTools) { String ragSystem = buildMultiQueryRagSystem(message, categoryIds, systemPrompt); return appendMcpToolEvents(getChatClient("CHAT", allowedMcpTools) .prompt() .system(ragSystem) .user(message) .advisors(s -> s.param(CONVERSATION_ID, chatId)) .stream() .content()); } /** * 多路查询扩展 + 检索合并,组合出注入了知识库资料的系统提示词。 * 将原始查询扩展为多个语义不同的查询,分别检索后按文档ID去重合并, * 资料注入 system(不污染用户消息与会话记忆)。供同步与流式两条路径复用。 */ private String buildMultiQueryRagSystem(String message, List categoryIds, String systemPrompt) { List mergedDocs = retrieveMultiQueryDocs(message, categoryIds); log.info("多路检索合并后文档数: {}", mergedDocs.size()); // 拼接资料为上下文,组合系统提示词 String context = mergedDocs.stream() .map(Document::getText) .filter(StringUtils::hasText) .collect(Collectors.joining("\n\n---\n\n")); return buildRagSystemPrompt(systemPrompt, context); } /** * 多路查询扩展 + 检索去重合并,返回命中的文档(供 RAG 回答与"引用来源"复用)。 */ private List retrieveMultiQueryDocs(String message, List categoryIds) { // 多路扩展依赖 RAG_REWRITE 模型;扩展失败时退回原问题检索,避免整次 RAG 不可用。 List expandedQueries; try { expandedQueries = multiQueryExpanderRewriter.doQueryRewrite(message); } catch (Exception e) { log.warn("多路查询扩展失败,降级为原始问题检索: {}", e.getMessage()); expandedQueries = List.of(message); } if (expandedQueries == null || expandedQueries.isEmpty()) { expandedQueries = List.of(message); } log.info("多路查询扩展结果: {}", expandedQueries); // 对每个查询分别检索,按文档 ID 去重合并,封顶 maxDocs,避免上下文膨胀。 final int maxDocs = 8; Map merged = new LinkedHashMap<>(); for (String query : expandedQueries) { if (!StringUtils.hasText(query) || merged.size() >= maxDocs) { continue; } List docs = pgVectorVectorStore.similaritySearch(buildRagSearchRequest(4, categoryIds, query)); if (docs == null) { continue; } for (Document doc : docs) { if (merged.size() >= maxDocs) { break; } merged.putIfAbsent(doc.getId(), doc); } } return new ArrayList<>(merged.values()); } /** * 检索本次问题命中的知识库片段(不生成回答),用于在回答下方展示"引用来源"。 * 复用与 RAG 回答完全相同的查询改写策略与分类范围,确保来源即答案所依据的片段。 * * @param strategy 查询重写策略(与回答保持一致) * @return 命中的文档片段(含 metadata:documentId/title/sourceName/chunkIndex/distance) */ public List retrieveRagSources(String message, String chatId, String strategy, List categoryIds) { if (!StringUtils.hasText(message)) { return Collections.emptyList(); } if ("MULTI_QUERY".equalsIgnoreCase(strategy)) { return retrieveMultiQueryDocs(message, categoryIds); } String rewritten = rewriteQuery(message, chatId, strategy); List docs = pgVectorVectorStore.similaritySearch(buildRagSearchRequest(4, categoryIds, rewritten)); return docs != null ? docs : Collections.emptyList(); } /** * 组合 RAG 场景的系统提示词:基础提示词 + 角色人设 + 检索到的资料。 * 资料为空时不声称“依据资料”,避免诱导模型编造。 */ private String buildRagSystemPrompt(String rolePrompt, String context) { String base = effectiveSystem(rolePrompt); if (!StringUtils.hasText(context)) { return base; } return base + "\n\n【RAG回答硬性规则】\n" + "- 只能依据下方【知识库资料】回答用户问题,不要调用模型自身记忆补充资料之外的内容。\n" + "- 回答诗词、制度、流程等具体内容时,必须从【知识库资料】中逐字引用或转述,不得使用未检索到的诗句、篇名、作者或步骤。\n" + "- 如果【知识库资料】中没有用户要求的内容,请明确说明未在当前知识库中检索到足够资料,并提示补充或调整知识库。\n" + "- 引用来源应能支撑回答内容;不要让回答内容和下方引用来源不一致。\n\n" + "【知识库资料】\n" + context; } private SearchRequest buildRagSearchRequest(int topK, Long categoryId) { List categoryIds = categoryId != null ? List.of(categoryId) : Collections.emptyList(); return buildRagSearchRequest(topK, categoryIds); } private SearchRequest buildRagSearchRequest(int topK, List categoryIds) { return ragSearchRequestBuilder(topK, categoryIds).build(); } /** * 带查询文本的检索请求,供手动向量检索(多路查询)使用。 * QuestionAnswerAdvisor 会自行设置查询文本,故那条路径不需要此重载。 */ private SearchRequest buildRagSearchRequest(int topK, List categoryIds, String query) { SearchRequest.Builder builder = ragSearchRequestBuilder(topK, categoryIds); if (StringUtils.hasText(query)) { builder.query(query); } return builder.build(); } private SearchRequest.Builder ragSearchRequestBuilder(int topK, List categoryIds) { SearchRequest.Builder builder = SearchRequest.builder() .similarityThreshold(0.0) .topK(topK); Filter.Expression filterExpression = buildCategoryFilterExpression(categoryIds); if (filterExpression != null) { builder.filterExpression(filterExpression); } return builder; } private Advisor buildRetrievalAdvisor(int topK, List categoryIds) { Filter.Expression filterExpression = buildCategoryFilterExpression(categoryIds); return RetrievalAugmentationAdvisor.builder() .documentRetriever(new VectorStoreDocumentRetriever( pgVectorVectorStore, 0.0, topK, () -> filterExpression)) .queryAugmenter(ContextualQueryAugmenter.builder() .allowEmptyContext(false) .build()) .build(); } private Filter.Expression buildCategoryFilterExpression(List categoryIds) { List values = normalizeCategoryIds(categoryIds).stream() .map(value -> (Object) value) .toList(); if (values.isEmpty()) { return null; } FilterExpressionBuilder filterBuilder = new FilterExpressionBuilder(); return filterBuilder.in("categoryId", values).build(); } private List normalizeCategoryIds(List categoryIds) { if (categoryIds == null || categoryIds.isEmpty()) { return Collections.emptyList(); } return categoryIds.stream() .filter(java.util.Objects::nonNull) .filter(id -> id > 0) .map(String::valueOf) .distinct() .toList(); } /** * 和 RAG 知识库进行对话(另外一种使用方式) * * @param message * @param chatId * @return */ public String doChatWithRagEnhance(String message, String chatId) { Advisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder() .documentRetriever(VectorStoreDocumentRetriever.builder() .vectorStore(pgVectorVectorStore) .similarityThreshold(0.5) .topK(4) .build()) .queryAugmenter(ContextualQueryAugmenter.builder() .allowEmptyContext(false) // 不允许模型在没有找到相关文档的情况下也生成回答 .build()) .build(); ChatResponse chatResponse = getChatClient("CHAT") .prompt() .user(message) .advisors(spec -> spec.param(CONVERSATION_ID, chatId)) // 应用 RAG 知识库问答 .advisors(retrievalAugmentationAdvisor) .call() .chatResponse(); return chatResponse.getResult().getOutput().getText(); } }