fix: bypass answerability gate for runtime actions

This commit is contained in:
mlogclub
2026-05-02 15:04:17 +08:00
parent b85e1a2557
commit a8f032d332
3 changed files with 134 additions and 1 deletions
@@ -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
@@ -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 {
@@ -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 {