package intent import ( "context" "fmt" "math" "sort" "strings" "time" "logging-system/logsys" ) // Recognizer 是识别的公开端口(Query 语义:纯函数式、无副作用)。 // error 只表示系统级失败(nil 快照、输入超长);BranchFallback 是正常结果。 type Recognizer interface { Recognize(ctx context.Context, input string, snap *Snapshot) (Result, error) } // FunnelRecognizer 四级漏斗实现:guard → expectation → rules → adjudicate, // 可选注入挂起预期提供者与 Ambiguous 分支的 L2 分类器。 type FunnelRecognizer struct { log *logsys.Logger exp ExpectationProvider cls Classifier } // NewRecognizer 构建漏斗识别器。opts 仅使用 Log 字段(阈值类参数在快照内); // exp / cls 可为 nil(对应阶段不启用)。 func NewRecognizer(opts Options, exp ExpectationProvider, cls Classifier) *FunnelRecognizer { o := opts.normalized() return &FunnelRecognizer{log: o.Log, exp: exp, cls: cls} } // Recognize 识别一次输入。确定性:同输入 + 同快照 → 同结果(日志中的耗时除外)。 func (r *FunnelRecognizer) Recognize(ctx context.Context, input string, snap *Snapshot) (Result, error) { if snap == nil { err := newError(InvalidArgument, "snapshot 为 nil") r.log.Module("intent").WithContext(ctx).Error("intent recognize rejected", err) return Result{}, err } if len(input) > snap.maxInput { err := newError(InvalidArgument, "输入超过长度上限", "actual_len", len(input), "max_len", snap.maxInput) r.log.Module("intent").WithContext(ctx).Error("intent recognize rejected", err) return Result{}, err } start := time.Now() res := r.recognize(ctx, input, snap) res.Commands = deriveCommands(res, snap) topID, conf := "", 0.0 if res.Top != nil { topID, conf = res.Top.ID, res.Top.Confidence } r.log.Module("intent").WithContext(ctx).Info("intent recognized", "branch", string(res.Branch), "top_id", topID, "confidence", conf, "snapshot_revision", res.SnapshotRevision, "duration_ms", time.Since(start).Milliseconds()) return res, nil } func (r *FunnelRecognizer) recognize(ctx context.Context, input string, snap *Snapshot) Result { if strings.TrimSpace(input) == "" { return Result{Input: input, Branch: BranchFallback, SnapshotRevision: snap.rev} } // 否定只认 excludes_any 子串,不做语义否定;「不要写入」这类说法交给命令层 Cancel。 matchInput, folded := snap.foldInput(input) // Stage 0:守卫命中即短路,可表达"识别即拒绝"。 if guardHits := snap.evalGuard(matchInput, folded); len(guardHits) > 0 { cands := candidatesOf(trimHits(guardHits, snap.topN), input, true) top := cands[0] return Result{Input: input, Branch: BranchGuardHit, Top: &top, Candidates: cands, SnapshotRevision: snap.rev} } // Stage 1:会话挂起预期优先于一般规则(宿主可选注入)。 if r.exp != nil { if pending := r.exp.Pending(ctx); pending != nil && len(pending.IntentIDs) > 0 { if hits := snap.evalIDs(pending.IntentIDs, matchInput, folded); len(hits) > 0 { cands := candidatesOf(trimHits(hits, snap.topN), input, true) top := cands[0] return Result{Input: input, Branch: BranchExpectationHit, Top: &top, Candidates: cands, SnapshotRevision: snap.rev} } } } // Stage 2 + 3:规则计分与置信度裁决。 hits := snap.evalRules(matchInput, folded) switch { case len(hits) == 0: return Result{Input: input, Branch: BranchFallback, SnapshotRevision: snap.rev} case len(hits) == 1 || hits[0].score-hits[1].score >= snap.margin || tieBreakByPriority(hits): cands := candidatesOf(trimHits(hits, snap.topN), input, true) top := cands[0] return Result{Input: input, Branch: BranchMatched, Top: &top, Candidates: cands, SnapshotRevision: snap.rev} } // Ambiguous:可选 L2 分类器重排;失败或结果不合法都保持 Ambiguous。 cands := candidatesOf(trimHits(hits, snap.topN), input, false) if r.cls != nil { refined, err := r.cls.Refine(ctx, input, cands) if err != nil { r.log.Module("intent").WithContext(ctx).Warn("intent classifier failed; keep ambiguous", "error", err.Error()) return Result{Input: input, Branch: BranchAmbiguous, Candidates: cands, SnapshotRevision: snap.rev} } rebuilt, err := validateRefined(cands, refined) if err != nil { r.log.Module("intent").WithContext(ctx).Warn("intent classifier result rejected; keep ambiguous", "error", err.Error()) return Result{Input: input, Branch: BranchAmbiguous, Candidates: cands, SnapshotRevision: snap.rev} } if top, ok := adjudicateRefined(rebuilt, snap); ok { snap.fillTopSlots(&top, input) top.Reason = top.Reason + "; refined_by=classifier" return Result{Input: input, Branch: BranchMatched, Top: &top, Candidates: rebuilt, SnapshotRevision: snap.rev} } cands = rebuilt } return Result{Input: input, Branch: BranchAmbiguous, Candidates: cands, SnapshotRevision: snap.rev} } // tieBreakByPriority 报告同分(分差为 0)且前两名 Priority 不同: // 此时高 Priority 者直接胜出,不再标歧(I5)。分差小于 margin 但非零时 // 仍按 ambiguous 处理(margin 语义不变)。 func tieBreakByPriority(hits []specHit) bool { if len(hits) < 2 { return false } return hits[0].score == hits[1].score && hits[0].cs.spec.Priority > hits[1].cs.spec.Priority } // validateRefined 要求分类器返回与原候选完全相同的 ID 集合且无重复。 // 置信度必须是有限值并落在 [0,1];越界判为不合法,不截断。 // 除 Confidence 外的字段一律按 ID 从原候选重建,分类器改写的 Handler 等不生效。 func validateRefined(original, refined []Candidate) ([]Candidate, error) { byID := make(map[string]Candidate, len(original)) for _, c := range original { if _, dup := byID[c.ID]; dup { return nil, fmt.Errorf("duplicate original id %s", c.ID) } byID[c.ID] = c } if len(refined) != len(original) { return nil, fmt.Errorf("refined candidate count %d != %d", len(refined), len(original)) } seen := make(map[string]struct{}, len(refined)) out := make([]Candidate, len(refined)) for i, got := range refined { if _, dup := seen[got.ID]; dup { return nil, fmt.Errorf("duplicate refined id %s", got.ID) } seen[got.ID] = struct{}{} src, ok := byID[got.ID] if !ok { return nil, fmt.Errorf("unknown refined id %s", got.ID) } if !validConfidence(got.Confidence) { return nil, fmt.Errorf("confidence out of range for %s", got.ID) } src.Confidence = got.Confidence out[i] = src } return out, nil } func validConfidence(v float64) bool { return !math.IsNaN(v) && !math.IsInf(v, 0) && v >= 0 && v <= 1 } // adjudicateRefined 对分类器结果重排后应用同一裁决规则;分差仍贴近则返回 false。 func adjudicateRefined(refined []Candidate, snap *Snapshot) (Candidate, bool) { if len(refined) == 0 { return Candidate{}, false } sorted := append([]Candidate(nil), refined...) sortCandidates(sorted) if len(sorted) > 1 && sorted[0].Confidence-sorted[1].Confidence < snap.margin { return Candidate{}, false } return sorted[0], true } func sortCandidates(cands []Candidate) { sort.SliceStable(cands, func(i, j int) bool { a, b := cands[i], cands[j] if a.Confidence != b.Confidence { return a.Confidence > b.Confidence } if a.Priority != b.Priority { return a.Priority > b.Priority } return a.ID < b.ID }) } func trimHits(hits []specHit, n int) []specHit { if len(hits) > n { return hits[:n] } return hits } // Explain 输出单条输入的裁决轨迹(guard 与 rules 全量、确定性、无识别器状态)。 // 这是配置规范要求的只读诊断;intentctl explain 在此之上构建。 func (s *Snapshot) Explain(input string) Trace { trace := Trace{InputLen: len(input), AmbiguityMargin: s.margin} if strings.TrimSpace(input) == "" { trace.Branch = BranchFallback return trace } matchInput, folded := s.foldInput(input) for _, idx := range s.guardIdx { cs := s.specs[idx] o := cs.evaluate(matchInput, folded) if o.passed { trace.Guard = append(trace.Guard, ScoredSpec{ID: cs.spec.ID, Score: o.score, HitTerms: o.hitTerms, HitPatterns: o.hitPat}) } else { trace.GuardRejected = append(trace.GuardRejected, RejectedSpec{ID: cs.spec.ID, Reason: o.reject}) } } sortScored(trace.Guard) sortRejected(trace.GuardRejected) if len(trace.Guard) > 0 { trace.Branch = BranchGuardHit trace.TopID = trace.Guard[0].ID return trace } var hits []specHit for _, cs := range s.specs { if cs.guard { continue } o := cs.evaluate(matchInput, folded) if o.passed { hits = append(hits, specHit{cs: cs, score: o.score, terms: o.hitTerms, pats: o.hitPat}) trace.Rules = append(trace.Rules, ScoredSpec{ID: cs.spec.ID, Score: o.score, HitTerms: o.hitTerms, HitPatterns: o.hitPat}) } else { trace.RulesRejected = append(trace.RulesRejected, RejectedSpec{ID: cs.spec.ID, Reason: o.reject}) } } sortHits(hits) sortScored(trace.Rules) sortRejected(trace.RulesRejected) switch { case len(hits) == 0: trace.Branch = BranchFallback case len(hits) == 1 || hits[0].score-hits[1].score >= s.margin || tieBreakByPriority(hits): trace.Branch = BranchMatched trace.TopID = hits[0].cs.spec.ID default: trace.Branch = BranchAmbiguous } return trace } func sortScored(ss []ScoredSpec) { sort.SliceStable(ss, func(i, j int) bool { return ss[i].ID < ss[j].ID }) } func sortRejected(rs []RejectedSpec) { sort.SliceStable(rs, func(i, j int) bool { return rs[i].ID < rs[j].ID }) }