feat(intent): implement three-branch flexible intent resolution and expectation contracts
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+270
-38
@@ -14,7 +14,7 @@ type Recognizer interface {
|
||||
Recognize(ctx context.Context, input string, userCtx *ContextSnapshot) (*IntentRecognitionResult, error)
|
||||
}
|
||||
|
||||
// RuleBasedRecognizer 规则与上下文约束结合的轻量高效意图识别器
|
||||
// RuleBasedRecognizer 规则与上下文约束结合的三分支柔性意图消解器
|
||||
type RuleBasedRecognizer struct{}
|
||||
|
||||
func NewRecognizer() Recognizer {
|
||||
@@ -26,54 +26,90 @@ func (r *RuleBasedRecognizer) Recognize(ctx context.Context, input string, userC
|
||||
if trimmed == "" {
|
||||
return &IntentRecognitionResult{
|
||||
RawInput: input,
|
||||
Branch: BranchAmbiguousFallback,
|
||||
TopIntent: &CandidateIntent{
|
||||
Intent: IntentCasualChat,
|
||||
Branch: BranchAmbiguousFallback,
|
||||
Confidence: 1.0,
|
||||
Reasoning: "输入为空",
|
||||
},
|
||||
Candidates: []CandidateIntent{
|
||||
{Intent: IntentCasualChat, Confidence: 1.0, Reasoning: "空输入"},
|
||||
{Intent: IntentCasualChat, Branch: BranchAmbiguousFallback, Confidence: 1.0, Reasoning: "空输入"},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
snapID := ""
|
||||
currentStage := bizdev.StageV01_Triage
|
||||
var pending *PendingExpectation
|
||||
|
||||
if userCtx != nil {
|
||||
snapID = userCtx.SnapshotID
|
||||
if userCtx.CurrentStage != "" {
|
||||
currentStage = bizdev.MaturityStage(userCtx.CurrentStage)
|
||||
}
|
||||
pending = userCtx.PendingExpectation
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 阶段一:若输入明确是咨询参谋/犹豫(如“这个先放放,你觉得呢”),优先归入分支 3 (参谋主动分忧)
|
||||
// -------------------------------------------------------------
|
||||
if isConsultativeOrHesitant(trimmed) {
|
||||
res := buildAmbiguousConsultResult(trimmed, snapID, pending)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 阶段二:分支 1 判定——命中历史轮次挂起预期 (肯定 / 选项序号 / 复合带补丁 / 明确撤销)
|
||||
// -------------------------------------------------------------
|
||||
if pending != nil {
|
||||
if hitCand := matchExpectationHit(trimmed, pending); hitCand != nil {
|
||||
return &IntentRecognitionResult{
|
||||
RawInput: trimmed,
|
||||
Branch: BranchExpectationHit,
|
||||
TopIntent: hitCand,
|
||||
Candidates: []CandidateIntent{*hitCand},
|
||||
ContextSnapshotID: snapID,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 阶段三:分支 2 判定——独立新意图抢占 (用户新需求最高优先级)
|
||||
// -------------------------------------------------------------
|
||||
var candidates []CandidateIntent
|
||||
|
||||
// 1. 检验 ADVANCE_STAGE (推进阶段)
|
||||
if cand := matchAdvanceStage(trimmed, currentStage); cand != nil {
|
||||
cand.Branch = BranchNewIntentPreempt
|
||||
candidates = append(candidates, *cand)
|
||||
}
|
||||
|
||||
// 2. 检验 SUPPLEMENT_INFO (补充信息/参数变更)
|
||||
if cand := matchSupplementInfo(trimmed); cand != nil {
|
||||
cand.Branch = BranchNewIntentPreempt
|
||||
candidates = append(candidates, *cand)
|
||||
}
|
||||
|
||||
// 3. 检验 QUERY_PROGRESS (查询进度/参谋诊断/话术)
|
||||
// 3. 检验 QUERY_PROGRESS (查询进度/技术咨询/参谋诊断/话术)
|
||||
if cand := matchQueryProgress(trimmed); cand != nil {
|
||||
cand.Branch = BranchNewIntentPreempt
|
||||
candidates = append(candidates, *cand)
|
||||
}
|
||||
|
||||
// 4. 检验 TRIGGER_TASK (特定任务触发)
|
||||
if cand := matchTriggerTask(trimmed); cand != nil {
|
||||
cand.Branch = BranchNewIntentPreempt
|
||||
candidates = append(candidates, *cand)
|
||||
}
|
||||
|
||||
// 5. 检验 CASUAL_CHAT (寒暄/通用咨询)
|
||||
// 5. 检验 CASUAL_CHAT (寒暄/通用求助)
|
||||
chatCand := CandidateIntent{
|
||||
Intent: IntentCasualChat,
|
||||
Branch: BranchNewIntentPreempt,
|
||||
Confidence: 0.2,
|
||||
FilledSlots: make(map[string]string),
|
||||
Reasoning: "常规日常问候或未匹配特定业务指令",
|
||||
Reasoning: "未匹配到明确业务指令,归入通用对话",
|
||||
}
|
||||
if isGreetingOrHelp(trimmed) {
|
||||
chatCand.Confidence = 0.85
|
||||
@@ -81,17 +117,23 @@ func (r *RuleBasedRecognizer) Recognize(ctx context.Context, input string, userC
|
||||
}
|
||||
candidates = append(candidates, chatCand)
|
||||
|
||||
// 对 candidates 按置信度排序
|
||||
sortCandidates(candidates)
|
||||
|
||||
top := &candidates[0]
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 阶段四:分支 3 判定——模糊/无实际意义兜底 (置信度不足或需澄清)
|
||||
// -------------------------------------------------------------
|
||||
branch := BranchNewIntentPreempt
|
||||
requiresClarifying := false
|
||||
if top.Confidence < 0.6 || len(top.MissingSlots) > 0 {
|
||||
branch = BranchAmbiguousFallback
|
||||
top.Branch = BranchAmbiguousFallback
|
||||
requiresClarifying = true
|
||||
}
|
||||
|
||||
return &IntentRecognitionResult{
|
||||
RawInput: trimmed,
|
||||
Branch: branch,
|
||||
TopIntent: top,
|
||||
Candidates: candidates,
|
||||
ContextSnapshotID: snapID,
|
||||
@@ -99,6 +141,219 @@ func (r *RuleBasedRecognizer) Recognize(ctx context.Context, input string, userC
|
||||
}, nil
|
||||
}
|
||||
|
||||
// matchExpectationHit 检验是否命中挂起预期 (分支 1)
|
||||
func matchExpectationHit(input string, pending *PendingExpectation) *CandidateIntent {
|
||||
// 1. 否定/暂缓意图
|
||||
declineKeywords := []string{"先别", "别推", "算了", "先不", "等等", "暂时不", "先别推", "不要推进", "不要", "不行", "算了吧", "先放一放", "先等等"}
|
||||
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]
|
||||
}
|
||||
|
||||
// 3. 肯定词检验 (如 "好", "行", "可以", "成", "没问题", "同意", "好的", "就按这个", "按你说的办")
|
||||
affirmativePrefixes := []string{"好", "好的", "行", "行啊", "成", "可以", "没问题", "同意", "就按这个", "按这个", "就按这个办", "听你的", "ok", "yes", "就这么定"}
|
||||
isAffirmative := false
|
||||
|
||||
for _, aff := range affirmativePrefixes {
|
||||
if input == aff || strings.HasPrefix(input, aff+",") || strings.HasPrefix(input, aff+",") || strings.HasPrefix(input, aff+"但") || strings.HasPrefix(input, aff+" ") {
|
||||
isAffirmative = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if matchedOpt == nil && isAffirmative {
|
||||
if pending.DefaultOption != nil {
|
||||
matchedOpt = pending.DefaultOption
|
||||
} else if len(pending.Options) > 0 {
|
||||
matchedOpt = &pending.Options[0]
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否匹配 OptionID
|
||||
for idx, opt := range options {
|
||||
if strings.EqualFold(opt.OptionID, input) {
|
||||
return idx
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
// 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,
|
||||
}
|
||||
|
||||
return &IntentRecognitionResult{
|
||||
RawInput: input,
|
||||
Branch: BranchAmbiguousFallback,
|
||||
TopIntent: top,
|
||||
Candidates: []CandidateIntent{*top},
|
||||
ContextSnapshotID: snapID,
|
||||
RequiresClarifying: true,
|
||||
}
|
||||
}
|
||||
|
||||
// extractFactSlots 从文本中提取结构化事实槽位 (支持补丁合并与补充信息)
|
||||
func extractFactSlots(input string) map[string]string {
|
||||
slots := make(map[string]string)
|
||||
|
||||
// 目标市场
|
||||
marketPrefixes := []string{
|
||||
"目标市场改成", "目标市场改为", "目标市场是", "目标市场为",
|
||||
"市场改成", "市场改为", "市场是", "市场为", "目标市场除了国内还想考虑",
|
||||
}
|
||||
for _, p := range marketPrefixes {
|
||||
if idx := strings.Index(input, p); idx != -1 {
|
||||
val := strings.TrimSpace(input[idx+len(p):])
|
||||
val = strings.Trim(val, "::,,。 ")
|
||||
if val != "" {
|
||||
slots["target_market"] = val
|
||||
slots[SlotFactKey] = "target_market"
|
||||
slots[SlotFactValue] = val
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 预算
|
||||
budgetPrefixes := []string{"预算改成", "预算改为", "预算是", "预算为", "预算只有", "预算大概", "预算"}
|
||||
for _, p := range budgetPrefixes {
|
||||
if idx := strings.Index(input, p); idx != -1 && slots["budget"] == "" {
|
||||
val := strings.TrimSpace(input[idx+len(p):])
|
||||
val = strings.Trim(val, "::,,。 ")
|
||||
if val != "" {
|
||||
slots["budget"] = val
|
||||
slots[SlotFactKey] = "budget"
|
||||
slots[SlotFactValue] = val
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 预期用途 / 使用场景
|
||||
usePrefixes := []string{"用途是", "用途为", "预期用途是", "场景是", "场景为"}
|
||||
for _, p := range usePrefixes {
|
||||
if idx := strings.Index(input, p); idx != -1 && slots["intended_use"] == "" {
|
||||
val := strings.TrimSpace(input[idx+len(p):])
|
||||
val = strings.Trim(val, "::,,。 ")
|
||||
if val != "" {
|
||||
slots["intended_use"] = val
|
||||
slots[SlotFactKey] = "intended_use"
|
||||
slots[SlotFactValue] = val
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return slots
|
||||
}
|
||||
|
||||
func matchAdvanceStage(input string, currentStage bizdev.MaturityStage) *CandidateIntent {
|
||||
patterns := []string{
|
||||
"进入下一阶段", "推进到下一阶段", "推进阶段", "进入方案", "进入论证",
|
||||
@@ -119,7 +374,6 @@ func matchAdvanceStage(input string, currentStage bizdev.MaturityStage) *Candida
|
||||
} 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 {
|
||||
@@ -161,23 +415,11 @@ func matchSupplementInfo(input string) *CandidateIntent {
|
||||
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{"用途是", "用途为", "预期用途"})
|
||||
slots := extractFactSlots(input)
|
||||
if len(slots) == 0 {
|
||||
slots = make(map[string]string)
|
||||
}
|
||||
slots[SlotSupplementDoc] = input
|
||||
|
||||
return &CandidateIntent{
|
||||
Intent: IntentSupplementInfo,
|
||||
@@ -191,6 +433,7 @@ func matchQueryProgress(input string) *CandidateIntent {
|
||||
patterns := []string{
|
||||
"进展", "进度", "走到哪", "到哪一步", "状态", "情况", "评估到哪",
|
||||
"风险", "坑", "注意什么", "话术", "怎么说", "怎么跟客户说", "微信", "草稿", "参谋",
|
||||
"探头功耗", "功耗", "供电", "怎么设计", "设计方案", "临床要不要", "怎么做", "查一下", "看下",
|
||||
}
|
||||
|
||||
for _, p := range patterns {
|
||||
@@ -202,6 +445,8 @@ func matchQueryProgress(input string) *CandidateIntent {
|
||||
topic = "talk_script"
|
||||
} else if strings.Contains(input, "进展") || strings.Contains(input, "进度") || strings.Contains(input, "步") {
|
||||
topic = "progress"
|
||||
} else if strings.Contains(input, "功耗") || strings.Contains(input, "供电") || strings.Contains(input, "设计") {
|
||||
topic = "technical_design"
|
||||
}
|
||||
|
||||
return &CandidateIntent{
|
||||
@@ -210,7 +455,7 @@ func matchQueryProgress(input string) *CandidateIntent {
|
||||
FilledSlots: map[string]string{
|
||||
SlotQueryTopic: topic,
|
||||
},
|
||||
Reasoning: fmt.Sprintf("用户在询问项目状态、风险或对外沟通话术 (主题: %s)", topic),
|
||||
Reasoning: fmt.Sprintf("用户在询问项目状态、技术方案或对外沟通话术 (主题: %s)", topic),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -256,19 +501,6 @@ func isGreetingOrHelp(input string) bool {
|
||||
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++ {
|
||||
|
||||
@@ -3,6 +3,7 @@ package intent_test
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"chat-agent-system/internal/bizdev"
|
||||
"chat-agent-system/internal/intent"
|
||||
@@ -100,3 +101,186 @@ func TestIntentRecognition_CasualChat(t *testing.T) {
|
||||
t.Errorf("expected intent CASUAL_CHAT, got %s", res.TopIntent.Intent)
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------
|
||||
// 三分支柔性消解核心测试集
|
||||
// ----------------------------------------------------------------------------------
|
||||
|
||||
func samplePendingExpectation() *intent.PendingExpectation {
|
||||
optAdvance := intent.ExpectationOption{
|
||||
OptionID: "1",
|
||||
Title: "推进到 V0.5 方案成型与法规对齐",
|
||||
Description: "小队将自动开展技术路线比选与分类判定",
|
||||
Intent: intent.IntentAdvanceStage,
|
||||
Slots: map[string]string{
|
||||
intent.SlotTargetStage: string(bizdev.StageV05_Feasibility),
|
||||
},
|
||||
}
|
||||
optRefresh := intent.ExpectationOption{
|
||||
OptionID: "2",
|
||||
Title: "局部刷新 V0.1 用户需求表",
|
||||
Description: "根据新补充材料重新生成初筛画像",
|
||||
Intent: intent.IntentTriggerTask,
|
||||
Slots: map[string]string{
|
||||
intent.SlotTargetTask: "intake",
|
||||
},
|
||||
}
|
||||
|
||||
return &intent.PendingExpectation{
|
||||
ContextSnippet: "当前 V0.1 闪电摸底已就绪,建议推进到 V0.5",
|
||||
DefaultOption: &optAdvance,
|
||||
Options: []intent.ExpectationOption{optAdvance, optRefresh},
|
||||
AllowAffirmative: true,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// 用例 1(分支 1 极速兑现):"好", "行", "选1"
|
||||
func TestThreeBranch_Branch1_DirectAffirmative(t *testing.T) {
|
||||
rec := intent.NewRecognizer()
|
||||
ctx := &intent.ContextSnapshot{
|
||||
SnapshotID: "ctx_pending_1",
|
||||
CurrentStage: string(bizdev.StageV01_Triage),
|
||||
PendingExpectation: samplePendingExpectation(),
|
||||
}
|
||||
|
||||
testCases := []string{"好", "行", "可以", "没问题", "同意", "好的", "就按这个办", "选1", "1"}
|
||||
for _, tc := range testCases {
|
||||
res, err := rec.Recognize(context.Background(), tc, ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("recognize '%s' failed: %v", tc, err)
|
||||
}
|
||||
if res.Branch != intent.BranchExpectationHit {
|
||||
t.Errorf("input '%s': expected branch EXPECTATION_HIT, got %s", tc, res.Branch)
|
||||
}
|
||||
if res.TopIntent.Intent != intent.IntentAdvanceStage {
|
||||
t.Errorf("input '%s': expected intent ADVANCE_STAGE, got %s", tc, res.TopIntent.Intent)
|
||||
}
|
||||
if res.TopIntent.FilledSlots[intent.SlotTargetStage] != string(bizdev.StageV05_Feasibility) {
|
||||
t.Errorf("input '%s': expected target stage V0.5, got %s", tc, res.TopIntent.FilledSlots[intent.SlotTargetStage])
|
||||
}
|
||||
if res.TopIntent.Confidence < 0.95 {
|
||||
t.Errorf("input '%s': expected confidence >= 0.95, got %v", tc, res.TopIntent.Confidence)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 用例 2(分支 1 复合打补丁):"好,但目标市场改成欧洲CE"
|
||||
func TestThreeBranch_Branch1_AffirmativeWithPatch(t *testing.T) {
|
||||
rec := intent.NewRecognizer()
|
||||
ctx := &intent.ContextSnapshot{
|
||||
SnapshotID: "ctx_pending_2",
|
||||
CurrentStage: string(bizdev.StageV01_Triage),
|
||||
PendingExpectation: samplePendingExpectation(),
|
||||
}
|
||||
|
||||
res, err := rec.Recognize(context.Background(), "好,但目标市场改成欧洲CE", ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("recognize failed: %v", err)
|
||||
}
|
||||
|
||||
if res.Branch != intent.BranchExpectationHit {
|
||||
t.Fatalf("expected branch EXPECTATION_HIT, got %s", res.Branch)
|
||||
}
|
||||
if res.TopIntent.Intent != intent.IntentAdvanceStage {
|
||||
t.Errorf("expected intent ADVANCE_STAGE, got %s", res.TopIntent.Intent)
|
||||
}
|
||||
if res.TopIntent.FilledSlots[intent.SlotTargetStage] != string(bizdev.StageV05_Feasibility) {
|
||||
t.Errorf("expected target stage V0.5, got %s", res.TopIntent.FilledSlots[intent.SlotTargetStage])
|
||||
}
|
||||
if res.TopIntent.FilledSlots["target_market"] != "欧洲CE" {
|
||||
t.Errorf("expected patched target_market '欧洲CE', got '%s'", res.TopIntent.FilledSlots["target_market"])
|
||||
}
|
||||
if res.TopIntent.PatchSlots["target_market"] != "欧洲CE" {
|
||||
t.Errorf("expected PatchSlots to have target_market '欧洲CE', got '%s'", res.TopIntent.PatchSlots["target_market"])
|
||||
}
|
||||
}
|
||||
|
||||
// 用例 3(分支 1 否定撤销):"先别推,等等"
|
||||
func TestThreeBranch_Branch1_Decline(t *testing.T) {
|
||||
rec := intent.NewRecognizer()
|
||||
ctx := &intent.ContextSnapshot{
|
||||
SnapshotID: "ctx_pending_3",
|
||||
CurrentStage: string(bizdev.StageV01_Triage),
|
||||
PendingExpectation: samplePendingExpectation(),
|
||||
}
|
||||
|
||||
testCases := []string{"先别推,等等", "算了吧", "先别", "先等等"}
|
||||
for _, tc := range testCases {
|
||||
res, err := rec.Recognize(context.Background(), tc, ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("recognize '%s' failed: %v", tc, err)
|
||||
}
|
||||
if res.Branch != intent.BranchExpectationHit {
|
||||
t.Errorf("input '%s': expected branch EXPECTATION_HIT, got %s", tc, res.Branch)
|
||||
}
|
||||
if res.TopIntent.Intent != intent.IntentDeclineExpectation {
|
||||
t.Errorf("input '%s': expected intent DECLINE_EXPECTATION, got %s", tc, res.TopIntent.Intent)
|
||||
}
|
||||
if !res.TopIntent.IsDecline {
|
||||
t.Errorf("input '%s': expected IsDecline to be true", tc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 用例 4(分支 2 话题突发抢占):有挂起预期时,用户突然问新问题或补新事实
|
||||
func TestThreeBranch_Branch2_NewIntentPreempt(t *testing.T) {
|
||||
rec := intent.NewRecognizer()
|
||||
ctx := &intent.ContextSnapshot{
|
||||
SnapshotID: "ctx_pending_4",
|
||||
CurrentStage: string(bizdev.StageV01_Triage),
|
||||
PendingExpectation: samplePendingExpectation(),
|
||||
}
|
||||
|
||||
// 抢占场景 A: 突然提问技术问题
|
||||
res1, err := rec.Recognize(context.Background(), "查一下探头功耗要怎么设计", ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("recognize failed: %v", err)
|
||||
}
|
||||
if res1.Branch != intent.BranchNewIntentPreempt {
|
||||
t.Errorf("expected branch NEW_INTENT_PREEMPT, got %s", res1.Branch)
|
||||
}
|
||||
if res1.TopIntent.Intent != intent.IntentQueryProgress {
|
||||
t.Errorf("expected intent QUERY_PROGRESS, got %s", res1.TopIntent.Intent)
|
||||
}
|
||||
|
||||
// 抢占场景 B: 突然补充事实
|
||||
res2, err := rec.Recognize(context.Background(), "客户补充说预算大概15万", ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("recognize failed: %v", err)
|
||||
}
|
||||
if res2.Branch != intent.BranchNewIntentPreempt {
|
||||
t.Errorf("expected branch NEW_INTENT_PREEMPT, got %s", res2.Branch)
|
||||
}
|
||||
if res2.TopIntent.Intent != intent.IntentSupplementInfo {
|
||||
t.Errorf("expected intent SUPPLEMENT_INFO, got %s", res2.TopIntent.Intent)
|
||||
}
|
||||
if res2.TopIntent.FilledSlots["budget"] != "15万" {
|
||||
t.Errorf("expected budget '15万', got '%s'", res2.TopIntent.FilledSlots["budget"])
|
||||
}
|
||||
}
|
||||
|
||||
// 用例 5(分支 3 模糊分忧):"这个先放放,你觉得呢"
|
||||
func TestThreeBranch_Branch3_AmbiguousConsult(t *testing.T) {
|
||||
rec := intent.NewRecognizer()
|
||||
ctx := &intent.ContextSnapshot{
|
||||
SnapshotID: "ctx_pending_5",
|
||||
CurrentStage: string(bizdev.StageV01_Triage),
|
||||
PendingExpectation: samplePendingExpectation(),
|
||||
}
|
||||
|
||||
res, err := rec.Recognize(context.Background(), "这个先放放,你觉得呢", ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("recognize failed: %v", err)
|
||||
}
|
||||
|
||||
if res.Branch != intent.BranchAmbiguousFallback {
|
||||
t.Errorf("expected branch AMBIGUOUS_FALLBACK, got %s", res.Branch)
|
||||
}
|
||||
if res.TopIntent.Intent != intent.IntentCasualChat {
|
||||
t.Errorf("expected intent CASUAL_CHAT, got %s", res.TopIntent.Intent)
|
||||
}
|
||||
if res.TopIntent.ClarificationQuestion == "" {
|
||||
t.Errorf("expected non-empty clarification question for branch 3")
|
||||
}
|
||||
}
|
||||
|
||||
+53
-15
@@ -1,24 +1,60 @@
|
||||
package intent
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// IntentType 核心意图类别定义
|
||||
type IntentType string
|
||||
|
||||
const (
|
||||
IntentQueryProgress IntentType = "QUERY_PROGRESS" // 查询进度、成熟度、风险研判或沟通话术
|
||||
IntentSupplementInfo IntentType = "SUPPLEMENT_INFO" // 补充/修改材料、事实信息或参数
|
||||
IntentAdvanceStage IntentType = "ADVANCE_STAGE" // 确认推进到下一成熟度阶段 (如 V0.1 -> V0.5)
|
||||
IntentTriggerTask IntentType = "TRIGGER_TASK" // 显式生成或重新执行特定交付物任务 (如 intake, quote)
|
||||
IntentCasualChat IntentType = "CASUAL_CHAT" // 问候、闲聊或通用帮助引导
|
||||
IntentQueryProgress IntentType = "QUERY_PROGRESS" // 查询进度、成熟度、风险研判或沟通话术
|
||||
IntentSupplementInfo IntentType = "SUPPLEMENT_INFO" // 补充/修改材料、事实信息或参数
|
||||
IntentAdvanceStage IntentType = "ADVANCE_STAGE" // 确认推进到下一成熟度阶段 (如 V0.1 -> V0.5)
|
||||
IntentTriggerTask IntentType = "TRIGGER_TASK" // 显式生成或重新执行特定交付物任务 (如 intake, quote)
|
||||
IntentCasualChat IntentType = "CASUAL_CHAT" // 问候、闲聊或通用帮助引导
|
||||
IntentDeclineExpectation IntentType = "DECLINE_EXPECTATION" // 用户明确暂缓/不采纳上轮建议 (非阻塞放弃)
|
||||
)
|
||||
|
||||
// ResolutionBranch 三分支意图消解类别
|
||||
type ResolutionBranch string
|
||||
|
||||
const (
|
||||
BranchExpectationHit ResolutionBranch = "EXPECTATION_HIT" // 分支 1: 命中历史轮次挂起预期 (肯定/选择/带补丁/取消)
|
||||
BranchNewIntentPreempt ResolutionBranch = "NEW_INTENT_PREEMPT" // 分支 2: 独立新意图抢占 (用户意图最高优先级)
|
||||
BranchAmbiguousFallback ResolutionBranch = "AMBIGUOUS_FALLBACK" // 分支 3: 模糊/无实际意义兜底 (参谋主动分析引导)
|
||||
)
|
||||
|
||||
// ExpectationOption 供用户挑选或单字快速兑现的具体选项
|
||||
type ExpectationOption struct {
|
||||
OptionID string `json:"option_id"` // 选项代号 (如 "1", "2", "advance_v05")
|
||||
Title string `json:"title"` // 选项简述 (如 "推进到 V0.5 方案成型与法规对齐")
|
||||
Description string `json:"description,omitempty"` // 参谋为用户做的工作说明与收益分析
|
||||
Intent IntentType `json:"intent"` // 对应绑定的业务意图
|
||||
Slots map[string]string `json:"slots,omitempty"` // 预设槽位键值对
|
||||
}
|
||||
|
||||
// PendingExpectation 挂起预期草稿(非阻塞桌角备忘贴纸)
|
||||
type PendingExpectation struct {
|
||||
ContextSnippet string `json:"context_snippet"` // 上一轮建议的业务背景简述
|
||||
DefaultOption *ExpectationOption `json:"default_option"` // 参谋最推荐的基准选项(用户说“好/行”时默认执行)
|
||||
Options []ExpectationOption `json:"options"` // 供用户挑选的备选选项列表
|
||||
AllowAffirmative bool `json:"allow_affirmative"` // 是否允许用单字肯定(好/行/可以)触发
|
||||
CreatedAt time.Time `json:"created_at"` // 挂起时间
|
||||
}
|
||||
|
||||
// CandidateIntent 候选意图提案(L1 对话层输出,由 L2 协调层终审)
|
||||
type CandidateIntent struct {
|
||||
Intent IntentType `json:"intent"`
|
||||
Confidence float64 `json:"confidence"` // 置信度 0.0 - 1.0
|
||||
FilledSlots map[string]string `json:"filled_slots"`
|
||||
MissingSlots []string `json:"missing_slots,omitempty"`
|
||||
Reasoning string `json:"reasoning"` // 判断依据与文本佐证
|
||||
ClarificationQuestion string `json:"clarification_question,omitempty"` // 槽位缺失时的澄清追问
|
||||
Intent IntentType `json:"intent"`
|
||||
Confidence float64 `json:"confidence"` // 置信度 0.0 - 1.0
|
||||
Branch ResolutionBranch `json:"branch"` // 归属的三分支判定
|
||||
FilledSlots map[string]string `json:"filled_slots"`
|
||||
MissingSlots []string `json:"missing_slots,omitempty"`
|
||||
Reasoning string `json:"reasoning"` // 判断依据与文本佐证
|
||||
ClarificationQuestion string `json:"clarification_question,omitempty"` // 槽位缺失时的澄清追问
|
||||
MatchedOption *ExpectationOption `json:"matched_option,omitempty"` // 分支 1 命中的具体选项
|
||||
PatchSlots map[string]string `json:"patch_slots,omitempty"` // 分支 1 复合输入的增量槽位补丁
|
||||
IsDecline bool `json:"is_decline,omitempty"` // 是否为明确暂缓/放弃
|
||||
}
|
||||
|
||||
// IntentRecognitionResult 意图识别完整输出(多输出策略)
|
||||
@@ -28,6 +64,7 @@ type IntentRecognitionResult struct {
|
||||
Candidates []CandidateIntent `json:"candidates"`
|
||||
ContextSnapshotID string `json:"context_snapshot_id,omitempty"`
|
||||
RequiresClarifying bool `json:"requires_clarifying"`
|
||||
Branch ResolutionBranch `json:"branch"` // 顶层分支标记
|
||||
}
|
||||
|
||||
// 常见槽位 Key 常量定义
|
||||
@@ -42,8 +79,9 @@ const (
|
||||
|
||||
// ContextSnapshot 传递给 L1 意图识别器的轻量上下文快照
|
||||
type ContextSnapshot struct {
|
||||
SnapshotID string `json:"snapshot_id,omitempty"`
|
||||
ProjectName string `json:"project_name,omitempty"`
|
||||
CurrentStage string `json:"current_stage,omitempty"`
|
||||
AvailableActions []string `json:"available_actions,omitempty"`
|
||||
SnapshotID string `json:"snapshot_id,omitempty"`
|
||||
ProjectName string `json:"project_name,omitempty"`
|
||||
CurrentStage string `json:"current_stage,omitempty"`
|
||||
AvailableActions []string `json:"available_actions,omitempty"`
|
||||
PendingExpectation *PendingExpectation `json:"pending_expectation,omitempty"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user