Files
smartmate/backend/newAgent/node/chat.go
Losita 2038185730 Version: 0.9.2.dev.260406
后端:
   1.Chat 四路由升级(二分类 chat/task → 四路由 direct_reply/execute/deep_answer/plan)
     - 新建model/chat_contract.go:路由决策模型,含 NeedsRoughBuild 粗排标记
     - 更新node/chat.go:四路由分流;新增 deep_answer 深度回答路径(二次 LLM 开 thinking)
     - 更新prompt/chat.go:意图分类 prompt 升级为四路由 prompt;新增 deep_answer prompt
   2.粗排节点(RoughBuild)全链路
     - 新建node/rough_build.go:粗排节点,调用注入的算法函数,结果写入 ScheduleState 后进 Execute 微调
     - 更新graph/common_graph.go:注册 RoughBuild 节点;Chat/Confirm 后可路由至粗排
     - 更新model/graph_run_state.go:新增 RoughBuildPlacement/RoughBuildFunc 类型;Deps 注入入口
     - 更新model/plan_contract.go:PlanDecision 新增 NeedsRoughBuild/TaskClassIDs 字段
     - 更新node/plan.go:plan_done 时写入粗排标记和 TaskClassIDs
   3.任务类约束元数据(TaskClassMeta)贯穿 prompt → tools → 持久化
     - 更新tools/state.go:新增 TaskClassMeta;ScheduleState.TaskClasses;ScheduleTask.TaskClassID;Clone 深拷贝
     - 更新conv/schedule_state.go:加载时构建 TaskClassMeta;Diff 支持 HostEventID 嵌入关系
     - 更新conv/schedule_provider.go:新增 LoadTaskClassMetas 按需加载
     - 更新model/state_store.go:ScheduleStateProvider 接口新增 LoadTaskClassMetas
     - 更新prompt/base.go:renderStateSummary 渲染任务类约束
     - 更新prompt/plan.go:注入任务类 ID 上下文和粗排识别规则
     - 更新tools/read_tools.go:GetOverview 展示任务类约束
     - 更新model/common_state.go:CommonState 新增 TaskClassIDs/TaskClasses/NeedsRoughBuild
   4.Execute 健壮性增强(correction 重试 + 纯 ReAct 模式)
     - 更新node/execute.go:未知工具名/空文本走 correction 重试而非 fatal;maxConsecutiveCorrections 提升为包级常量;新增无 plan 纯ReAct 模式;工具结果截断;speak 排除 ask_user/confirm
     - 更新prompt/execute.go:新增 ReAct 模式 system prompt 和 contract
   5.写入持久化完善(task_item source + 嵌入水课)
     - 更新conv/schedule_persist.go:place/move/unplace 支持 task_item source,含嵌入水课和普通 task event 两条路径
     - 新建conv/schedule_preview.go:ScheduleState → 排程预览缓存,复用旧格式,前端无需改动
   6.状态持久化体系(Redis → MySQL outbox 异步)
     - 更新dao/cache.go:Redis 快照 TTL 从 24h 改为 2h,配合 MySQL outbox
     - 新建model/agent_state_snapshot_record.go:快照 MySQL 记录模型
     - 新建service/events/agent_state_persist.go:outbox 异步持久化处理器
     - 更新cmd/start.go + inits/mysql.go:注册快照事件处理器 + AutoMigrate
     - 更新service/agentsvc/agent_newagent.go:注入 RoughBuildFunc;outbox 异步写快照;排程结果写 Redis 预览缓存
   7.基础设施与稳定性
     - 更新stream/sse_adapter.go:outChan 满时静默丢弃,保证持久化不被 SSE 阻断
     - 更新service/agentsvc/agent.go:新增 readAgentExtraIntSlice;outChan 容量 8→256
     - 更新node/agent_nodes.go:Chat 注入工具 schema;Deliver 改 saveAgentState 替代 deleteAgentState
前端:无
仓库:无
2026-04-06 23:15:54 +08:00

