package coordinator import ( "context" "fmt" "strings" "chat-agent-system/internal/intent" "chat-agent-system/internal/maturity" ) // handleAmbiguousConsult 处理用户犹豫或征询建议 (分支 3 模糊分忧) func (e *CoordinatorEngine) handleAmbiguousConsult(ctx context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext, rawInput string) (*CoordinationResult, error) { if cand != nil && strings.TrimSpace(cand.ClarificationQuestion) != "" { return &CoordinationResult{ Status: StatusClarificationRequired, Intent: cand.Intent, Branch: cand.Branch, ClarificationPrompt: cand.ClarificationQuestion, NaturalReply: cand.ClarificationQuestion, UpdatedContext: userCtx, }, nil } currentStage := userCtx.Project.CurrentStage projName := userCtx.Project.Name if projName == "" { projName = "当前项目" } // 只在低置信度的模糊咨询里,才用关键词改走测试分析。高置信度保持原意图。 if cand.Confidence < 0.6 && (strings.Contains(rawInput, "测试") || strings.Contains(rawInput, "验证") || strings.Contains(rawInput, "型检") || strings.Contains(rawInput, "难点") || strings.Contains(rawInput, "注意") || strings.Contains(rawInput, "比较难") || strings.Contains(rawInput, "方案")) { if cand.FilledSlots == nil { cand.FilledSlots = make(map[string]string) } cand.FilledSlots[intent.SlotQueryTopic] = "testing_analysis" return e.handleQueryProgress(ctx, cand, userCtx, rawInput) } // 犹豫/征询优先于问句标记,避免「你觉得呢」被闲聊分流吃掉阶段对照。 if strings.TrimSpace(rawInput) != "" && intent.IsConsultativeOrHesitant(rawInput) { return e.replyConsultativeAmbiguous(ctx, cand, userCtx, rawInput, currentStage, projName) } // 通用非业务问句不得吃阶段推销模板;改道闲聊处理器(LLM 优先,失败回退问候模板)。 if intent.IsSmallTalkOrGeneralQuestion(rawInput) { return e.handleCasualChat(ctx, userCtx, rawInput) } // 资料名、名词短语等并非犹豫征询:先走对话模型,失败时用中性追问,不用阶段推销模板。 if strings.TrimSpace(rawInput) != "" { return e.handleNonConsultativeAmbiguous(ctx, cand, userCtx, rawInput, projName) } return e.replyConsultativeAmbiguous(ctx, cand, userCtx, rawInput, currentStage, projName) } func (e *CoordinatorEngine) replyConsultativeAmbiguous(ctx context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext, rawInput string, currentStage maturity.MaturityStage, projName string) (*CoordinationResult, error) { var sb strings.Builder sb.WriteString(fmt.Sprintf("完全理解您想再掂量一下。针对「%s」目前的节点,我帮您快速对照一下:\n\n", projName)) if currentStage == maturity.StageV01_Triage { sb.WriteString("• 若推进 V0.5(方案成型与法规对齐):\n") sb.WriteString(" 我可帮您梳理初步架构与注册分类路径,并预估型检成本,便于与客户深入沟通;\n") sb.WriteString("• 若先放一放 / 多做调研:\n") sb.WriteString(" 可先补齐预算、目标市场或使用场景等信息,降低后续返工风险。\n\n") sb.WriteString("💡 如果您接下来更希望先有一份可审阅的材料,我可以先整理一版 V0.5 方案框架与法规路径;节奏仍由您决定。\n\n") sb.WriteString("您更倾向于:\n") sb.WriteString("[1] 先做 V0.5 方案框架(可直接回复“好”或“1”)\n") sb.WriteString("[2] 先说明您当前最关心的细节,我做针对性分析") } else { sb.WriteString("当前各项工作进展平稳。您可以把零散想法随时告诉我,我来帮您沉淀;时机成熟时,再一起推进下一阶段也完全可以。") } reply := sb.String() source := ReplySourceTemplate fallbackReason := "" if llmReply, llmSource, llmReason := e.generateDialogueReply(ctx, rawInput, userCtx); llmReply != "" { reply = llmReply source = llmSource } else { fallbackReason = llmReason } nextExp := BuildNextExpectation(currentStage, projName) // 路由层:复杂自由任务画像命中时,先给即时初答,再附升级建议(复用挂起期望机制,含每会话冷却) reply, nextExp = maybeAttachUpgradeHintWithEngine(e, ctx, rawInput, userCtx, reply, nextExp) userCtx.applyPending(nextExp) return &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: intent.BranchAmbiguousFallback, NaturalReply: reply, AdvisorBrief: userCtx.LatestAdvisorBrief, UpdatedContext: userCtx, NextExpectation: nextExp, ReplySource: source, ReplyFallbackReason: fallbackReason, }, nil } func (e *CoordinatorEngine) handleNonConsultativeAmbiguous(ctx context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext, rawInput, projName string) (*CoordinationResult, error) { reply := neutralUnmatchedReply(rawInput, projName) source := ReplySourceTemplate fallbackReason := "" if llmReply, llmSource, llmReason := e.generateDialogueReply(ctx, rawInput, userCtx); llmReply != "" { reply = llmReply source = llmSource } else { fallbackReason = llmReason } nextExp := BuildNextExpectation(userCtx.Project.CurrentStage, projName) reply, nextExp = maybeAttachUpgradeHintWithEngine(e, ctx, rawInput, userCtx, reply, nextExp) userCtx.applyPending(nextExp) return &CoordinationResult{ Status: StatusAccepted, Intent: cand.Intent, Branch: intent.BranchAmbiguousFallback, NaturalReply: reply, AdvisorBrief: userCtx.LatestAdvisorBrief, UpdatedContext: userCtx, NextExpectation: nextExp, ReplySource: source, ReplyFallbackReason: fallbackReason, }, nil } func neutralUnmatchedReply(rawInput, projName string) string { topic := strings.TrimSpace(rawInput) runes := []rune(topic) if len(runes) > 80 { topic = string(runes[:80]) } if topic == "" { topic = "这段内容" } return fmt.Sprintf("我收到您提到的「%s」(项目「%s」)。\n\n我可以按这个主题检索已有资料,或整理一版要点。请直接补充您最想先看到的材料,或说明您关心的具体问题。", topic, projName) } func (e *CoordinatorEngine) handleCasualChat(ctx context.Context, userCtx *UserProjectContext, rawInput string) (*CoordinationResult, error) { projName := userCtx.Project.Name if projName == "" { projName = "当前医疗器械研发项目" } nextExp := BuildNextExpectation(userCtx.Project.CurrentStage, userCtx.Project.Name) userCtx.applyPending(nextExp) nameSuffix := "" if userCtx.User.Name != "" { nameSuffix = "," + userCtx.User.Name } reply := fmt.Sprintf("您好%s!我是您的项目助手,很高兴协助您推进医疗器械立项与研发协同。\n\n针对「%s」,我可以帮您:\n• 查询进展、成熟度与风险;\n• 沉淀客户需求与技术指标;\n• 起草沟通话术与方案要点;\n• 推进方案论证与报价核算。\n\n如果您愿意,我们可以先从您当前最关心的一块开始——您想先看进展、补事实,还是直接聊方案?", nameSuffix, projName) source := ReplySourceTemplate fallbackReason := "" if llmReply, llmSource, llmReason := e.generateDialogueReply(ctx, rawInput, userCtx); llmReply != "" { reply = llmReply source = llmSource } else { fallbackReason = llmReason } // 路由层:复杂自由任务画像命中时,先给即时初答,再附升级建议(复用挂起期望机制,含每会话冷却) reply, nextExp = maybeAttachUpgradeHintWithEngine(e, ctx, rawInput, userCtx, reply, nextExp) userCtx.applyPending(nextExp) return &CoordinationResult{ Status: StatusAccepted, Intent: intent.IntentCasualChat, Branch: intent.BranchNewIntentPreempt, NaturalReply: reply, AdvisorBrief: userCtx.LatestAdvisorBrief, UpdatedContext: userCtx, NextExpectation: nextExp, ReplySource: source, ReplyFallbackReason: fallbackReason, }, nil } // handleSecurityForbidden 处理安全红线与合规拦截 (ISO 13485 / 21 CFR Part 11) func (e *CoordinatorEngine) handleSecurityForbidden(userCtx *UserProjectContext) (*CoordinationResult, error) { reply := "⚠️ **合规权限拦截**:根据医疗器械质量管理规范(ISO 13485 / 21 CFR Part 11),企业人员的法定身份(姓名、工号、职位、部门)与系统控制台属于基线受控资产,不支持通过对话动态修改。您可通过‘查询我的权限’查看您在当前各文档中的法定职责。" return &CoordinationResult{ Status: StatusRejected, Intent: intent.IntentSecurityForbidden, Branch: intent.BranchNewIntentPreempt, RejectReason: "SECURITY_FORBIDDEN_BASELINE_ASSET", NaturalReply: reply, UpdatedContext: userCtx, }, nil }