feat(intent): implement three-branch flexible intent resolution and expectation contracts

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
WeiCHEN
2026-09-07 22:11:45 +08:00
co-authored by Cursor
parent 3a15143fa7
commit d3251dee0b
3 changed files with 507 additions and 53 deletions
+270 -38
View File
@@ -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++ {
+184
View File
@@ -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
View File
@@ -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"`
}