diff --git a/frontend/src/views/PipelineFlow.vue b/frontend/src/views/PipelineFlow.vue
index cbaacd3..c9a28e0 100644
--- a/frontend/src/views/PipelineFlow.vue
+++ b/frontend/src/views/PipelineFlow.vue
@@ -78,7 +78,7 @@ mermaid.initialize({
// Mermaid 流程图 DSL 定义
// 节点类型: [矩形]=处理步骤, {菱形}=决策分支, subgraph=子系统
-// %%graph-meta: { updated: "2026-08-04", basedOn: "ChatPipeline v3, RagPipeline v2, AssistantApp v2", mermaidVersion: "flowchart-v2" }
+// %%graph-meta: { updated: "2026-08-27", basedOn: "ChatPipeline v3, RagPipeline v2, AssistantApp v2", mermaidVersion: "flowchart-v2" }
const GRAPH_DEFINITION = `
flowchart TD
A["用户请求
message + roleId + accountId + chatId"]
@@ -97,7 +97,7 @@ flowchart TD
G -- "FAQ
confidence ≧ 0.8" --> H["FaqMatchEngine
三级匹配策略
精确 → 关键词 → 向量语义"]
- G -- "CHITCHAT
confidence ≧ 0.6" --> I["模式: 纯对话
跳过知识库检索
不注入资料块"]
+ G -- "CHITCHAT
confidence ≧ 0.6" --> CHK["闲聊前 FAQ 精准匹配
先试 FaqMatchEngine
命中则短路返回"]
G -- "RAG / 降级
其余情况" --> J["RagPipeline.retrieve
RAG 检索流水线入口"]
@@ -111,6 +111,8 @@ flowchart TD
M --> S["4. 构建资料块
拼接检索文档
注入 system prompt 末尾"]
end
+ CHK -. "❌ 未命中 → 纯对话" .-> I["模式: 纯对话
跳过知识库检索
不注入资料块"]
+ CHK -- "✅ 命中标准答案" --> T
I --> T
S --> T
E --> T
diff --git a/src/main/java/com/wok/supportbot/app/ChatPipeline.java b/src/main/java/com/wok/supportbot/app/ChatPipeline.java
index 9c7b792..302f8ea 100644
--- a/src/main/java/com/wok/supportbot/app/ChatPipeline.java
+++ b/src/main/java/com/wok/supportbot/app/ChatPipeline.java
@@ -88,7 +88,7 @@ public class ChatPipeline {
// FAQ 高置信度:优先匹配标准答案;未命中时降级到 RAG 检索,避免知识库中已有答案却返回兜底提示
if (intent != null && "FAQ".equals(intent.getIntent())
&& intent.getConfidence() >= FAQ_HIGH_CONFIDENCE_THRESHOLD) {
- Optional faqMatch = ragPipeline.tryFaqMatchResult(ctx.message());
+ Optional faqMatch = ragPipeline.tryFaqMatchResult(ctx.message(), ctx.categoryIds());
if (faqMatch.isPresent()) {
log.info("FAQ 高置信({}),命中标准答案: chatId={}", intent.getConfidence(), ctx.chatId());
Optional faqAnswer = Optional.ofNullable(faqMatch.get().getFaq().getAnswer());
@@ -101,6 +101,14 @@ public class ChatPipeline {
// 寒暄/闲聊:IntentRouter 判定 CHITCHAT 高置信,跳过 KB 检索
if (intent != null && "CHITCHAT".equals(intent.getIntent())
&& intent.getConfidence() >= CHITCHAT_CONFIDENCE_THRESHOLD) {
+ // 闲聊前先尝试 FAQ 精准匹配,避免"你是谁"等被配置成 FAQ 后命中不了
+ Optional faqMatch = ragPipeline.tryFaqMatchResult(ctx.message(), ctx.categoryIds());
+ if (faqMatch.isPresent()) {
+ log.info("闲聊意图但 FAQ 命中标准答案: chatId={}, matchType={}", ctx.chatId(), faqMatch.get().getMatchType());
+ return new ChatRequest(ctx, ctx.message(), baseSystem,
+ Optional.ofNullable(faqMatch.get().getFaq().getAnswer()),
+ globalPrompt, null, null, "FAQ", null, null, faqMatch.get());
+ }
return new ChatRequest(ctx, ctx.message(), baseSystem, Optional.empty(),
globalPrompt, null, null, "CHITCHAT", null, null, null);
}
diff --git a/src/main/java/com/wok/supportbot/rag/RagPipeline.java b/src/main/java/com/wok/supportbot/rag/RagPipeline.java
index 8767b4a..c6f4b23 100644
--- a/src/main/java/com/wok/supportbot/rag/RagPipeline.java
+++ b/src/main/java/com/wok/supportbot/rag/RagPipeline.java
@@ -109,7 +109,7 @@ public class RagPipeline {
*/
public RagContext retrieve(ChatContext ctx) {
// 1. FAQ 优先匹配:命中则直接返回标准答案,跳过检索与生成
- Optional faqMatch = tryFaqMatchResult(ctx.message());
+ Optional faqMatch = tryFaqMatchResult(ctx.message(), ctx.categoryIds());
if (faqMatch.isPresent()) {
log.info("FAQ 命中,跳过知识库检索: chatId={}, matchType={}", ctx.chatId(), faqMatch.get().getMatchType());
String answer = faqMatch.get().getFaq().getAnswer();
@@ -150,7 +150,7 @@ public class RagPipeline {
}
// RAG 命中日志:记录本次检索的命中/未命中情况
- logRagHit(ctx.chatId(), ctx.message(), docs, rewrittenQuery);
+ logRagHit(ctx.chatId(), ctx.message(), docs, currentSearchMode());
String contextText = joinContext(docs);
return new RagContext(Optional.empty(), docs, contextText, rewrittenQuery, currentSearchMode(), null);
@@ -177,10 +177,13 @@ public class RagPipeline {
/**
* 尝试 FAQ 三级匹配(精确→关键词→语义),命中返回完整匹配结果(含 matchType/score)。
* 异常时降级为未命中。
+ *
+ * @param message 用户问题
+ * @param categoryIds 角色授权分类 ID 列表(null/空表示不限制)
*/
- public Optional tryFaqMatchResult(String message) {
+ public Optional tryFaqMatchResult(String message, List categoryIds) {
try {
- return faqMatchEngine.match(message);
+ return faqMatchEngine.match(message, categoryIds);
} catch (Exception e) {
log.warn("FAQ 匹配异常,降级到 RAG: {}", e.getMessage());
return Optional.empty();
@@ -190,9 +193,12 @@ public class RagPipeline {
/**
* 尝试 FAQ 三级匹配,命中返回标准答案(仅答案文本)。
* 异常时降级为未命中,供仅需答案的调用方使用。
+ *
+ * @param message 用户问题
+ * @param categoryIds 角色授权分类 ID 列表(null/空表示不限制)
*/
- public Optional tryFaqMatch(String message) {
- return tryFaqMatchResult(message).map(result -> result.getFaq().getAnswer());
+ public Optional tryFaqMatch(String message, List categoryIds) {
+ return tryFaqMatchResult(message, categoryIds).map(result -> result.getFaq().getAnswer());
}
/**
diff --git a/src/main/java/com/wok/supportbot/service/FaqMatchEngine.java b/src/main/java/com/wok/supportbot/service/FaqMatchEngine.java
index cc180fe..bc1c3b4 100644
--- a/src/main/java/com/wok/supportbot/service/FaqMatchEngine.java
+++ b/src/main/java/com/wok/supportbot/service/FaqMatchEngine.java
@@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.wok.supportbot.config.EmbeddingModelFactory;
import com.wok.supportbot.dao.KnowledgeFaqMapper;
import com.wok.supportbot.entity.KnowledgeFaq;
+import com.wok.supportbot.rag.CategoryFilter;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
@@ -37,6 +38,9 @@ public class FaqMatchEngine {
@Autowired
private EmbeddingModelFactory embeddingModelFactory;
+ @Autowired
+ private CategoryFilter categoryFilter;
+
/** 向量维度,与 PgVectorStore 保持一致 */
@Value("${knowledge.vector.dimension:1024}")
private int vectorDimension;
@@ -68,31 +72,32 @@ public class FaqMatchEngine {
/**
* 对用户问题进行三级匹配
*
- * @param question 用户问题
+ * @param question 用户问题
+ * @param categoryIds 角色授权分类 ID 列表(null/空表示不限制),未设置分类的 FAQ 一并可见
* @return 匹配结果(可能为空)
*/
- public Optional match(String question) {
+ public Optional match(String question, List categoryIds) {
if (question == null || question.isBlank()) {
return Optional.empty();
}
String trimmedQuestion = question.trim();
// 第一级:精确匹配
- Optional exactResult = exactMatch(trimmedQuestion);
+ Optional exactResult = exactMatch(trimmedQuestion, categoryIds);
if (exactResult.isPresent()) {
log.info("FAQ 精确匹配命中: question={}", trimmedQuestion);
return exactResult;
}
// 第二级:关键词匹配
- Optional keywordResult = keywordMatch(trimmedQuestion);
+ Optional keywordResult = keywordMatch(trimmedQuestion, categoryIds);
if (keywordResult.isPresent()) {
log.info("FAQ 关键词匹配命中: question={}", trimmedQuestion);
return keywordResult;
}
// 第三级:语义匹配
- Optional semanticResult = semanticMatch(trimmedQuestion);
+ Optional semanticResult = semanticMatch(trimmedQuestion, categoryIds);
if (semanticResult.isPresent()) {
log.info("FAQ 语义匹配命中: question={}, score={}", trimmedQuestion, semanticResult.get().getScore());
return semanticResult;
@@ -107,12 +112,16 @@ public class FaqMatchEngine {
/**
* 精确匹配:问题文本完全一致
*/
- private Optional exactMatch(String question) {
+ private Optional exactMatch(String question, List categoryIds) {
try {
+ List