本地 RAG 知识库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

781 lines
36 KiB

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<String, ChatClient> 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<String> allowedMcpTools) {
// 空列表降级为 ["*"](允许所有工具),与无参数版本行为一致
final List<String> 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<String> 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<String> doChatByStream(String message, String chatId) {
return doChatByStream(message, chatId, null);
}
/**
* AI 基础对话(多轮记忆 + 角色系统提示词,SSE 流式传输)
*
* @param message 用户消息(保持原样)
* @param chatId 会话ID
* @param systemPrompt 角色人设/风格,作为系统提示词叠加;为空则仅用基础提示词
* @return 流式回答
*/
public Flux<String> 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<String> doChatByStream(String message, String chatId, String systemPrompt, List<String> 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<String> appendMcpToolEvents(Flux<String> contentFlux) {
return contentFlux.concatMap(chunk -> Flux.just(chunk))
.concatWith(Flux.defer(() -> {
java.util.List<McpToolCallback.ToolCallEvent> events = McpToolCallback.drainEvents();
if (events.isEmpty()) {
return Flux.empty();
}
log.info("本次对话触发了 {} 个 MCP 工具调用", events.size());
List<String> 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<String> 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<Long> categoryIds = categoryId != null ? List.of(categoryId) : Collections.emptyList();
return doChatWithRagStrategy(message, chatId, strategy, categoryIds);
}
public String doChatWithRagStrategy(String message, String chatId, String strategy, List<Long> categoryIds) {
return doChatWithRagStrategy(message, chatId, strategy, categoryIds, null);
}
public String doChatWithRagStrategy(String message, String chatId, String strategy, List<Long> categoryIds, String systemPrompt) {
// FAQ 优先匹配:三级匹配(精确→关键词→语义)命中则直接返回标准答案
Optional<String> 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<Long> categoryIds,
String systemPrompt, List<String> allowedMcpTools) {
// FAQ 优先匹配
Optional<String> 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<Message> 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<String> doChatWithRagStrategyByStream(String message, String chatId, String strategy,
List<Long> categoryIds, String systemPrompt) {
// FAQ 优先匹配:命中则直接以流式形式返回标准答案
Optional<String> 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<String> doChatWithRagStrategyByStream(String message, String chatId, String strategy,
List<Long> categoryIds, String systemPrompt,
List<String> allowedMcpTools) {
// 重置 MCP 工具调用状态
McpToolCallback.resetEvents();
McpToolCallback.resetCallRounds();
// FAQ 优先匹配:命中则直接以流式形式返回标准答案
Optional<String> 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<Long> categoryIds, String systemPrompt) {
return doChatWithMultiQueryRag(message, chatId, categoryIds, systemPrompt, null);
}
private String doChatWithMultiQueryRag(String message, String chatId, List<Long> categoryIds,
String systemPrompt, List<String> 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<String> doChatWithMultiQueryRagByStream(String message, String chatId, List<Long> categoryIds, String systemPrompt) {
return doChatWithMultiQueryRagByStream(message, chatId, categoryIds, systemPrompt, null);
}
private Flux<String> doChatWithMultiQueryRagByStream(String message, String chatId, List<Long> categoryIds,
String systemPrompt, List<String> 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<Long> categoryIds, String systemPrompt) {
List<Document> 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<Document> retrieveMultiQueryDocs(String message, List<Long> categoryIds) {
// 多路扩展依赖 RAG_REWRITE 模型;扩展失败时退回原问题检索,避免整次 RAG 不可用。
List<String> 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<String, Document> merged = new LinkedHashMap<>();
for (String query : expandedQueries) {
if (!StringUtils.hasText(query) || merged.size() >= maxDocs) {
continue;
}
List<Document> 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<Document> retrieveRagSources(String message, String chatId, String strategy, List<Long> categoryIds) {
if (!StringUtils.hasText(message)) {
return Collections.emptyList();
}
if ("MULTI_QUERY".equalsIgnoreCase(strategy)) {
return retrieveMultiQueryDocs(message, categoryIds);
}
String rewritten = rewriteQuery(message, chatId, strategy);
List<Document> 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<Long> categoryIds = categoryId != null ? List.of(categoryId) : Collections.emptyList();
return buildRagSearchRequest(topK, categoryIds);
}
private SearchRequest buildRagSearchRequest(int topK, List<Long> categoryIds) {
return ragSearchRequestBuilder(topK, categoryIds).build();
}
/**
* 带查询文本的检索请求,供手动向量检索(多路查询)使用。
* QuestionAnswerAdvisor 会自行设置查询文本,故那条路径不需要此重载。
*/
private SearchRequest buildRagSearchRequest(int topK, List<Long> 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<Long> 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<Long> 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<Long> categoryIds) {
List<Object> 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<String> normalizeCategoryIds(List<Long> 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();
}
}