Files
Agent-System/internal/intent/recognizer.go
T
WeiCHEN ed6de25b82 merge(dev): absorb smalltalk routing into intent split
Keep use-case split of recognizer.go and port IsSmallTalk helpers
into recognizer_helpers.go so residual can re-integrate cleanly.
2026-09-23 10:13:29 +08:00

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
}