Keep use-case split of recognizer.go and port IsSmallTalk helpers into recognizer_helpers.go so residual can re-integrate cleanly.
327 lines
12 KiB
Go
327 lines
12 KiB
Go
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
|
|
}
|