From ccc485a670c23d95414bed47bcf99e65cfaceccb Mon Sep 17 00:00:00 2001 From: WeiCHEN Date: Wed, 9 Sep 2026 16:25:35 +0800 Subject: [PATCH] fix(agent): scope Task 2 to SSE phases/errors without dialogue_llm leak - untrack internal/coordinator/dialogue_llm.go (kept on disk untracked as local WIP for a future dialogue-LLM commit with its own tests; it depends on engine.go fields (dialogueBackend, ReplySource) not yet committed) - slim internal/chat/session.go back to Task 2 scope relative to bfa6889: keep EmitErrorInfo(stream.ClassifyError(...)) for intent+coordination errors and the dialogue_generating EmitPhase before ProcessIntent; drop sync.Mutex/mu, ChatResponse.ReplySource, preference/RecentDialogue injection block, and collectRecentDialogue helper (unrelated WIP for the dialogue-LLM feature) Co-authored-by: Cursor --- internal/chat/session.go | 52 +------ internal/coordinator/dialogue_llm.go | 218 --------------------------- 2 files changed, 5 insertions(+), 265 deletions(-) delete mode 100644 internal/coordinator/dialogue_llm.go diff --git a/internal/chat/session.go b/internal/chat/session.go index 84becbec..4c0e638a 100644 --- a/internal/chat/session.go +++ b/internal/chat/session.go @@ -3,7 +3,6 @@ package chat import ( "context" "fmt" - "sync" "time" "chat-agent-system/internal/coordinator" @@ -28,7 +27,6 @@ type Message struct { // ChatResponse 会话单轮响应 type ChatResponse struct { ReplyText string `json:"reply_text"` - ReplySource string `json:"reply_source,omitempty"` // pi-agent | template Branch intent.ResolutionBranch `json:"branch"` IntentResult *intent.IntentRecognitionResult `json:"intent_result"` CoordinationResult *coordinator.CoordinationResult `json:"coordination_result"` @@ -48,8 +46,6 @@ type Session struct { CtxMgr *ctxmgr.ContextManager `json:"-"` Messages []Message `json:"messages"` CreatedAt time.Time `json:"created_at"` - - mu sync.Mutex `json:"-"` // 同 session 消息串行化,保障并发正确性 } // NewSession 创建新对话会话 @@ -90,9 +86,6 @@ func (s *Session) SendMessageStream(ctx context.Context, userInput string, liste } func (s *Session) sendMessageInternal(ctx context.Context, userInput string, streamer *stream.Streamer) (*ChatResponse, error) { - s.mu.Lock() - defer s.mu.Unlock() - start := time.Now() if streamer != nil { streamer.EmitPhase("intent_recognizing", "正在研判您的意图与项目上下文...", "coordinator", 0, 0, 0) @@ -107,8 +100,7 @@ func (s *Session) sendMessageInternal(ctx context.Context, userInput string, str s.Messages = append(s.Messages, userMsg) if s.CtxMgr != nil { - userMeta := map[string]any{"project_id": s.ProjectID} - _ = s.CtxMgr.RecordUserMessage(ctx, s.ID, s.UserID, "user", userInput, "", "", userMeta) + _ = s.CtxMgr.RecordUserMessage(ctx, s.ID, s.UserID, "user", userInput, "", "", nil) } // 1. 意图识别 (L1 对话层,带非阻塞桌角备忘快照) @@ -134,23 +126,7 @@ func (s *Session) sendMessageInternal(ctx context.Context, userInput string, str ) } - // 2. 注入近轮历史与用户习惯偏好,供对话 LLM(不含本轮刚追加的用户消息,本轮由 RawInput 传入) - if s.UserCtx != nil { - s.UserCtx.RecentDialogue = s.collectRecentDialogue(8) - if s.CtxMgr != nil && s.UserID > 0 { - if pref, err := s.CtxMgr.GetUserPreference(ctx, s.UserID); err == nil && pref != nil { - s.UserCtx.PersonalizedGuidance = ctxmgr.NewPreferenceReflector().GeneratePromptGuidance(pref) - if pref.Name != "" && s.UserCtx.User.Name == "" { - s.UserCtx.User.Name = pref.Name - } - if pref.Role != "" && s.UserCtx.User.Role == "" { - s.UserCtx.User.Role = pref.Role - } - } - } - } - - // 3. 协调调度与校验 (L2 协调层) + // 2. 协调调度与校验 (L2 协调层) if streamer != nil { streamer.EmitPhase("dialogue_generating", "正在整理回复...", "assistant", 0, 0, 40) } @@ -162,7 +138,7 @@ func (s *Session) sendMessageInternal(ctx context.Context, userInput string, str return nil, fmt.Errorf("coordination execution failed: %w", err) } - // 4. 更新上下文与挂起预期生命周期 + // 3. 更新上下文与挂起预期生命周期 if coordRes.UpdatedContext != nil { s.UserCtx = coordRes.UpdatedContext } @@ -189,11 +165,10 @@ func (s *Session) sendMessageInternal(ctx context.Context, userInput string, str s.Messages = append(s.Messages, asstMsg) if s.CtxMgr != nil { - asstMeta := map[string]any{"project_id": s.ProjectID} - _ = s.CtxMgr.RecordUserMessage(ctx, s.ID, 0, "assistant", coordRes.NaturalReply, string(coordRes.Intent), string(recRes.Branch), asstMeta) + _ = s.CtxMgr.RecordUserMessage(ctx, s.ID, 0, "assistant", coordRes.NaturalReply, string(coordRes.Intent), string(recRes.Branch), nil) } - // 5. 流式推送参谋自然语言回复与决策锦囊 + // 4. 流式推送参谋自然语言回复与决策锦囊 if streamer != nil { if coordRes.NaturalReply != "" { streamer.EmitDelta(coordRes.NaturalReply) @@ -206,7 +181,6 @@ func (s *Session) sendMessageInternal(ctx context.Context, userInput string, str return &ChatResponse{ ReplyText: coordRes.NaturalReply, - ReplySource: coordRes.ReplySource, Branch: recRes.Branch, IntentResult: recRes, CoordinationResult: coordRes, @@ -215,19 +189,3 @@ func (s *Session) sendMessageInternal(ctx context.Context, userInput string, str PendingExpectation: s.UserCtx.PendingExpectation, }, nil } - -// collectRecentDialogue 收集本轮之前的近 N 条消息供 LLM 上下文(排除末尾刚写入的本轮用户消息) -func (s *Session) collectRecentDialogue(limit int) []coordinator.DialogueTurn { - if limit <= 0 || len(s.Messages) <= 1 { - return nil - } - prior := s.Messages[:len(s.Messages)-1] - if len(prior) > limit { - prior = prior[len(prior)-limit:] - } - out := make([]coordinator.DialogueTurn, 0, len(prior)) - for _, m := range prior { - out = append(out, coordinator.DialogueTurn{Role: m.Role, Content: m.Content}) - } - return out -} diff --git a/internal/coordinator/dialogue_llm.go b/internal/coordinator/dialogue_llm.go deleted file mode 100644 index 7d5bd117..00000000 --- a/internal/coordinator/dialogue_llm.go +++ /dev/null @@ -1,218 +0,0 @@ -package coordinator - -import ( - "context" - "fmt" - "os" - "path/filepath" - "strings" - "time" - - "chat-agent-system/internal/agent" - "chat-agent-system/internal/stream" -) - -const ( - ReplySourcePiAgent = "pi-agent" - ReplySourceTemplate = "template" - dialogueHistoryMax = 8 - dialogueTimeout = 3 * time.Minute -) - -// DialogueTurn 多轮对话历史片段(不落盘到 JSON 快照) -type DialogueTurn struct { - Role string `json:"role"` - Content string `json:"content"` -} - -// generateDialogueReply 通过 pi-agent 生成参谋自然语言答复;失败返回空串由调用方回退模板。 -func (e *CoordinatorEngine) generateDialogueReply( - ctx context.Context, - userInput string, - userCtx *UserProjectContext, -) (reply string, source string) { - if e == nil || e.dialogueBackend == nil || strings.TrimSpace(userInput) == "" { - return "", ReplySourceTemplate - } - - workRoot := e.dataDir - if workRoot == "" { - workRoot = "./data/dev" - } - sessionKey := "anon" - if userCtx != nil && userCtx.SnapshotID != "" { - sessionKey = userCtx.SnapshotID - } else if userCtx != nil && userCtx.Project.ID != "" { - sessionKey = userCtx.Project.ID - } - // 每轮独立子目录,避免复用上一轮残留的 reply.md(曾导致「问驳回却答疲劳寿命」) - workDir := filepath.Join( - workRoot, - "workspaces", - "dialogue", - sanitizePathSegment(sessionKey), - fmt.Sprintf("turn_%d", time.Now().UnixNano()), - ) - if err := os.MkdirAll(workDir, 0o755); err != nil { - return "", ReplySourceTemplate - } - - prompt := buildDialoguePrompt(userInput, userCtx) - instructions := `你是「水木东方」医疗器械「项目助手」——温和、靠谱、会替用户着想的项目助理。 -定位:以助理身份与用户友好协作。能力覆盖商务研判、工程方案与法规合规,但对外只以「项目助手」自称,不要分饰多角色,也不要自称「小队/参谋/总工/法规专家」。 - -协作立场(优先遵守): -1. 站在用户视角:结合其姓名/角色、当前阶段、待办、近期对话与已知习惯偏好,揣摩本轮提问背后想达成的效果(如准备汇报、消解风险、推进文档、对外沟通、弄清技术点)。 -2. 先回应核心诉求,再补一两句温和的辅助内容:可落地的下一步、需留意的点,或「如果您接下来是想…,我可以帮您…」。 -3. 主动但轻量地给建议:通常给 1~2 个最贴合的选项,用商量口吻;不强推、不推销阶段、不堆选项清单。 -4. 若上下文含「用户个性化工作习惯与偏好」,优先遵循其沟通风格与关注重点来组织答复。 - -回复风格: -1. 语气温和、友好、自然;可用适度口语与关心式过渡(如「好的」「我帮您看一下」),保持专业分寸。 -2. 严谨有重点:先结论/直接回答,再补必要依据;篇幅适中——有实质内容,但不冗长堆砌。 -3. 直接回答本轮问题;闲聊可轻松回应,并可自然关联当前项目。 -4. 技术机理、法规、工程问题须给出有实质内容的专业解答,不要回避成阶段推销话术。 -5. 若用户询问受控文档驳回/退回原因或整改要点,必须紧扣本轮问题与已知驳回理由作答,禁止复述无关的上一轮技术长文。 -6. 不要声称已修改系统配置、用户身份或受控文档状态;不要伪造已执行的工作流结果。 -7. 只把最终答复写入 reply.md,使用中文,不要输出无关元数据。` - - if st := stream.FromContext(ctx); st != nil { - st.EmitPhase("dialogue_generating", "正在生成项目助手回复...", "assistant", 0, 0, 60) - } - - runCtx, cancel := context.WithTimeout(ctx, dialogueTimeout) - defer cancel() - - startedAt := time.Now() - res, err := e.dialogueBackend.Execute(runCtx, agent.Request{ - WorkDir: workDir, - Prompt: prompt, - Instructions: instructions, - Targets: []string{"reply.md"}, - Timeout: dialogueTimeout, - }) - if err != nil || res == nil || !res.Success { - return "", ReplySourceTemplate - } - - content := "" - if b, ok := res.Patches["reply.md"]; ok { - content = string(b) - } - // 仅当本轮执行后新写入的 reply.md 才可读盘,禁止吃到历史残留 - if strings.TrimSpace(content) == "" { - replyPath := filepath.Join(workDir, "reply.md") - if info, statErr := os.Stat(replyPath); statErr == nil && !info.ModTime().Before(startedAt.Add(-2*time.Second)) { - if raw, readErr := os.ReadFile(replyPath); readErr == nil { - content = string(raw) - } - } - } - content = strings.TrimSpace(content) - if content == "" { - return "", ReplySourceTemplate - } - return content, ReplySourcePiAgent -} - -func buildDialoguePrompt(userInput string, userCtx *UserProjectContext) string { - var sb strings.Builder - sb.WriteString("【项目上下文】\n") - if userCtx != nil { - proj := userCtx.Project - name := proj.Name - if name == "" { - name = "未命名医疗器械项目" - } - sb.WriteString(fmt.Sprintf("- 项目名称: %s\n", name)) - if userCtx.User.Name != "" { - sb.WriteString(fmt.Sprintf("- 当前用户: %s", userCtx.User.Name)) - if userCtx.User.Role != "" { - sb.WriteString(fmt.Sprintf("(%s)", userCtx.User.Role)) - } - if userCtx.User.ProjectRole != "" { - sb.WriteString(fmt.Sprintf(" / 项目角色: %s", userCtx.User.ProjectRole)) - } - sb.WriteString("\n") - } - if proj.CustomerName != "" { - sb.WriteString(fmt.Sprintf("- 客户: %s\n", proj.CustomerName)) - } - if proj.DeviceType != "" { - sb.WriteString(fmt.Sprintf("- 器械类型: %s\n", proj.DeviceType)) - } - if proj.CurrentStage != "" { - sb.WriteString(fmt.Sprintf("- 当前阶段: %s\n", proj.CurrentStage)) - } - if len(userCtx.Pending.ConfirmModifications) > 0 || - len(userCtx.Pending.PendingReviews) > 0 || - len(userCtx.Pending.PendingApprovals) > 0 || - len(userCtx.Pending.MyDraftsInProgress) > 0 { - sb.WriteString("- 用户侧待办线索:\n") - for _, x := range userCtx.Pending.ConfirmModifications { - sb.WriteString(fmt.Sprintf(" - 待确认修改: %s\n", x)) - } - for _, x := range userCtx.Pending.PendingReviews { - sb.WriteString(fmt.Sprintf(" - 待审核: %s\n", x)) - } - for _, x := range userCtx.Pending.PendingApprovals { - sb.WriteString(fmt.Sprintf(" - 待批准: %s\n", x)) - } - for _, x := range userCtx.Pending.MyDraftsInProgress { - sb.WriteString(fmt.Sprintf(" - 编制中: %s\n", x)) - } - } - if userCtx.PendingExpectation != nil && userCtx.PendingExpectation.ContextSnippet != "" { - sb.WriteString(fmt.Sprintf("- 上一轮挂起建议摘要: %s\n", userCtx.PendingExpectation.ContextSnippet)) - } - if len(userCtx.BlackboardFacts) > 0 { - sb.WriteString("- 已沉淀事实:\n") - n := 0 - for k, v := range userCtx.BlackboardFacts { - sb.WriteString(fmt.Sprintf(" - %s = %s\n", k, v)) - n++ - if n >= 12 { - break - } - } - } - if g := strings.TrimSpace(userCtx.PersonalizedGuidance); g != "" { - sb.WriteString("\n") - sb.WriteString(g) - if !strings.HasSuffix(g, "\n") { - sb.WriteString("\n") - } - } - if len(userCtx.RecentDialogue) > 0 { - sb.WriteString("\n【近期对话】\n") - turns := userCtx.RecentDialogue - if len(turns) > dialogueHistoryMax { - turns = turns[len(turns)-dialogueHistoryMax:] - } - for _, t := range turns { - role := t.Role - if role == "" { - role = "user" - } - sb.WriteString(fmt.Sprintf("%s: %s\n", role, t.Content)) - } - } - } - sb.WriteString("\n【本轮用户】\n") - sb.WriteString(userInput) - sb.WriteString("\n\n请结合上下文,以温和助理口吻作答,并写入 reply.md。") - return sb.String() -} - -func sanitizePathSegment(s string) string { - s = strings.TrimSpace(s) - if s == "" { - return "anon" - } - replacer := strings.NewReplacer("/", "_", "\\", "_", "..", "_", " ", "_") - out := replacer.Replace(s) - if len(out) > 80 { - out = out[:80] - } - return out -}