378 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package newagentnode
import (
"context"
"fmt"
"log"
"strings"
"time"
"github.com/cloudwego/eino/schema"
newagentllm "github.com/LoveLosita/smartflow/backend/newAgent/llm"
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
newagentprompt "github.com/LoveLosita/smartflow/backend/newAgent/prompt"
newagentstream "github.com/LoveLosita/smartflow/backend/newAgent/stream"
)
const (
chatStageName = "chat"
chatStatusBlockID = "chat.status"
chatSpeakBlockID = "chat.speak"
)
// ChatNodeInput 描述聊天节点单轮运行所需的最小依赖。
//
// 职责边界:
// 1. 只承载"本轮 chat"需要的输入,不负责持久化;
// 2. RuntimeState 提供 pending interaction 与流程状态;
// 3. ConversationContext 提供历史对话;
// 4. ConfirmAction 仅在 confirm 恢复场景下由前端传入 "accept" / "reject"。
type ChatNodeInput struct {
RuntimeState *newagentmodel.AgentRuntimeState
ConversationContext *newagentmodel.ConversationContext
UserInput string
ConfirmAction string
Client *newagentllm.Client
ChunkEmitter *newagentstream.ChunkEmitter
}
// RunChatNode 执行一轮聊天节点逻辑。
//
// 核心职责:
// 1. 恢复判定:有 pending interaction 则处理恢复;
// 2. 路由分流:无 pending 时,调 LLM 判断复杂度并路由;
// 3. direct_reply简单任务直接输出回复 → END
// 4. execute中等任务推 Execute ReAct
// 5. deep_answer复杂问答原地开 thinking 深度回答 → END
// 6. plan复杂规划推 Plan 节点。
func RunChatNode(ctx context.Context, input ChatNodeInput) error {
runtimeState, conversationContext, emitter, err := prepareChatNodeInput(input)
if err != nil {
return err
}
// 1. 有 pending interaction → 纯状态传递,处理恢复。
if runtimeState.HasPendingInteraction() {
return handleChatResume(input, runtimeState, conversationContext, emitter)
}
// 2. 无 pending → 路由决策(一次快速 LLM 调用,不开 thinking
flowState := runtimeState.EnsureCommonState()
messages := newagentprompt.BuildChatRoutingMessages(conversationContext, input.UserInput, flowState)
decision, rawResult, err := newagentllm.GenerateJSON[newagentmodel.ChatRoutingDecision](
ctx,
input.Client,
messages,
newagentllm.GenerateOptions{
Temperature: 0.1,
MaxTokens: 500,
Thinking: newagentllm.ThinkingModeDisabled,
Metadata: map[string]any{
"stage": chatStageName,
"phase": "routing",
},
},
)
rawText := ""
if rawResult != nil {
rawText = strings.TrimSpace(rawResult.Text)
}
if err != nil {
// 路由失败 → 保守:走 plan。
log.Printf("[WARN] chat routing LLM failed chat=%s raw=%s err=%v",
flowState.ConversationID, rawText, err)
flowState.Phase = newagentmodel.PhasePlanning
return nil
}
if validateErr := decision.Validate(); validateErr != nil {
log.Printf("[WARN] chat routing decision invalid chat=%s raw=%s err=%v",
flowState.ConversationID, rawText, validateErr)
flowState.Phase = newagentmodel.PhasePlanning
return nil
}
log.Printf("[DEBUG] chat routing chat=%s route=%s reason=%s",
flowState.ConversationID, decision.Route, decision.Reason)
// 3. 按路由决策推进。
switch decision.Route {
case newagentmodel.ChatRouteDirectReply:
return handleDirectReply(ctx, decision, conversationContext, emitter, flowState)
case newagentmodel.ChatRouteExecute:
return handleRouteExecute(decision, emitter, flowState)
case newagentmodel.ChatRouteDeepAnswer:
return handleDeepAnswer(ctx, input, decision, conversationContext, emitter, flowState)
case newagentmodel.ChatRoutePlan:
return handleRoutePlan(decision, emitter, flowState)
default:
flowState.Phase = newagentmodel.PhasePlanning
return nil
}
}
// handleDirectReply 处理简单任务:直接输出回复。
func handleDirectReply(
ctx context.Context,
decision *newagentmodel.ChatRoutingDecision,
conversationContext *newagentmodel.ConversationContext,
emitter *newagentstream.ChunkEmitter,
flowState *newagentmodel.CommonState,
) error {
if strings.TrimSpace(decision.Speak) != "" {
if err := emitter.EmitPseudoAssistantText(
ctx, chatSpeakBlockID, chatStageName,
decision.Speak,
newagentstream.DefaultPseudoStreamOptions(),
); err != nil {
return fmt.Errorf("闲聊回复推送失败: %w", err)
}
conversationContext.AppendHistory(schema.AssistantMessage(decision.Speak, nil))
}
flowState.Phase = newagentmodel.PhaseChatting
return nil
}
// handleRouteExecute 处理中等任务:推送简短确认,设 PhaseExecuting。
//
// 不把 speak 写入 history因为真正的回复由 Execute 节点产出。
func handleRouteExecute(
decision *newagentmodel.ChatRoutingDecision,
emitter *newagentstream.ChunkEmitter,
flowState *newagentmodel.CommonState,
) error {
speak := strings.TrimSpace(decision.Speak)
if speak == "" {
speak = "好的,我来处理。"
}
// 推送轻量状态通知,让前端知道请求已接收。
_ = emitter.EmitStatus(chatStatusBlockID, chatStageName, "accepted", speak, false)
flowState.Phase = newagentmodel.PhaseExecuting
// 安全兜底:只有真正持有 task_class_ids 时才开粗排。
if decision.NeedsRoughBuild && len(flowState.TaskClassIDs) > 0 {
flowState.NeedsRoughBuild = true
}
return nil
}
// handleDeepAnswer 处理复杂问答:推送过渡语 → 原地开 thinking 再调一次 LLM → 输出深度回答。
func handleDeepAnswer(
ctx context.Context,
input ChatNodeInput,
decision *newagentmodel.ChatRoutingDecision,
conversationContext *newagentmodel.ConversationContext,
emitter *newagentstream.ChunkEmitter,
flowState *newagentmodel.CommonState,
) error {
// 1. 推送过渡语。
briefSpeak := strings.TrimSpace(decision.Speak)
if briefSpeak == "" {
briefSpeak = "让我想想。"
}
if err := emitter.EmitPseudoAssistantText(
ctx, chatSpeakBlockID, chatStageName,
briefSpeak,
newagentstream.DefaultPseudoStreamOptions(),
); err != nil {
return fmt.Errorf("过渡文案推送失败: %w", err)
}
// 2. 第二次 LLM 调用:开 thinking深度回答。
deepMessages := newagentprompt.BuildDeepAnswerMessages(conversationContext, input.UserInput)
deepResult, err := input.Client.GenerateText(ctx, deepMessages, newagentllm.GenerateOptions{
Temperature: 0.5,
MaxTokens: 2000,
Thinking: newagentllm.ThinkingModeEnabled,
Metadata: map[string]any{
"stage": chatStageName,
"phase": "deep_answer",
},
})
if err != nil || deepResult == nil {
// 深度回答失败 → 降级,只保留过渡语。
log.Printf("[WARN] deep answer LLM failed chat=%s err=%v", flowState.ConversationID, err)
conversationContext.AppendHistory(schema.AssistantMessage(briefSpeak, nil))
flowState.Phase = newagentmodel.PhaseChatting
return nil
}
// 3. 输出深度回答。
deepText := strings.TrimSpace(deepResult.Text)
if deepText == "" {
conversationContext.AppendHistory(schema.AssistantMessage(briefSpeak, nil))
flowState.Phase = newagentmodel.PhaseChatting
return nil
}
if err := emitter.EmitPseudoAssistantText(
ctx, chatSpeakBlockID, chatStageName,
deepText,
newagentstream.DefaultPseudoStreamOptions(),
); err != nil {
return fmt.Errorf("深度回答推送失败: %w", err)
}
// 将完整回复(过渡语 + 深度回答)写入 history。
fullReply := briefSpeak + "\n\n" + deepText
conversationContext.AppendHistory(schema.AssistantMessage(fullReply, nil))
flowState.Phase = newagentmodel.PhaseChatting
return nil
}
// handleRoutePlan 处理复杂规划:推送确认语,设 PhasePlanning。
func handleRoutePlan(
decision *newagentmodel.ChatRoutingDecision,
emitter *newagentstream.ChunkEmitter,
flowState *newagentmodel.CommonState,
) error {
speak := strings.TrimSpace(decision.Speak)
if speak == "" {
speak = "好的,让我来规划一下。"
}
_ = emitter.EmitStatus(chatStatusBlockID, chatStageName, "planning", speak, false)
flowState.Phase = newagentmodel.PhasePlanning
return nil
}
// ─── 恢复处理(保持原有逻辑不变)───
// handleChatResume 处理 pending interaction 恢复。
//
// 职责边界:
// 1. 只做状态传递:吞掉用户输入、写回历史、恢复 phase
// 2. 不生成 speak真正的回复由下游 Plan / Execute 节点产出;
// 3. 只推送轻量 status 通知前端"已收到回复,正在继续"。
func handleChatResume(
input ChatNodeInput,
runtimeState *newagentmodel.AgentRuntimeState,
conversationContext *newagentmodel.ConversationContext,
emitter *newagentstream.ChunkEmitter,
) error {
pending := runtimeState.PendingInteraction
flowState := runtimeState.EnsureCommonState()
// 把用户本轮输入写回历史ask_user 回复、confirm 附言等)。
if strings.TrimSpace(input.UserInput) != "" {
conversationContext.AppendHistory(schema.UserMessage(input.UserInput))
}
switch pending.Type {
case newagentmodel.PendingInteractionTypeAskUser:
// 用户回答了问题 → 恢复 phase交给下游节点继续。
runtimeState.ResumeFromPending()
_ = emitter.EmitStatus(
chatStatusBlockID, chatStageName,
"resumed", "收到回复,继续处理。", false,
)
return nil
case newagentmodel.PendingInteractionTypeConfirm:
return handleConfirmResume(input, runtimeState, flowState, pending, emitter)
default:
// connection_lost 等其他类型 → 直接恢复。
runtimeState.ResumeFromPending()
return nil
}
}
// handleConfirmResume 处理 confirm 类型恢复。
//
// 分支逻辑:
// 1. accept → 恢复后 phase 设为 executing下游 Execute 节点接管;
// 2. reject + 有 PendingTool工具确认→ 回到 executing 让 Execute 节点换策略;
// 3. reject + 无 PendingTool计划确认→ 清空计划,回到 planning 重新规划。
func handleConfirmResume(
input ChatNodeInput,
runtimeState *newagentmodel.AgentRuntimeState,
flowState *newagentmodel.CommonState,
pending *newagentmodel.PendingInteraction,
emitter *newagentstream.ChunkEmitter,
) error {
action := strings.ToLower(strings.TrimSpace(input.ConfirmAction))
switch action {
case "accept":
// 恢复前保存待执行工具Execute 节点需要它。
pendingTool := pending.PendingTool
runtimeState.ResumeFromPending()
// 将待执行工具放回临时邮箱,供 Execute 节点执行。
if pendingTool != nil {
copied := *pendingTool
runtimeState.PendingConfirmTool = &copied
}
flowState.Phase = newagentmodel.PhaseExecuting
_ = emitter.EmitStatus(
chatStatusBlockID, chatStageName,
"confirmed", "已确认,开始执行。", false,
)
case "reject":
runtimeState.ResumeFromPending()
if pending.PendingTool != nil {
// 工具确认被拒 → 回到 executing 换策略。
flowState.Phase = newagentmodel.PhaseExecuting
} else {
// 计划确认被拒 → 清空计划,回到 planning。
flowState.RejectPlan()
}
_ = emitter.EmitStatus(
chatStatusBlockID, chatStageName,
"rejected", "已取消,准备重新规划。", false,
)
default:
// 无合法 confirm action → 保守:等同于 reject。
runtimeState.ResumeFromPending()
if pending.PendingTool != nil {
flowState.Phase = newagentmodel.PhaseExecuting
} else {
flowState.RejectPlan()
}
}
return nil
}
// prepareChatNodeInput 校验并准备聊天节点的运行态依赖。
func prepareChatNodeInput(input ChatNodeInput) (
*newagentmodel.AgentRuntimeState,
*newagentmodel.ConversationContext,
*newagentstream.ChunkEmitter,
error,
) {
if input.RuntimeState == nil {
return nil, nil, nil, fmt.Errorf("chat node: runtime state 不能为空")
}
if input.Client == nil {
return nil, nil, nil, fmt.Errorf("chat node: chat client 未注入")
}
input.RuntimeState.EnsureCommonState()
if input.ConversationContext == nil {
input.ConversationContext = newagentmodel.NewConversationContext("")
}
if input.ChunkEmitter == nil {
input.ChunkEmitter = newagentstream.NewChunkEmitter(
newagentstream.NoopPayloadEmitter(), "", "", time.Now().Unix(),
)
}
return input.RuntimeState, input.ConversationContext, input.ChunkEmitter, nil
}