|
|
@ -2,49 +2,26 @@ package com.wok.supportbot.app; |
|
|
|
|
|
|
|
|
import com.wok.supportbot.advisor.ContentSafetyAdvisor; |
|
|
import com.wok.supportbot.advisor.ContentSafetyAdvisor; |
|
|
import com.wok.supportbot.advisor.MyLoggerAdvisor; |
|
|
import com.wok.supportbot.advisor.MyLoggerAdvisor; |
|
|
import com.wok.supportbot.advisor.ReReadingAdvisor; |
|
|
|
|
|
import com.wok.supportbot.chatmemory.DatabaseChatMemory; |
|
|
import com.wok.supportbot.chatmemory.DatabaseChatMemory; |
|
|
import com.wok.supportbot.config.ChatModelFactory; |
|
|
import com.wok.supportbot.config.ChatModelFactory; |
|
|
import com.wok.supportbot.config.RagPromptConfig; |
|
|
|
|
|
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.McpToolCallback; |
|
|
import com.wok.supportbot.mcp.McpToolCallbackAdapter; |
|
|
import com.wok.supportbot.mcp.McpToolCallbackAdapter; |
|
|
import jakarta.annotation.Resource; |
|
|
import jakarta.annotation.Resource; |
|
|
import lombok.extern.slf4j.Slf4j; |
|
|
import lombok.extern.slf4j.Slf4j; |
|
|
import org.springframework.ai.chat.client.ChatClient; |
|
|
import org.springframework.ai.chat.client.ChatClient; |
|
|
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; |
|
|
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.ChatModel; |
|
|
import org.springframework.ai.chat.model.ChatResponse; |
|
|
|
|
|
import org.springframework.ai.chat.prompt.PromptTemplate; |
|
|
|
|
|
import org.springframework.ai.document.Document; |
|
|
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.tool.ToolCallback; |
|
|
import org.springframework.ai.vectorstore.SearchRequest; |
|
|
|
|
|
import org.springframework.ai.vectorstore.VectorStore; |
|
|
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.beans.factory.annotation.Value; |
|
|
import org.springframework.stereotype.Component; |
|
|
import org.springframework.stereotype.Component; |
|
|
import org.springframework.util.StringUtils; |
|
|
import org.springframework.util.StringUtils; |
|
|
import reactor.core.publisher.Flux; |
|
|
import reactor.core.publisher.Flux; |
|
|
|
|
|
|
|
|
import java.util.ArrayList; |
|
|
import java.util.ArrayList; |
|
|
import java.util.Collections; |
|
|
|
|
|
import java.util.LinkedHashMap; |
|
|
|
|
|
import java.util.List; |
|
|
import java.util.List; |
|
|
import java.util.Map; |
|
|
|
|
|
import java.util.Optional; |
|
|
|
|
|
import java.util.concurrent.ConcurrentHashMap; |
|
|
import java.util.concurrent.ConcurrentHashMap; |
|
|
import java.util.stream.Collectors; |
|
|
|
|
|
|
|
|
|
|
|
import static org.springframework.ai.chat.memory.ChatMemory.CONVERSATION_ID; |
|
|
import static org.springframework.ai.chat.memory.ChatMemory.CONVERSATION_ID; |
|
|
|
|
|
|
|
|
@ -59,21 +36,14 @@ import static org.springframework.ai.chat.memory.ChatMemory.CONVERSATION_ID; |
|
|
@Slf4j |
|
|
@Slf4j |
|
|
public class AssistantApp { |
|
|
public class AssistantApp { |
|
|
|
|
|
|
|
|
@Resource |
|
|
|
|
|
private VectorStore pgVectorVectorStore; |
|
|
|
|
|
|
|
|
|
|
|
@Resource |
|
|
@Resource |
|
|
private ContentSafetyAdvisor contentSafetyAdvisor; |
|
|
private ContentSafetyAdvisor contentSafetyAdvisor; |
|
|
|
|
|
|
|
|
@Resource |
|
|
|
|
|
private FaqMatchEngine faqMatchEngine; |
|
|
|
|
|
|
|
|
|
|
|
@Resource |
|
|
@Resource |
|
|
private McpToolCallbackAdapter mcpToolCallbackAdapter; |
|
|
private McpToolCallbackAdapter mcpToolCallbackAdapter; |
|
|
|
|
|
|
|
|
/** RAG 回答规则(保真护栏),通过 application.yml 的 knowledge.rag.answer-rules 配置 */ |
|
|
|
|
|
@Resource |
|
|
@Resource |
|
|
private RagPromptConfig ragPromptConfig; |
|
|
|
|
|
|
|
|
private ChatPipeline chatPipeline; |
|
|
|
|
|
|
|
|
/** MCP 工具开关,默认启用,可通过 application.yml 的 chat.mcp.enabled 关闭 */ |
|
|
/** MCP 工具开关,默认启用,可通过 application.yml 的 chat.mcp.enabled 关闭 */ |
|
|
@Value("${chat.mcp.enabled:true}") |
|
|
@Value("${chat.mcp.enabled:true}") |
|
|
@ -158,119 +128,82 @@ public class AssistantApp { |
|
|
log.info("AssistantApp ChatClient cache cleared"); |
|
|
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 基础对话(支持多轮对话记忆 + 角色系统提示词) |
|
|
|
|
|
|
|
|
* 同步对话(新入口,委托 {@link ChatPipeline} 编排)。 |
|
|
* |
|
|
* |
|
|
* @param message 用户消息(保持原样,不做包装,避免污染会话记忆) |
|
|
|
|
|
* @param chatId 会话ID |
|
|
|
|
|
* @param systemPrompt 角色人设/风格,作为系统提示词叠加在基础提示词之上;为空则仅用基础提示词 |
|
|
|
|
|
* @return AI 回答 |
|
|
|
|
|
|
|
|
* @param ctx 对话上下文 |
|
|
|
|
|
* @return AI 回答文本 |
|
|
*/ |
|
|
*/ |
|
|
public String doChat(String message, String chatId, String systemPrompt) { |
|
|
|
|
|
return doChat(message, chatId, systemPrompt, null); |
|
|
|
|
|
|
|
|
public String chat(ChatContext ctx) { |
|
|
|
|
|
return chatWithEvents(ctx).text(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* AI 基础对话(支持多轮对话记忆 + 角色系统提示词 + MCP 工具权限) |
|
|
|
|
|
|
|
|
* 同步对话 + MCP 工具调用事件(新入口)。 |
|
|
|
|
|
* 比 {@link #chat(ChatContext)} 多返回本次触发的工具调用事件,供需要展示调用过程的场景。 |
|
|
* |
|
|
* |
|
|
* @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(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 组合系统提示词:基础客服提示词 + 角色人设。 |
|
|
|
|
|
* 角色人设作为附加段落叠加,既保留客服基线约束,又让角色风格生效。 |
|
|
|
|
|
|
|
|
* @param ctx 对话上下文 |
|
|
|
|
|
* @return 回答文本 + MCP 事件 |
|
|
*/ |
|
|
*/ |
|
|
private String effectiveSystem(String rolePrompt) { |
|
|
|
|
|
if (!StringUtils.hasText(rolePrompt)) { |
|
|
|
|
|
return SYSTEM_PROMPT; |
|
|
|
|
|
|
|
|
public ChatResult chatWithEvents(ChatContext ctx) { |
|
|
|
|
|
ChatRequest req = chatPipeline.buildRequest(ctx); |
|
|
|
|
|
if (req.faqHit()) { |
|
|
|
|
|
return new ChatResult(req.faqAnswer().get(), List.of()); |
|
|
} |
|
|
} |
|
|
return SYSTEM_PROMPT + "\n\n【当前角色设定】\n" + rolePrompt; |
|
|
|
|
|
|
|
|
McpToolCallback.resetEvents(); |
|
|
|
|
|
McpToolCallback.resetCallRounds(); |
|
|
|
|
|
ChatClient.ChatClientRequestSpec spec = getChatClient(ctx.appType(), ctx.allowedMcpTools()) |
|
|
|
|
|
.prompt() |
|
|
|
|
|
.user(req.finalMessage()) |
|
|
|
|
|
.advisors(s -> s.param(CONVERSATION_ID, ctx.chatId())); |
|
|
|
|
|
if (StringUtils.hasText(req.finalSystemPrompt())) { |
|
|
|
|
|
spec = spec.system(req.finalSystemPrompt()); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* AI 基础对话(支持多轮对话记忆,SSE 流式传输) |
|
|
|
|
|
* |
|
|
|
|
|
* @param message |
|
|
|
|
|
* @param chatId |
|
|
|
|
|
* @return |
|
|
|
|
|
*/ |
|
|
|
|
|
public Flux<String> doChatByStream(String message, String chatId) { |
|
|
|
|
|
return doChatByStream(message, chatId, null); |
|
|
|
|
|
|
|
|
String text = spec.call().chatResponse().getResult().getOutput().getText(); |
|
|
|
|
|
return new ChatResult(text, McpToolCallback.drainEvents()); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* AI 基础对话(多轮记忆 + 角色系统提示词,SSE 流式传输) |
|
|
|
|
|
|
|
|
* 流式对话(新入口,委托 {@link ChatPipeline} 编排)。 |
|
|
|
|
|
* 自动重置 MCP 事件并在流末尾追加工具调用事件行。 |
|
|
* |
|
|
* |
|
|
* @param message 用户消息(保持原样) |
|
|
|
|
|
* @param chatId 会话ID |
|
|
|
|
|
* @param systemPrompt 角色人设/风格,作为系统提示词叠加;为空则仅用基础提示词 |
|
|
|
|
|
|
|
|
* @param ctx 对话上下文 |
|
|
* @return 流式回答 |
|
|
* @return 流式回答 |
|
|
*/ |
|
|
*/ |
|
|
public Flux<String> doChatByStream(String message, String chatId, String systemPrompt) { |
|
|
|
|
|
ChatClient.ChatClientRequestSpec spec = getChatClient("CHAT") |
|
|
|
|
|
|
|
|
public Flux<String> chatStream(ChatContext ctx) { |
|
|
|
|
|
ChatRequest req = chatPipeline.buildRequest(ctx); |
|
|
|
|
|
if (req.faqHit()) { |
|
|
|
|
|
return Flux.just(req.faqAnswer().get()); |
|
|
|
|
|
} |
|
|
|
|
|
McpToolCallback.resetEvents(); |
|
|
|
|
|
McpToolCallback.resetCallRounds(); |
|
|
|
|
|
ChatClient.ChatClientRequestSpec spec = getChatClient(ctx.appType(), ctx.allowedMcpTools()) |
|
|
.prompt() |
|
|
.prompt() |
|
|
.user(message) |
|
|
|
|
|
.advisors(s -> s.param(CONVERSATION_ID, chatId)); |
|
|
|
|
|
if (StringUtils.hasText(systemPrompt)) { |
|
|
|
|
|
spec = spec.system(effectiveSystem(systemPrompt)); |
|
|
|
|
|
|
|
|
.user(req.finalMessage()) |
|
|
|
|
|
.advisors(s -> s.param(CONVERSATION_ID, ctx.chatId())); |
|
|
|
|
|
if (StringUtils.hasText(req.finalSystemPrompt())) { |
|
|
|
|
|
spec = spec.system(req.finalSystemPrompt()); |
|
|
} |
|
|
} |
|
|
return spec.stream().content(); |
|
|
|
|
|
|
|
|
return appendMcpToolEvents(spec.stream().content()); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* AI 基础对话(多轮记忆 + 角色系统提示词 + MCP 工具权限,SSE 流式传输) |
|
|
|
|
|
|
|
|
* 统一检索引用来源(新入口,委托 {@link ChatPipeline#retrieveSources})。 |
|
|
* |
|
|
* |
|
|
* @param message 用户消息 |
|
|
|
|
|
* @param chatId 会话ID |
|
|
|
|
|
* @param systemPrompt 角色人设/风格 |
|
|
|
|
|
* @param allowedMcpTools 允许的 MCP 工具列表;null=不注册工具 |
|
|
|
|
|
* @return 流式回答 |
|
|
|
|
|
|
|
|
* @param ctx 对话上下文(使用 message / rewriteStrategy / categoryIds) |
|
|
|
|
|
* @return 命中的知识库片段,含 metadata(documentId/title/sourceName/chunkIndex/distance) |
|
|
*/ |
|
|
*/ |
|
|
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()); |
|
|
|
|
|
|
|
|
public List<Document> retrieveSources(ChatContext ctx) { |
|
|
|
|
|
return chatPipeline.retrieveSources(ctx); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// ==================== 内部工具方法 ==================== |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 在 SSE 文本流末尾追加 MCP 工具调用事件 |
|
|
|
|
|
* 前端通过解析 "event: tool_call_start" / "event: tool_call_result" 行来展示工具调用过程。 |
|
|
|
|
|
* 事件格式遵循 SSE 标准,与 readSSEStreamWithEvents 的解析逻辑匹配。 |
|
|
|
|
|
|
|
|
* 在 SSE 文本流末尾追加 MCP 工具调用事件行。 |
|
|
|
|
|
* 事件格式遵循 SSE 标准,前端由 readSSEStreamWithEvents 解析。 |
|
|
*/ |
|
|
*/ |
|
|
private Flux<String> appendMcpToolEvents(Flux<String> contentFlux) { |
|
|
private Flux<String> appendMcpToolEvents(Flux<String> contentFlux) { |
|
|
return contentFlux.concatMap(chunk -> Flux.just(chunk)) |
|
|
return contentFlux.concatMap(chunk -> Flux.just(chunk)) |
|
|
@ -300,491 +233,4 @@ public class AssistantApp { |
|
|
return s.replace("\\", "\\\\").replace("\"", "\\\"") |
|
|
return s.replace("\\", "\\\\").replace("\"", "\\\"") |
|
|
.replace("\n", "\\n").replace("\r", "\\r"); |
|
|
.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" |
|
|
|
|
|
+ ragPromptConfig.getAnswerRules() + "\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); |
|
|
|
|
|
// 自定义模板替换 Spring AI 默认模板,注入与 MULTI_QUERY 路径一致的 RAG 回答规则 |
|
|
|
|
|
PromptTemplate qaTemplate = new PromptTemplate( |
|
|
|
|
|
"【RAG回答硬性规则】\n" + ragPromptConfig.getAnswerRules() + "\n\n" |
|
|
|
|
|
+ "【知识库资料】\n---------------------\n{context}\n---------------------\n\n" |
|
|
|
|
|
+ "用户问题:{query}\n\n请按上述规则回答:"); |
|
|
|
|
|
return RetrievalAugmentationAdvisor.builder() |
|
|
|
|
|
.documentRetriever(new VectorStoreDocumentRetriever( |
|
|
|
|
|
pgVectorVectorStore, |
|
|
|
|
|
0.0, |
|
|
|
|
|
topK, |
|
|
|
|
|
() -> filterExpression)) |
|
|
|
|
|
.queryAugmenter(ContextualQueryAugmenter.builder() |
|
|
|
|
|
.allowEmptyContext(false) |
|
|
|
|
|
.promptTemplate(qaTemplate) |
|
|
|
|
|
.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(); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
} |