package coordinator import ( "context" "log/slog" "strings" "chat-agent-system/internal/intent" ) // FactBlock 是本轮答复允许引用的确定性事实。模型只组织语言,不补充事实。 type FactBlock struct { Title string Body string } const agentNarrativeInstruction = `只把【本轮事实】中的「系统确定性底稿」改写成项目助手的自然中文,并写入 reply.md。 不得新增底稿没有的标准编号、条款、数值、文档状态或执行结果;不得删除底稿中的文档名、路径、数字与〔引用键〕。 若底稿写明尚未执行、需要确认或请到项目设置界面操作,答复必须保留该边界,不得宣称已经完成。` const intentReplyInstructions = `你是「水木东方」医疗器械「项目助手」。只以项目助手自称。 本轮任务是把系统已经确定的事实组织成自然答复,不是重新检索或执行操作。 1. 【本轮事实】是唯一事实来源。底稿未写出的标准编号、条款、项目数据、文档状态与执行结果一律不得编造。 2. 保留底稿中的文档名、路径、数字、角色与〔引用键〕。 3. 底稿若说明需要确认、权限不足、或请到项目设置界面操作,必须原意保留,不得改成已经执行。 4. 使用中文,只把最终答复写入 reply.md。` // narrateReply 优先用智能体改写确定性底稿;失败或关闭时原样返回模板。 func (e *CoordinatorEngine) narrateReply( ctx context.Context, userCtx *UserProjectContext, handler string, it intent.IntentType, rawInput, template, instruction string, ) (string, string) { template = strings.TrimSpace(template) if template == "" || !agentReplyLLMEnabled() || e == nil || e.dialogueBackend == nil { return template, ReplySourceTemplate } if strings.TrimSpace(instruction) == "" { instruction = agentNarrativeInstruction } e.ensureProjectDialogueSnippet(ctx, userCtx) prompt := NewMemoryComposer().ComposeIntentReplyPrompt(rawInput, userCtx, string(it), []FactBlock{{ Title: "系统确定性底稿", Body: template, }}, instruction) content, reason := e.runDialogueBackend(ctx, userCtx, prompt, intentReplyInstructions, "intent_reply_generating", "正在组织项目助手回复...") if reason != "" || strings.TrimSpace(content) == "" { if reason != "" && reason != "backend_nil" && reason != "no_data_dir" { slog.InfoContext(ctx, "agent_reply_fallback", "handler", handler, "intent", string(it), "reason", reason) } return template, ReplySourceTemplate } return strings.TrimSpace(content), ReplySourcePiAgent } // applyNarrative 用智能体叙述替换已生成结果的 NaturalReply,并写入 ReplySource。 // 调用方只应对知识叙述、帮助导航和已完成/已受理通报使用;确认提问与权限拒绝不要调用。 func (e *CoordinatorEngine) applyNarrative( ctx context.Context, userCtx *UserProjectContext, handler, rawInput, instruction string, res *CoordinationResult, ) *CoordinationResult { if res == nil { return nil } reply, source := e.narrateReply(ctx, userCtx, handler, res.Intent, rawInput, res.NaturalReply, instruction) res.NaturalReply = reply res.ReplySource = source return res }