package com.wok.supportbot.app; import com.wok.supportbot.rag.RagContext; import com.wok.supportbot.rag.RagPipeline; import com.wok.supportbot.service.IntentRouter; import com.wok.supportbot.service.SystemConfigService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.document.Document; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Optional; /** * 统一对话管道(编排层)。 *

* 编排一次完整对话的决策流程:意图路由 → FAQ 优先 → RAG 检索 → 组装系统提示词与用户消息, * 产出 {@link ChatRequest} 交由 {@code AssistantApp} 执行实际的 {@code call()} / {@code stream()}。 *

* 设计说明:本类为纯编排层,不持有 ChatClient(ChatClient 构建与 Advisor 链装配仍在 * {@code AssistantApp}),因此 {@code call} / {@code stream} 由 {@code AssistantApp} 承担, * 避免 {@code ChatPipeline} ↔ {@code AssistantApp} 循环依赖。 *

* 接入 {@link IntentRouter} 替代原 {@code AiController.shouldBypassKnowledgeRetrieval} 的硬编码寒暄词判断: * 寒暄词列表保留为快速路径与兜底,IntentRouter 负责细粒度意图分类,二者命中其一即跳过 KB 检索。 *

* {@code @pipeline} orchestration-layer order=0
* {@code @pipeline-step} buildRequest: 意图路由 → FAQ优先 → RAG检索 → 提示词组装
* {@code @pipeline-step} routeIntent: 寒暄词快速路径 → IntentRouter LLM分类 → 降级RAG
* {@code @pipeline-step} effectiveSystem: DB全局提示词 + 角色人设 动态组合
* 同步至: frontend/src/views/PipelineFlow.vue, CLAUDE.md ASCII管道图 */ @Component @Slf4j public class ChatPipeline { /** IntentRouter 判为 CHITCHAT 的置信度阈值,低于此值视为不确定,继续走 RAG */ private static final double CHITCHAT_CONFIDENCE_THRESHOLD = 0.6; /** FAQ 意图高置信度阈值:IntentRouter 返回 FAQ 且高于此值时,仅走 FAQ 匹配,不降级 RAG */ private static final double FAQ_HIGH_CONFIDENCE_THRESHOLD = 0.8; @Resource private IntentRouter intentRouter; @Resource private RagPipeline ragPipeline; @Resource private SystemConfigService systemConfigService; /** * 编排一次对话请求,产出执行决策。 *

* 决策分支: *

* * @param ctx 对话上下文 * @return 执行决策 */ public ChatRequest buildRequest(ChatContext ctx) { String baseSystem = effectiveSystem(ctx.systemPrompt()); // 普通对话:enableRag=false(含 Controller 层 isKbDenied 强制置 false 的情况) if (!ctx.enableRag()) { return new ChatRequest(ctx, ctx.message(), baseSystem, Optional.empty()); } // 意图路由:先用 IntentRouter 做细粒度分类 IntentRouter.IntentResult intent = routeIntent(ctx.message()); // FAQ 高置信度:优先匹配标准答案;未命中时降级到 RAG 检索,避免知识库中已有答案却返回兜底提示 if (intent != null && "FAQ".equals(intent.getIntent()) && intent.getConfidence() >= FAQ_HIGH_CONFIDENCE_THRESHOLD) { Optional faqAnswer = ragPipeline.tryFaqMatch(ctx.message()); if (faqAnswer.isPresent()) { log.info("FAQ 高置信({}),命中标准答案: chatId={}", intent.getConfidence(), ctx.chatId()); return new ChatRequest(ctx, ctx.message(), baseSystem, faqAnswer); } log.info("FAQ 高置信({}) 未命中标准答案,降级到 RAG 检索: chatId={}", intent.getConfidence(), ctx.chatId()); } // 寒暄/闲聊:IntentRouter 判定 CHITCHAT 高置信,跳过 KB 检索 if (intent != null && "CHITCHAT".equals(intent.getIntent()) && intent.getConfidence() >= CHITCHAT_CONFIDENCE_THRESHOLD) { return new ChatRequest(ctx, ctx.message(), baseSystem, Optional.empty()); } // RAG 检索(含 FAQ 优先匹配) RagContext rag = ragPipeline.retrieve(ctx); if (rag.faqHit()) { return new ChatRequest(ctx, ctx.message(), baseSystem, rag.faqAnswer()); } // RAG 生成:资料块注入 system,重写后查询作为 user 消息 String finalSystem = baseSystem + ragPipeline.buildRagContextBlock(rag.contextText()); return new ChatRequest(ctx, rag.rewrittenQuery(), finalSystem, Optional.empty()); } /** * 意图路由:先用寒暄词列表做快速路径,未命中再调 IntentRouter 做 LLM 分类。 * 异常时返回 null(调用方默认走 RAG 检索)。 */ private IntentRouter.IntentResult routeIntent(String message) { // 寒暄词快速路径(零 LLM 开销) if (isChitchat(message)) { return new IntentRouter.IntentResult("CHITCHAT", 1.0); } try { return intentRouter.route(message); } catch (Exception e) { log.debug("意图路由异常,沿用 RAG 检索: {}", e.getMessage()); return null; } } /** * 判断是否跳过 KB 检索(保留旧方法签名供 retrieveSources 等使用)。 */ private boolean shouldBypassRag(String message) { if (isChitchat(message)) { return true; } try { IntentRouter.IntentResult intent = intentRouter.route(message); return "CHITCHAT".equals(intent.getIntent()) && intent.getConfidence() >= CHITCHAT_CONFIDENCE_THRESHOLD; } catch (Exception e) { log.debug("意图路由异常,沿用 RAG: {}", e.getMessage()); } return false; } /** * 寒暄词快速判断:问候/感谢/告别等短消息无知识库检索意图。 * 与原 {@code AiController.shouldBypassKnowledgeRetrieval} 逻辑一致,作为 IntentRouter 的快速路径与兜底。 *

* 供 {@code buildRequest} 与"引用来源"等不需 LLM 意图分类的场景共用。 */ public boolean isChitchat(String message) { if (!StringUtils.hasText(message)) { return true; } String normalized = message.trim() .toLowerCase(Locale.ROOT) .replaceAll("[\\s,。!?!?,.;;::、~~…]+", ""); if (normalized.isEmpty()) { return true; } if (normalized.length() > 12) { return false; } return List.of( "你好", "您好", "hello", "hi", "哈喽", "嗨", "在吗", "早上好", "上午好", "中午好", "下午好", "晚上好", "谢谢", "感谢", "多谢", "好的", "好", "嗯", "嗯嗯", "再见", "拜拜", "辛苦了" ).contains(normalized); } /** * 组合系统提示词:全局系统提示词(从 system_config 动态读取) + 角色人设。 * 修改 system_config 中对应配置后即时生效,无需重启。 *

* 注意:推荐问题(suggest-message-list)已不再由主回复生成, * 改由 {@link SuggestionGenerator} 在 AI 回复结束后按需异步生成。 */ private String effectiveSystem(String rolePrompt) { StringBuilder sb = new StringBuilder(); // 全局系统提示词(从 DB system_config 表动态读取) String globalPrompt = systemConfigService.getValueByKey("ai_system_prompt"); if (StringUtils.hasText(globalPrompt)) { sb.append(globalPrompt); } // 角色人设 if (StringUtils.hasText(rolePrompt)) { if (sb.length() > 0) { sb.append("\n\n"); } sb.append("【当前角色设定】\n").append(rolePrompt); } return sb.toString(); } /** * 统一检索引用来源(不含 FAQ 匹配与意图路由)。 * 寒暄词无 KB 检索意图时直接返回空列表,避免向量库召回"最相似但实际无关"的片段。 * * @param ctx 对话上下文(使用 message / rewriteStrategy / categoryIds) * @return 命中的知识库片段,寒暄词或无命中时返回空列表 */ public List retrieveSources(ChatContext ctx) { if (!StringUtils.hasText(ctx.message()) || isChitchat(ctx.message())) { return Collections.emptyList(); } return ragPipeline.retrieveDocuments(ctx); } }