merge: fix/intent-smalltalk-routing-2026-09-23 (smalltalk → casual chat)
This commit is contained in:
@@ -298,3 +298,32 @@ func TestDialogueLLM_QueryProgressNoBriefFallsBackToSynthesizedTemplate(t *testi
|
||||
t.Fatalf("expected synthesized baseline brief in fallback result")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDialogueLLM_AmbiguousSmalltalkDoesNotUseStageSalesTemplate 模糊分支若仍收到通用问句,
|
||||
// 即使 LLM 失败也不得回退到「掂量」阶段推销模板,应改道闲聊问候模板。
|
||||
func TestDialogueLLM_AmbiguousSmalltalkDoesNotUseStageSalesTemplate(t *testing.T) {
|
||||
backend := &scriptedDialogueBackend{fail: true}
|
||||
engine := NewCoordinatorEngineWithDialogue(nil, backend, t.TempDir())
|
||||
userCtx := &UserProjectContext{
|
||||
Project: ProjectInfo{Name: "全自动冲击波治疗仪", CurrentStage: maturity.StageV01_Triage},
|
||||
}
|
||||
rec := &intent.IntentRecognitionResult{
|
||||
RawInput: "今天天气怎么样",
|
||||
Branch: intent.BranchAmbiguousFallback,
|
||||
TopIntent: &intent.CandidateIntent{
|
||||
Intent: intent.IntentCasualChat,
|
||||
Confidence: 0.2,
|
||||
Branch: intent.BranchAmbiguousFallback,
|
||||
},
|
||||
}
|
||||
res, err := engine.ProcessIntent(context.Background(), rec, userCtx)
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessIntent: %v", err)
|
||||
}
|
||||
if strings.Contains(res.NaturalReply, "掂量") {
|
||||
t.Fatalf("must not use stage-sales consult template, got: %s", res.NaturalReply)
|
||||
}
|
||||
if !strings.Contains(res.NaturalReply, "项目助手") {
|
||||
t.Fatalf("expected casual-chat template fallback, got: %s", res.NaturalReply)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,11 @@ func (e *CoordinatorEngine) handleAmbiguousConsult(ctx context.Context, cand *in
|
||||
return e.handleQueryProgress(ctx, cand, userCtx, rawInput)
|
||||
}
|
||||
|
||||
// 通用非业务问句不得吃阶段推销模板;改道闲聊处理器(LLM 优先,失败回退问候模板)。
|
||||
if intent.IsSmallTalkOrGeneralQuestion(rawInput) {
|
||||
return e.handleCasualChat(ctx, userCtx, rawInput)
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("完全理解您想再掂量一下。针对「%s」目前的节点,我帮您快速对照一下:\n\n", projName))
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ func detectDialogueAct(input string) DialogueAct {
|
||||
askMarkers := []string{
|
||||
"吗", "呢", "谁", "是否", "有没有", "通过了吗", "了吗", "了没",
|
||||
"什么状态", "当前状态", "查询", "看看", "问一下", "了解一下",
|
||||
"怎么样", "怎么", "什么是", "为什么", "哪些", "多少", "好吗",
|
||||
}
|
||||
hasAskPunct := strings.Contains(trimmed, "?") || strings.Contains(trimmed, "?")
|
||||
for _, m := range askMarkers {
|
||||
|
||||
@@ -257,6 +257,10 @@ func (r *RuleBasedRecognizer) recognizeCore(ctx context.Context, input string, u
|
||||
if isGreetingOrHelp(trimmed) {
|
||||
chatCand.Confidence = 0.85
|
||||
chatCand.Reasoning = "匹配到问候语或通用帮助请求"
|
||||
} else if IsSmallTalkOrGeneralQuestion(trimmed) {
|
||||
// 抬高至 ≥0.6,避免落入 AMBIGUOUS_FALLBACK 的阶段推销模板
|
||||
chatCand.Confidence = 0.7
|
||||
chatCand.Reasoning = "通用非业务问句,归入闲聊对话"
|
||||
}
|
||||
candidates = append(candidates, chatCand)
|
||||
|
||||
@@ -1407,6 +1411,46 @@ func isGreetingOrHelp(input string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsSmallTalkOrGeneralQuestion 判定是否为通用非业务问句(闲聊/常识提问)。
|
||||
// 用于抬高 CASUAL_CHAT 置信度,并在 L2 模糊分支将问句改道到闲聊处理器,
|
||||
// 避免「今天天气怎么样」等落入阶段推销模板。
|
||||
func IsSmallTalkOrGeneralQuestion(input string) bool {
|
||||
trimmed := strings.TrimSpace(input)
|
||||
if trimmed == "" || utf8.RuneCountInString(trimmed) < 3 {
|
||||
return false
|
||||
}
|
||||
if hasStrongBusinessLexicon(trimmed) {
|
||||
return false
|
||||
}
|
||||
questionMarkers := []string{
|
||||
"怎么样", "好吗", "什么是", "为什么", "怎么", "哪些", "多少",
|
||||
"吗", "呢", "?", "?",
|
||||
}
|
||||
for _, m := range questionMarkers {
|
||||
if strings.Contains(trimmed, m) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// hasStrongBusinessLexicon 粗粒度业务词闸门:含这些词时不当作纯闲聊问句提升置信度,
|
||||
// 留给业务匹配器或模糊参谋分支处理(业务匹配器置信度通常 ≥0.86,本闸门主要服务 L2 改道)。
|
||||
func hasStrongBusinessLexicon(input string) bool {
|
||||
markers := []string{
|
||||
"项目", "阶段", "推进", "法规", "注册", "文档", "审核", "批准", "驳回",
|
||||
"报价", "需求", "型检", "成熟度", "工作流", "交付", "立项", "送审", "认领",
|
||||
"方案成型", "V0.1", "V0.5", "V1.0", "v0.1", "v0.5", "v1.0",
|
||||
"进度", "风险", "预算", "目标市场", "受控",
|
||||
}
|
||||
for _, m := range markers {
|
||||
if strings.Contains(input, m) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func matchImpactIntent(input string) *CandidateIntent {
|
||||
propose := strings.Contains(input, "修订提案") || strings.Contains(input, "生成修订") || strings.Contains(input, "提议修改文档")
|
||||
analyze := strings.Contains(input, "分析影响") || strings.Contains(input, "影响分析") || strings.Contains(input, "波及哪些") ||
|
||||
|
||||
@@ -794,3 +794,84 @@ func TestIntentRecognition_ImpactPhrases(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntentRecognition_GeneralQuestionRoutesToCasualNotAmbiguous(t *testing.T) {
|
||||
rec := intent.NewRecognizer()
|
||||
ctx := &intent.ContextSnapshot{
|
||||
SnapshotID: "ctx_smalltalk",
|
||||
CurrentStage: string(maturity.StageV01_Triage),
|
||||
}
|
||||
|
||||
for _, input := range []string{"今天天气怎么样", "明天天气好吗,适合出门吗"} {
|
||||
res, err := rec.Recognize(context.Background(), input, ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("recognize %q failed: %v", input, err)
|
||||
}
|
||||
if res.TopIntent.Intent != intent.IntentCasualChat {
|
||||
t.Errorf("%q: expected CASUAL_CHAT, got %s", input, res.TopIntent.Intent)
|
||||
}
|
||||
if res.TopIntent.Confidence < 0.6 {
|
||||
t.Errorf("%q: expected confidence >= 0.6, got %v", input, res.TopIntent.Confidence)
|
||||
}
|
||||
if res.Branch == intent.BranchAmbiguousFallback {
|
||||
t.Errorf("%q: must not use AMBIGUOUS_FALLBACK, got %s", input, res.Branch)
|
||||
}
|
||||
if res.Branch != intent.BranchNewIntentPreempt {
|
||||
t.Errorf("%q: expected NEW_INTENT_PREEMPT, got %s", input, res.Branch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntentRecognition_BusinessQueryStillProgressNotSmalltalk(t *testing.T) {
|
||||
rec := intent.NewRecognizer()
|
||||
ctx := &intent.ContextSnapshot{
|
||||
SnapshotID: "ctx_biz_q",
|
||||
CurrentStage: string(maturity.StageV01_Triage),
|
||||
}
|
||||
res, err := rec.Recognize(context.Background(), "分析一下测试方案中,有哪些比较难的项目,以及需要特别注意的地方", ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("recognize failed: %v", err)
|
||||
}
|
||||
if res.TopIntent.Intent != intent.IntentQueryProgress {
|
||||
t.Errorf("expected QUERY_PROGRESS, got %s", res.TopIntent.Intent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntentRecognition_ShortFragmentsStayAmbiguous(t *testing.T) {
|
||||
rec := intent.NewRecognizer()
|
||||
ctx := &intent.ContextSnapshot{
|
||||
SnapshotID: "ctx_frag",
|
||||
CurrentStage: string(maturity.StageV01_Triage),
|
||||
}
|
||||
for _, input := range []string{"嗯", "哦"} {
|
||||
res, err := rec.Recognize(context.Background(), input, ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("recognize %q failed: %v", input, err)
|
||||
}
|
||||
if res.Branch != intent.BranchAmbiguousFallback {
|
||||
t.Errorf("%q: expected AMBIGUOUS_FALLBACK, got %s (intent=%s conf=%.2f)",
|
||||
input, res.Branch, res.TopIntent.Intent, res.TopIntent.Confidence)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntentRecognition_ConsultativeStillAmbiguous(t *testing.T) {
|
||||
rec := intent.NewRecognizer()
|
||||
ctx := &intent.ContextSnapshot{
|
||||
SnapshotID: "ctx_consult",
|
||||
CurrentStage: string(maturity.StageV01_Triage),
|
||||
}
|
||||
res, err := rec.Recognize(context.Background(), "这个方案你觉得呢", ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("recognize failed: %v", err)
|
||||
}
|
||||
if res.Branch != intent.BranchAmbiguousFallback {
|
||||
t.Errorf("expected AMBIGUOUS_FALLBACK for consultative, got %s", res.Branch)
|
||||
}
|
||||
if !intent.IsSmallTalkOrGeneralQuestion("今天天气怎么样") {
|
||||
t.Error("expected weather question to be smalltalk")
|
||||
}
|
||||
if intent.IsSmallTalkOrGeneralQuestion("嗯") {
|
||||
t.Error("short fragment must not count as general question")
|
||||
}
|
||||
}
|
||||
|
||||
+26
@@ -144,6 +144,32 @@
|
||||
"expect_phase1": {
|
||||
"must_not_branch": "EXPECTATION_HIT"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "hn-weather-smalltalk",
|
||||
"input": "今天天气怎么样",
|
||||
"snapshot": {
|
||||
"current_stage": "V0.1_triage"
|
||||
},
|
||||
"expect_phase1": {
|
||||
"legacy_intent": "CASUAL_CHAT",
|
||||
"branch": "NEW_INTENT_PREEMPT",
|
||||
"must_not_branch": "AMBIGUOUS_FALLBACK",
|
||||
"dialogue_act": "ask"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "hn-weather-outing-smalltalk",
|
||||
"input": "明天天气好吗,适合出门吗",
|
||||
"snapshot": {
|
||||
"current_stage": "V0.1_triage"
|
||||
},
|
||||
"expect_phase1": {
|
||||
"legacy_intent": "CASUAL_CHAT",
|
||||
"branch": "NEW_INTENT_PREEMPT",
|
||||
"must_not_branch": "AMBIGUOUS_FALLBACK",
|
||||
"dialogue_act": "ask"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user