package intent import ( "fmt" "regexp" "strings" "time" ) // matchExpectationHit 检验是否命中挂起预期 (分支 1) func matchExpectationHit(input string, pending *PendingExpectation) *CandidateIntent { // 0. 检验超时过期 if !pending.ExpiresAt.IsZero() && time.Now().After(pending.ExpiresAt) { return nil } // 1. 否定/暂缓意图 declineKeywords := []string{"先别", "别推", "算了", "先不", "等等", "暂时不", "先别推", "不要推进", "不要", "不行", "算了吧", "先放一放", "先等等", "取消", "放弃"} if pending.DeclineWord != "" { declineKeywords = append(declineKeywords, pending.DeclineWord) } for _, dk := range declineKeywords { if input == dk || strings.HasPrefix(input, dk) { return &CandidateIntent{ Intent: IntentDeclineExpectation, Confidence: 0.95, Branch: BranchExpectationHit, IsDecline: true, FilledSlots: make(map[string]string), Reasoning: fmt.Sprintf("用户明确表示暂缓或取消当前挂起建议 ('%s')", input), } } } // 2. 选择序号检验 (如 "1", "2", "选1", "选2", "第1个", "第一个", "选项1") var matchedOpt *ExpectationOption optIndex := matchOptionIndex(input, pending.Options) if optIndex >= 0 && optIndex < len(pending.Options) { matchedOpt = &pending.Options[optIndex] } // 肯定前缀后含独立新请求谓词时,不得吞掉新意图(如「好,请先查法规」) if matchedOpt == nil && hasIndependentNewPredicate(input) { return nil } // 3. 显式确认短语(不受 AllowAffirmative 限制) explicitConfirm := isExplicitConfirmPhrase(input) // 4. 肯定词检验 (如 "好", "行", "可以", "成", "没问题", "同意", "好的", "就按这个", "按你说的办", "确认") affirmativePrefixes := []string{"好", "好的", "行", "行啊", "成", "可以", "没问题", "同意", "就按这个", "按这个", "就按这个办", "听你的", "ok", "yes", "就这么定", "确认", "确定", "批准"} if pending.AffirmativeWord != "" { affirmativePrefixes = append(affirmativePrefixes, pending.AffirmativeWord) } isAffirmative := explicitConfirm for _, aff := range affirmativePrefixes { if input == aff || strings.HasPrefix(input, aff+",") || strings.HasPrefix(input, aff+",") || strings.HasPrefix(input, aff+"但") || strings.HasPrefix(input, aff+" ") || strings.HasPrefix(input, aff+"!") || strings.HasPrefix(input, aff+"!") || (aff == "确认" && (input == "确认" || input == "确认驳回" || input == "确认批准" || input == "确认执行")) || (pending.AffirmativeWord != "" && strings.HasPrefix(input, pending.AffirmativeWord)) { isAffirmative = true break } } // 裸肯定词必须 AllowAffirmative=true(显式确认短语除外) if isAffirmative && !explicitConfirm && !pending.AllowAffirmative { bareAffirmative := false for _, aff := range affirmativePrefixes { if input == aff { bareAffirmative = true break } } if bareAffirmative { isAffirmative = false } } if matchedOpt == nil && isAffirmative { if pending.DefaultOption != nil { matchedOpt = pending.DefaultOption } else if len(pending.Options) > 0 { matchedOpt = &pending.Options[0] } else if pending.ExpectedIntent != "" { matchedOpt = &ExpectationOption{ OptionID: pending.TargetAction, Title: pending.Prompt, Intent: pending.ExpectedIntent, Slots: pending.Payload, } } } if matchedOpt == nil { return nil } // 4. 提取增量事实补丁 (Patch Merge) // 例如: "好,但目标市场改成欧洲CE", "选1,先把预算定为15万" patchSlots := extractFactSlots(input) filledSlots := make(map[string]string) for k, v := range matchedOpt.Slots { filledSlots[k] = v } for k, v := range patchSlots { filledSlots[k] = v } if pending.TargetAction != "" && filledSlots["target_action"] == "" { filledSlots["target_action"] = pending.TargetAction } confidence := 1.0 reasoning := fmt.Sprintf("用户明确肯定上一轮参谋建议动作 [%s: %s]", matchedOpt.OptionID, matchedOpt.Title) if len(patchSlots) > 0 { confidence = 0.98 reasoning = fmt.Sprintf("用户肯定上一轮建议动作 [%s: %s],并附带增量事实补丁: %v", matchedOpt.OptionID, matchedOpt.Title, patchSlots) } else if optIndex >= 0 { confidence = 0.98 reasoning = fmt.Sprintf("用户按序号选择了备选动作 [%s: %s]", matchedOpt.OptionID, matchedOpt.Title) } return &CandidateIntent{ Intent: matchedOpt.Intent, Confidence: confidence, Branch: BranchExpectationHit, FilledSlots: filledSlots, PatchSlots: patchSlots, MatchedOption: matchedOpt, Reasoning: reasoning, } } // matchOptionIndex 匹配用户输入的选项编号 func matchOptionIndex(input string, options []ExpectationOption) int { input = strings.TrimSpace(input) if len(options) == 0 { return -1 } patterns := []struct { regex string idx int }{ {"^(1|选1|选第1个|第一个|第1个|选项1|选项一)$", 0}, {"^(2|选2|选第2个|第二个|第2个|选项2|选项二)$", 1}, {"^(3|选3|选第3个|第三个|第3个|选项3|选项三)$", 2}, } for _, p := range patterns { if matched, _ := regexp.MatchString("(?i)"+p.regex, input); matched { return p.idx } } for idx, opt := range options { if strings.EqualFold(opt.OptionID, input) { return idx } } return -1 } // IsConsultativeOrHesitant 报告输入是否在征询意见或表达犹豫。 // 只有这类输入才保留阶段对照话术;资料名、名词短语不得走该话术。 func IsConsultativeOrHesitant(input string) bool { return isConsultativeOrHesitant(input) } // isConsultativeOrHesitant 检验是否属于犹豫或征询参谋意见 (分支 3) func isConsultativeOrHesitant(input string) bool { patterns := []string{ "你觉得呢", "你怎么看", "有什么建议", "怎么选", "拿不定主意", "先放放.*你觉得", "这个先放放", "先放一放.*你觉得", "哪个好", "给点意见", } for _, p := range patterns { if matched, _ := regexp.MatchString("(?i)"+p, input); matched { return true } } return false } // buildAmbiguousConsultResult 构建分支 3 (模糊/分忧引导) 输出 func buildAmbiguousConsultResult(input, snapID string, pending *PendingExpectation) *IntentRecognitionResult { clarifyQ := "我为您准备了当前阶段最稳妥的推进选项。请问需要我为您详细分析技术可行性,还是先为您做方案摸底?" if pending != nil && len(pending.Options) > 0 { var optDesc strings.Builder optDesc.WriteString("针对目前的决策节点,我为您梳理了以下选项供您参考:\n") for _, o := range pending.Options { optDesc.WriteString(fmt.Sprintf("• [%s] %s: %s\n", o.OptionID, o.Title, o.Description)) } clarifyQ = optDesc.String() } top := &CandidateIntent{ Intent: IntentCasualChat, Confidence: 0.65, Branch: BranchAmbiguousFallback, FilledSlots: make(map[string]string), Reasoning: "用户表达犹豫或主动征询参谋意见,触发参谋主动分忧与引导", ClarificationQuestion: clarifyQ, } result := &IntentRecognitionResult{ RawInput: input, Branch: BranchAmbiguousFallback, TopIntent: top, Candidates: []CandidateIntent{*top}, ContextSnapshotID: snapID, RequiresClarifying: true, } finalizeRecognitionResult(result) return result }