diff --git a/internal/ai/runtime/executor/answerability_gate.go b/internal/ai/runtime/executor/answerability_gate.go index cd2482e..3fa54e4 100644 --- a/internal/ai/runtime/executor/answerability_gate.go +++ b/internal/ai/runtime/executor/answerability_gate.go @@ -181,6 +181,11 @@ func (g *KnowledgeAnswerabilityGate) retrieveKnowledge(ctx context.Context, stat } gate := g.withDefaults() req := state.Input.Request + if isRuntimeActionIntent(req.UserMessage.Content) { + state.SkipGate = true + state.recordAnswerability(answerabilityStatusSkipped, "runtime action intent", nil) + return state, nil + } configuredKnowledgeIDs := utils.SplitInt64s(req.AIAgent.KnowledgeIDs) retriever := gate.newRetriever(req.AIAgent) if retriever == nil { @@ -232,6 +237,72 @@ func (g *KnowledgeAnswerabilityGate) retrieveKnowledge(ctx context.Context, stat return state, nil } +func isRuntimeActionIntent(content string) bool { + text := strings.ToLower(strings.TrimSpace(content)) + if text == "" { + return false + } + compact := strings.NewReplacer(" ", "", "\t", "", "\n", "", "\r", "").Replace(text) + handoffPhrases := []string{ + "我要转人工", + "帮我转人工", + "转人工", + "接人工", + "找人工", + "真人客服", + "humanagent", + "liveagent", + } + for _, phrase := range handoffPhrases { + if strings.Contains(compact, phrase) { + return true + } + } + if containsAny(compact, []string{"人工客服", "人工服务", "人工处理"}) && + !containsAny(compact, []string{"是什么", "怎么", "如何", "多少", "几", "吗", "?"}) && + (isShortActionPhrase(compact) || containsAny(compact, []string{"我要", "帮我", "请", "联系", "需要"})) { + return true + } + ticketPhrases := []string{ + "创建工单", + "新建工单", + "提交工单", + "发起工单", + "建工单", + "开工单", + "我要建单", + "帮我建单", + "创建ticket", + "createticket", + } + for _, phrase := range ticketPhrases { + if strings.Contains(compact, phrase) { + return true + } + } + if strings.Contains(compact, "工单") { + for _, action := range []string{"创建", "新建", "提交", "发起", "建", "开", "帮我", "我要", "请"} { + if strings.Contains(compact, action) { + return true + } + } + } + return false +} + +func containsAny(text string, values []string) bool { + for _, value := range values { + if strings.Contains(text, value) { + return true + } + } + return false +} + +func isShortActionPhrase(text string) bool { + return len([]rune(text)) <= 8 +} + func (g *KnowledgeAnswerabilityGate) gradeAnswerability(ctx context.Context, state *answerabilityGateState) (*answerabilityGateState, error) { if state == nil { return &answerabilityGateState{}, nil diff --git a/internal/ai/runtime/executor/answerability_gate_test.go b/internal/ai/runtime/executor/answerability_gate_test.go index 8736268..4df42e0 100644 --- a/internal/ai/runtime/executor/answerability_gate_test.go +++ b/internal/ai/runtime/executor/answerability_gate_test.go @@ -349,6 +349,64 @@ func TestBuildRunMessagesInjectsKnowledgeWhenGateAllows(t *testing.T) { } } +func TestBuildRunMessagesSkipsAnswerabilityGateForExplicitHandoffIntent(t *testing.T) { + summary := &RunResult{} + retriever := &fakeKnowledgeContextRetriever{ + knowledgeBaseIDs: []int64{1}, + err: errors.New("retriever should not be called for handoff intent"), + } + gate := newTestKnowledgeAnswerabilityGate(retriever, nil) + + messages := buildRunMessages(context.Background(), newAnswerabilityGateRunInput("我要转人工", "1"), summary, nil, gate) + + if summary.ReplyText != "" { + t.Fatalf("expected handoff intent to continue to agent/tool routing, got fallback %q", summary.ReplyText) + } + if retriever.lastQuery != "" { + t.Fatalf("expected retriever to be skipped for handoff intent, got query %q", retriever.lastQuery) + } + if !messagesContainContent(messages, "我要转人工") { + t.Fatalf("expected current user message in messages, got %#v", messages) + } +} + +func TestBuildRunMessagesSkipsAnswerabilityGateForExplicitTicketIntent(t *testing.T) { + summary := &RunResult{} + retriever := &fakeKnowledgeContextRetriever{ + knowledgeBaseIDs: []int64{1}, + err: errors.New("retriever should not be called for ticket intent"), + } + gate := newTestKnowledgeAnswerabilityGate(retriever, nil) + + messages := buildRunMessages(context.Background(), newAnswerabilityGateRunInput("帮我创建一个工单", "1"), summary, nil, gate) + + if summary.ReplyText != "" { + t.Fatalf("expected ticket intent to continue to agent/tool routing, got fallback %q", summary.ReplyText) + } + if retriever.lastQuery != "" { + t.Fatalf("expected retriever to be skipped for ticket intent, got query %q", retriever.lastQuery) + } + if !messagesContainContent(messages, "帮我创建一个工单") { + t.Fatalf("expected current user message in messages, got %#v", messages) + } +} + +func TestRuntimeActionIntentDoesNotMatchKnowledgeQuestions(t *testing.T) { + cases := []string{ + "人工客服服务时间是什么?", + "工单状态怎么查询?", + "需要查询工单状态怎么操作?", + } + + for _, tc := range cases { + t.Run(tc, func(t *testing.T) { + if isRuntimeActionIntent(tc) { + t.Fatalf("expected %q to stay in knowledge answerability flow", tc) + } + }) + } +} + func newTestKnowledgeAnswerabilityGate(retriever knowledgeContextRetriever, chatModel model.BaseChatModel) *KnowledgeAnswerabilityGate { return &KnowledgeAnswerabilityGate{ newRetriever: func(aiAgent models.AIAgent) knowledgeContextRetriever { diff --git a/internal/ai/runtime/executor/knowledge_guard.go b/internal/ai/runtime/executor/knowledge_guard.go index 47140b6..fd04326 100644 --- a/internal/ai/runtime/executor/knowledge_guard.go +++ b/internal/ai/runtime/executor/knowledge_guard.go @@ -57,7 +57,11 @@ func resolveKnowledgeHumanSupportFallback(aiAgent models.AIAgent) string { if strs.IsBlank(base) { base = "当前知识库暂无明确信息。" } - return base + humanSupport := "建议你联系人工客服进一步确认。" + if strings.Contains(base, humanSupport) { + return base + } + return base + " " + humanSupport } func buildKnowledgeRuntimeInstruction(answerMode enums.KnowledgeAnswerMode, fallbackReply string) string {