Files
Agent-System/internal/intent/recognizer.go
T

281 lines
8.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package intent
import (
"context"
"fmt"
"regexp"
"strings"
"chat-agent-system/internal/bizdev"
)
// Recognizer 意图识别器接口
type Recognizer interface {
Recognize(ctx context.Context, input string, userCtx *ContextSnapshot) (*IntentRecognitionResult, error)
}
// RuleBasedRecognizer 规则与上下文约束结合的轻量高效意图识别器
type RuleBasedRecognizer struct{}
func NewRecognizer() Recognizer {
return &RuleBasedRecognizer{}
}
func (r *RuleBasedRecognizer) Recognize(ctx context.Context, input string, userCtx *ContextSnapshot) (*IntentRecognitionResult, error) {
trimmed := strings.TrimSpace(input)
if trimmed == "" {
return &IntentRecognitionResult{
RawInput: input,
TopIntent: &CandidateIntent{
Intent: IntentCasualChat,
Confidence: 1.0,
Reasoning: "输入为空",
},
Candidates: []CandidateIntent{
{Intent: IntentCasualChat, Confidence: 1.0, Reasoning: "空输入"},
},
}, nil
}
snapID := ""
currentStage := bizdev.StageV01_Triage
if userCtx != nil {
snapID = userCtx.SnapshotID
if userCtx.CurrentStage != "" {
currentStage = bizdev.MaturityStage(userCtx.CurrentStage)
}
}
var candidates []CandidateIntent
// 1. 检验 ADVANCE_STAGE (推进阶段)
if cand := matchAdvanceStage(trimmed, currentStage); cand != nil {
candidates = append(candidates, *cand)
}
// 2. 检验 SUPPLEMENT_INFO (补充信息/参数变更)
if cand := matchSupplementInfo(trimmed); cand != nil {
candidates = append(candidates, *cand)
}
// 3. 检验 QUERY_PROGRESS (查询进度/参谋诊断/话术)
if cand := matchQueryProgress(trimmed); cand != nil {
candidates = append(candidates, *cand)
}
// 4. 检验 TRIGGER_TASK (特定任务触发)
if cand := matchTriggerTask(trimmed); cand != nil {
candidates = append(candidates, *cand)
}
// 5. 检验 CASUAL_CHAT (寒暄/通用咨询)
chatCand := CandidateIntent{
Intent: IntentCasualChat,
Confidence: 0.2,
FilledSlots: make(map[string]string),
Reasoning: "常规日常问候或未匹配特定业务指令",
}
if isGreetingOrHelp(trimmed) {
chatCand.Confidence = 0.85
chatCand.Reasoning = "匹配到问候语或通用帮助请求"
}
candidates = append(candidates, chatCand)
// 对 candidates 按置信度排序
sortCandidates(candidates)
top := &candidates[0]
requiresClarifying := false
if top.Confidence < 0.6 || len(top.MissingSlots) > 0 {
requiresClarifying = true
}
return &IntentRecognitionResult{
RawInput: trimmed,
TopIntent: top,
Candidates: candidates,
ContextSnapshotID: snapID,
RequiresClarifying: requiresClarifying,
}, nil
}
func matchAdvanceStage(input string, currentStage bizdev.MaturityStage) *CandidateIntent {
patterns := []string{
"进入下一阶段", "推进到下一阶段", "推进阶段", "进入方案", "进入论证",
"方案论证", "可行性论证", "开始方案", "进入报价", "开始报价", "商业锁定",
"进入v0.1", "推进到v0.1", "进入v0.5", "推进到v0.5", "进入v1.0", "推进到v1.0", "可以进入",
"没有问题.*进入", "没问题.*推进", "通过.*下一阶段", "开始推进",
"做.*摸底", "开始.*摸底", "先做.*摸底", "进行.*摸底", "做v0.1", "先做v0.1",
"做.*论证", "做.*报价", "开始论证",
}
for _, p := range patterns {
if matched, _ := regexp.MatchString("(?i)"+p, input); matched {
var target bizdev.MaturityStage
if strings.Contains(input, "报价") || strings.Contains(input, "v1.0") || strings.Contains(input, "V1.0") || strings.Contains(input, "商业锁定") {
target = bizdev.StageV10_Quoting
} else if strings.Contains(input, "方案") || strings.Contains(input, "论证") || strings.Contains(input, "可行性") || strings.Contains(input, "v0.5") || strings.Contains(input, "V0.5") {
target = bizdev.StageV05_Feasibility
} else if strings.Contains(input, "摸底") || strings.Contains(input, "初筛") || strings.Contains(input, "v0.1") || strings.Contains(input, "V0.1") {
target = bizdev.StageV01_Triage
} else {
// 默认根据当前阶段自然顺推
if currentStage == bizdev.StageV01_Triage {
target = bizdev.StageV05_Feasibility
} else if currentStage == bizdev.StageV05_Feasibility {
target = bizdev.StageV10_Quoting
} else {
target = bizdev.StageV10_Quoting
}
}
reason := fmt.Sprintf("匹配推进指令 '%s',目标成熟度阶段为 %s", p, target)
return &CandidateIntent{
Intent: IntentAdvanceStage,
Confidence: 0.92,
FilledSlots: map[string]string{
SlotTargetStage: string(target),
},
Reasoning: reason,
}
}
}
return nil
}
func matchSupplementInfo(input string) *CandidateIntent {
patterns := []string{
"补充", "更新", "客户说", "客户反馈", "修改为", "改为", "增加",
"市场是", "参数是", "用途是", "设备是", "预算是", "时间是", "资料", "附件",
}
matchedAny := false
for _, p := range patterns {
if strings.Contains(input, p) {
matchedAny = true
break
}
}
if !matchedAny {
return nil
}
slots := make(map[string]string)
slots[SlotSupplementDoc] = input
// 细粒度槽位提取
if strings.Contains(input, "市场") {
slots[SlotFactKey] = "target_market"
slots[SlotFactValue] = extractValueAfter(input, []string{"市场是", "市场改为", "市场为", "目标市场是", "目标市场为", "市场"})
} else if strings.Contains(input, "预算") {
slots[SlotFactKey] = "budget"
slots[SlotFactValue] = extractValueAfter(input, []string{"预算是", "预算为", "预算"})
} else if strings.Contains(input, "时间") || strings.Contains(input, "周期") {
slots[SlotFactKey] = "timeline"
slots[SlotFactValue] = extractValueAfter(input, []string{"时间是", "时间为", "周期是", "周期为"})
} else if strings.Contains(input, "用途") {
slots[SlotFactKey] = "intended_use"
slots[SlotFactValue] = extractValueAfter(input, []string{"用途是", "用途为", "预期用途"})
}
return &CandidateIntent{
Intent: IntentSupplementInfo,
Confidence: 0.88,
FilledSlots: slots,
Reasoning: "用户输入中包含参数变更或新材料事实补充",
}
}
func matchQueryProgress(input string) *CandidateIntent {
patterns := []string{
"进展", "进度", "走到哪", "到哪一步", "状态", "情况", "评估到哪",
"风险", "坑", "注意什么", "话术", "怎么说", "怎么跟客户说", "微信", "草稿", "参谋",
}
for _, p := range patterns {
if strings.Contains(input, p) {
topic := "all"
if strings.Contains(input, "风险") || strings.Contains(input, "坑") {
topic = "risk"
} else if strings.Contains(input, "话术") || strings.Contains(input, "微信") || strings.Contains(input, "怎么说") {
topic = "talk_script"
} else if strings.Contains(input, "进展") || strings.Contains(input, "进度") || strings.Contains(input, "步") {
topic = "progress"
}
return &CandidateIntent{
Intent: IntentQueryProgress,
Confidence: 0.90,
FilledSlots: map[string]string{
SlotQueryTopic: topic,
},
Reasoning: fmt.Sprintf("用户在询问项目状态、风险或对外沟通话术 (主题: %s)", topic),
}
}
}
return nil
}
func matchTriggerTask(input string) *CandidateIntent {
taskMap := map[string]string{
"需求收集表": "intake",
"需求表": "intake",
"调研报告": "research",
"调研": "research",
"法规评估": "feasibility_reg",
"注册评估": "feasibility_reg",
"初步设计": "prelim_design",
"设计方案": "prelim_design",
"报价单": "cdo_quote",
"报价": "cdo_quote",
}
for kw, tKey := range taskMap {
if strings.Contains(input, kw) && (strings.Contains(input, "生成") || strings.Contains(input, "做") || strings.Contains(input, "跑") || strings.Contains(input, "出")) {
return &CandidateIntent{
Intent: IntentTriggerTask,
Confidence: 0.86,
FilledSlots: map[string]string{
SlotTargetTask: tKey,
},
Reasoning: fmt.Sprintf("用户要求显式生成交付任务: %s (%s)", kw, tKey),
}
}
}
return nil
}
func isGreetingOrHelp(input string) bool {
greetings := []string{"你好", "您好", "在吗", "hi", "hello", "你是谁", "帮助", "能做什么", "怎么用"}
for _, g := range greetings {
if strings.Contains(strings.ToLower(input), g) {
return true
}
}
return false
}
func extractValueAfter(input string, prefixes []string) string {
for _, p := range prefixes {
if idx := strings.Index(input, p); idx != -1 {
val := strings.TrimSpace(input[idx+len(p):])
val = strings.Trim(val, "::,,。 ")
if val != "" {
return val
}
}
}
return input
}
func sortCandidates(c []CandidateIntent) {
for i := 0; i < len(c)-1; i++ {
for j := i + 1; j < len(c); j++ {
if c[j].Confidence > c[i].Confidence {
c[i], c[j] = c[j], c[i]
}
}
}
}