package intent import ( "context" "strings" "chat-agent-system/internal/maturity" ) // 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) { result, err := r.recognizeCore(ctx, input, userCtx) if err != nil || result == nil { return result, err } attachPlanProposal(ctx, r, input, userCtx, result) return result, nil } func (r *RuleBasedRecognizer) recognizeCore(ctx context.Context, input string, userCtx *ContextSnapshot) (*IntentRecognitionResult, error) { trimmed := strings.TrimSpace(input) if trimmed == "" { result := &IntentRecognitionResult{ RawInput: input, Branch: BranchAmbiguousFallback, TopIntent: &CandidateIntent{ Intent: IntentCasualChat, Branch: BranchAmbiguousFallback, Confidence: 1.0, Reasoning: "输入为空", }, Candidates: []CandidateIntent{ {Intent: IntentCasualChat, Branch: BranchAmbiguousFallback, Confidence: 1.0, Reasoning: "空输入"}, }, } finalizeRecognitionResult(result) return result, nil } snapID := "" currentStage := maturity.StageV01_Triage var pending *PendingExpectation if userCtx != nil { snapID = userCtx.SnapshotID if userCtx.CurrentStage != "" { currentStage = maturity.MaturityStage(userCtx.CurrentStage) } pending = userCtx.PendingExpectation } // ------------------------------------------------------------- // 阶段零 (Stage 0):态势前置过滤与安全防护检查 (Security & UI Guard) // ------------------------------------------------------------- // 1. 安全红线校验:严禁通过自然语言对话修改系统受控配置、用户法定身份/职位/工号 // 「不要修改配置,只查询…」类否定写 + 只读意图不得误触安全拦截 if isSecurityViolation(trimmed) && !isNegatedWriteWithReadonlyIntent(trimmed) { top := &CandidateIntent{ Intent: IntentSecurityForbidden, Branch: BranchNewIntentPreempt, Confidence: 1.0, FilledSlots: map[string]string{SlotForbiddenTarget: "identity_or_settings"}, Reasoning: "检测到试图修改系统底层受控配置或用户法定身份/职位/工号,根据医疗器械质量与合规管理规范(ISO 13485 / 21 CFR Part 11),触发安全红线拦截", } result := &IntentRecognitionResult{ RawInput: trimmed, Branch: BranchNewIntentPreempt, TopIntent: top, Candidates: []CandidateIntent{*top}, ContextSnapshotID: snapID, RequiresClarifying: false, } finalizeRecognitionResult(result) return result, nil } // 1.5 否定写 + 只读查询:不得误触安全拦截,也不得当作写操作 if isNegatedWriteWithReadonlyIntent(trimmed) { top := &CandidateIntent{ Intent: IntentQueryUserRights, Branch: BranchNewIntentPreempt, Confidence: 0.88, FilledSlots: map[string]string{SlotQueryTopic: "system_configuration"}, Reasoning: "用户明确否定修改配置并请求只读查询,不得路由为 SECURITY_FORBIDDEN 或写操作", ClarificationQuestion: "我理解您希望只读查看当前系统配置,不会对配置做任何修改。请问您具体想查看哪一项配置?", } result := &IntentRecognitionResult{ RawInput: trimmed, Branch: BranchNewIntentPreempt, TopIntent: top, Candidates: []CandidateIntent{*top}, ContextSnapshotID: snapID, RequiresClarifying: true, } finalizeRecognitionResult(result) return result, nil } // 2. 纯 UI 视窗与主题控制指令拦截:归入 CASUAL_CHAT 并给出不可执行提示 if isPureUICommand(trimmed) { top := &CandidateIntent{ Intent: IntentCasualChat, Branch: BranchNewIntentPreempt, Confidence: 0.95, FilledSlots: make(map[string]string), Reasoning: "纯UI视窗或主题控制指令,系统暂不支持通过对话直接操作界面视窗布局或主题,请通过界面按钮或快捷键操作", ClarificationQuestion: "检测到您下达了界面视窗或主题控制指令(如折叠/打开侧边栏、切换主题/深色浅色)。该操作属于前端UI视窗布局与主题控制,请直接点击界面右上角或侧边的视窗/主题切换按钮。", } result := &IntentRecognitionResult{ RawInput: trimmed, Branch: BranchNewIntentPreempt, TopIntent: top, Candidates: []CandidateIntent{*top}, ContextSnapshotID: snapID, RequiresClarifying: false, } finalizeRecognitionResult(result) return result, nil } // ------------------------------------------------------------- // 阶段 0.5:若输入明确是咨询参谋/犹豫(如“这个先放放,你觉得呢”),优先归入分支 3 (参谋主动分忧) // ------------------------------------------------------------- if isConsultativeOrHesitant(trimmed) { res := buildAmbiguousConsultResult(trimmed, snapID, pending) return res, nil } // ------------------------------------------------------------- // 阶段 0.8:分支 1 判定——命中历史轮次挂起预期 (肯定 / 选项序号 / 复合带补丁 / 明确撤销) // ------------------------------------------------------------- if pending != nil { if hitCand := matchExpectationHit(trimmed, pending); hitCand != nil { result := &IntentRecognitionResult{ RawInput: trimmed, Branch: BranchExpectationHit, TopIntent: hitCand, Candidates: []CandidateIntent{*hitCand}, ContextSnapshotID: snapID, } finalizeRecognitionResult(result) return result, nil } } // ------------------------------------------------------------- // 阶段一与阶段二 (Stage 1 & Stage 2):领域大类路由、微观细分动作与槽位提取 // ------------------------------------------------------------- var candidates []CandidateIntent // 领域 1: 法定受控文档治理流转 (GOV_LIFECYCLE) // (涵盖 驳回、通过、送审、认领、终审批准、提案裁决) if cand := matchDocGovAction(trimmed, userCtx); cand != nil { cand.Branch = BranchNewIntentPreempt candidates = append(candidates, *cand) } // 领域 2: 决策辅助、合规与知识检索 (DECISION_QUERY - Knowledge & Rights) // (涵盖 法规强标、标杆类似案例、用户法定职责与权限) if cand := matchKnowledgeQuery(trimmed); cand != nil { cand.Branch = BranchNewIntentPreempt candidates = append(candidates, *cand) } // 领域 3: 交付物与阶段演进 (CONTENT_SYNTHESIS) // 3.1 阶段推进 (ADVANCE_STAGE) if cand := matchAdvanceStage(trimmed, currentStage); cand != nil { cand.Branch = BranchNewIntentPreempt candidates = append(candidates, *cand) } if cand := matchImpactIntent(trimmed); cand != nil { cand.Branch = BranchNewIntentPreempt candidates = append(candidates, *cand) } // 3.2 补充信息/参数事实 (SUPPLEMENT_INFO) if cand := matchSupplementInfo(trimmed); cand != nil { cand.Branch = BranchNewIntentPreempt candidates = append(candidates, *cand) } // 3.3 交付任务触发 (TRIGGER_TASK) if cand := matchTriggerTask(trimmed, userCtx); cand != nil { cand.Branch = BranchNewIntentPreempt candidates = append(candidates, *cand) } // 领域 4: 长任务运行控制 (Phase 4) if cand := matchRunControl(trimmed, userCtx); cand != nil { cand.Branch = BranchNewIntentPreempt candidates = append(candidates, *cand) } // 领域 5: 工作流控制与状态查询 (Phase 3 — 优先于进度查询,避免「测试经理」等词误判) if CurrentFeatureFlags().WorkflowIntentsEnabled { if cand := matchQueryWorkflowStatus(trimmed, userCtx); cand != nil { cand.Branch = BranchNewIntentPreempt candidates = append(candidates, *cand) } if cand := matchSwitchWorkflow(trimmed, userCtx); cand != nil { cand.Branch = BranchNewIntentPreempt candidates = append(candidates, *cand) } if cand := matchTriggerWorkflow(trimmed, userCtx); cand != nil { cand.Branch = BranchNewIntentPreempt candidates = append(candidates, *cand) } } // 领域 5: 文档/交付物状态只读查询 if cand := matchQueryDocStatus(trimmed, userCtx); cand != nil { cand.Branch = BranchNewIntentPreempt candidates = append(candidates, *cand) } // 领域 6: 帮助与暂不支持 if cand := matchHelpCapabilities(trimmed); cand != nil { cand.Branch = BranchNewIntentPreempt candidates = append(candidates, *cand) } if cand := matchUnsupportedActionCandidate(trimmed); cand != nil { cand.Branch = BranchNewIntentPreempt candidates = append(candidates, *cand) } // 领域 7: 进度与技术研判查询 (DECISION_QUERY - Progress & Technical) if cand := matchQueryProgress(trimmed); cand != nil { cand.Branch = BranchNewIntentPreempt candidates = append(candidates, *cand) } // 通用对话/闲聊保底 (CASUAL_CHAT) chatCand := CandidateIntent{ Intent: IntentCasualChat, Branch: BranchNewIntentPreempt, Confidence: 0.2, FilledSlots: make(map[string]string), Reasoning: "未匹配到明确业务指令,归入通用对话", } if isGreetingOrHelp(trimmed) { chatCand.Confidence = 0.85 chatCand.Reasoning = "匹配到问候语或通用帮助请求" } else if IsSmallTalkOrGeneralQuestion(trimmed) { chatCand.Confidence = 0.7 chatCand.Reasoning = "通用非业务问句,归入闲聊对话" } candidates = append(candidates, chatCand) sortCandidates(candidates) top := &candidates[0] applyCapabilityHint(top, userCtx) // ------------------------------------------------------------- // 阶段三 (Stage 3):槽位完备性与置信度裁决 (门禁裁决与追问下发) // ------------------------------------------------------------- branch := BranchNewIntentPreempt requiresClarifying := false slotOrPolicyClarify := false // 1. 若为 DOCGOV_REVIEW_REJECT 或 DOCGOV_FINAL_REJECT,检查必填槽位 reject_reason if top.Intent == IntentDocGovReviewReject || top.Intent == IntentDocGovFinalReject { if top.FilledSlots[SlotRejectReason] == "" { requiresClarifying = true slotOrPolicyClarify = true top.ClarificationQuestion = "请说明具体的法定驳回理由,我将为您注入文档批注并退回编制阶段。" top.MissingSlots = []string{SlotRejectReason} } } // 2. 权限/生命周期前置剪枝:保留真实写意图,仅标记需澄清(L2 最终裁决) if strings.Contains(top.Reasoning, "权限不符") || strings.Contains(top.Reasoning, "前置剪枝") { requiresClarifying = true slotOrPolicyClarify = true } // 3. 已有定向澄清问题(如查询语气的只读确认) if top.ClarificationQuestion != "" && !slotOrPolicyClarify { requiresClarifying = true slotOrPolicyClarify = true } // 4. 写/读候选冲突:分差不足时禁止直接执行 Top1 if applyCandidateConflictGate(candidates, top) { requiresClarifying = true slotOrPolicyClarify = true } // 5. 低置信度且无定向澄清时才归入模糊咨询分支 if top.Confidence < 0.6 && !slotOrPolicyClarify { branch = BranchAmbiguousFallback top.Branch = BranchAmbiguousFallback requiresClarifying = true } result := &IntentRecognitionResult{ RawInput: trimmed, Branch: branch, TopIntent: top, Candidates: candidates, ContextSnapshotID: snapID, RequiresClarifying: requiresClarifying, } finalizeRecognitionResult(result) if pending != nil { if plan := buildExpectationPlusNewPlan(ctx, r, trimmed, userCtx, pending, result); plan != nil { result.IntentPlan = plan } } if result.IntentPlan == nil { if plan := buildLimitedIntentPlan(ctx, r, trimmed, userCtx, result); plan != nil { result.IntentPlan = plan } } return result, nil }