diff --git a/backend/newAgent/model/execute_contract.go b/backend/newAgent/model/execute_contract.go index 283f51a..23b6a4f 100644 --- a/backend/newAgent/model/execute_contract.go +++ b/backend/newAgent/model/execute_contract.go @@ -38,10 +38,11 @@ const ( // 3. Reason 是给后端和日志看的简短解释,不直接等价于完成证明; // 4. ToolCall 只是“意图”,不代表工具已经真正执行成功。 type ExecuteDecision struct { - Speak string `json:"speak,omitempty"` - Action ExecuteAction `json:"action"` - Reason string `json:"reason,omitempty"` - ToolCall *ToolCallIntent `json:"tool_call,omitempty"` + Speak string `json:"speak,omitempty"` + Action ExecuteAction `json:"action"` + Reason string `json:"reason,omitempty"` + GoalCheck string `json:"goal_check,omitempty"` + ToolCall *ToolCallIntent `json:"tool_call,omitempty"` } // Normalize 统一清洗 execute 决策中的字符串字段。 @@ -52,6 +53,7 @@ func (d *ExecuteDecision) Normalize() { d.Speak = strings.TrimSpace(d.Speak) d.Action = ExecuteAction(strings.TrimSpace(string(d.Action))) d.Reason = strings.TrimSpace(d.Reason) + d.GoalCheck = strings.TrimSpace(d.GoalCheck) if d.ToolCall != nil { d.ToolCall.Normalize() } diff --git a/backend/newAgent/model/plan_contract.go b/backend/newAgent/model/plan_contract.go index 275b8e5..e6542ad 100644 --- a/backend/newAgent/model/plan_contract.go +++ b/backend/newAgent/model/plan_contract.go @@ -5,6 +5,20 @@ import ( "strings" ) +// PlanComplexity 表示规划阶段评估的任务复杂度。 +type PlanComplexity string + +const ( + // PlanComplexitySimple 表示简单明确的操作,步骤之间无复杂依赖。 + PlanComplexitySimple PlanComplexity = "simple" + + // PlanComplexityModerate 表示多步操作,需要一定推理但不涉及深度分析。 + PlanComplexityModerate PlanComplexity = "moderate" + + // PlanComplexityComplex 表示需要深度推理、多方案比较或复杂依赖关系的任务。 + PlanComplexityComplex PlanComplexity = "complex" +) + // PlanAction 表示规划阶段单轮决策的动作类型。 // // 设计原则: @@ -32,10 +46,12 @@ const ( // 3. Reason 是给后端和日志看的简短解释; // 4. PlanSteps 只在 plan_done 时要求返回,表示本轮最终确认下来的完整自然语言计划。 type PlanDecision struct { - Speak string `json:"speak,omitempty"` - Action PlanAction `json:"action"` - Reason string `json:"reason,omitempty"` - PlanSteps []PlanStep `json:"plan_steps,omitempty"` + Speak string `json:"speak,omitempty"` + Action PlanAction `json:"action"` + Reason string `json:"reason,omitempty"` + Complexity PlanComplexity `json:"complexity"` + NeedThinking bool `json:"need_thinking"` + PlanSteps []PlanStep `json:"plan_steps,omitempty"` } // Normalize 统一清洗规划决策中的字符串字段。 @@ -46,6 +62,7 @@ func (d *PlanDecision) Normalize() { d.Speak = strings.TrimSpace(d.Speak) d.Action = PlanAction(strings.TrimSpace(string(d.Action))) d.Reason = strings.TrimSpace(d.Reason) + d.Complexity = PlanComplexity(strings.TrimSpace(string(d.Complexity))) for i := range d.PlanSteps { d.PlanSteps[i].Normalize() } @@ -67,6 +84,16 @@ func (d *PlanDecision) Validate() error { return fmt.Errorf("plan decision.action 不能为空") } + // 复杂度兜底:未填写时默认 moderate,不因此拒绝整个决策。 + switch d.Complexity { + case PlanComplexitySimple, PlanComplexityModerate, PlanComplexityComplex: + // ok + case "": + d.Complexity = PlanComplexityModerate + default: + return fmt.Errorf("未知 complexity: %s", d.Complexity) + } + switch d.Action { case PlanActionContinue, PlanActionAskUser: if len(d.PlanSteps) > 0 { diff --git a/backend/newAgent/node/execute.go b/backend/newAgent/node/execute.go index 41234bd..328c448 100644 --- a/backend/newAgent/node/execute.go +++ b/backend/newAgent/node/execute.go @@ -131,6 +131,20 @@ func RunExecuteNode(ctx context.Context, input ExecuteNodeInput) error { return fmt.Errorf("执行决策不合法: %w", err) } + // 自省校验:next_plan / done 必须附带 goal_check,否则不推进,追加修正让 LLM 重试。 + if decision.Action == newagentmodel.ExecuteActionNextPlan || + decision.Action == newagentmodel.ExecuteActionDone { + if strings.TrimSpace(decision.GoalCheck) == "" { + AppendLLMCorrectionWithHint( + conversationContext, + decision.Speak, + fmt.Sprintf("你输出了 action=%s,但 goal_check 为空。", decision.Action), + fmt.Sprintf("输出 %s 时,必须在 goal_check 中对照 done_when 逐条说明完成依据。", decision.Action), + ) + return nil + } + } + // 6. 若 LLM 先对用户说话,则伪流式推送并写回历史。 if strings.TrimSpace(decision.Speak) != "" { if err := emitter.EmitPseudoAssistantText( diff --git a/backend/newAgent/node/plan.go b/backend/newAgent/node/plan.go index aadf5db..2e70b32 100644 --- a/backend/newAgent/node/plan.go +++ b/backend/newAgent/node/plan.go @@ -38,13 +38,14 @@ type PlanNodeInput struct { // RunPlanNode 执行一轮规划节点逻辑。 // // 步骤说明: -// 1. 先校验最小依赖,并推送一条“正在规划”的状态,避免用户空等; -// 2. 再用 prompt/plan.go 组装 messages,请模型严格输出 PlanDecision JSON; -// 3. 若模型先对用户说了话,则先把 speak 伪流式推给前端,并写回 history; -// 4. 最后按 action 推进流程: -// 4.1 continue:继续停留在 planning; -// 4.2 ask_user:打开 pending interaction,后续交给 interrupt 收口; -// 4.3 plan_done:固化完整计划,刷新 pinned context,并进入 waiting_confirm。 +// 1. 先校验最小依赖,并推送一条”正在规划”的状态,避免用户空等; +// 2. Phase 1(快速评估):不开 thinking,让 LLM 同时产出复杂度评估和规划结果; +// 3. Phase 2(深度规划):若 LLM 自评需要深度思考且规划已完成,开 thinking 重跑; +// 4. 若模型先对用户说了话,则先把 speak 伪流式推给前端,并写回 history; +// 5. 最后按 action 推进流程: +// 5.1 continue:继续停留在 planning; +// 5.2 ask_user:打开 pending interaction,后续交给 interrupt 收口; +// 5.3 plan_done:固化完整计划,刷新 pinned context,并进入 waiting_confirm。 func RunPlanNode(ctx context.Context, input PlanNodeInput) error { runtimeState, conversationContext, emitter, err := preparePlanNodeInput(input) if err != nil { @@ -63,8 +64,10 @@ func RunPlanNode(ctx context.Context, input PlanNodeInput) error { return fmt.Errorf("规划阶段状态推送失败: %w", err) } - // 2. 构造本轮规划输入,并要求模型输出结构化 PlanDecision。 + // 2. 构造本轮规划输入。 messages := newagentprompt.BuildPlanMessages(flowState, conversationContext, input.UserInput) + + // 3. Phase 1:快速评估(不开 thinking),让 LLM 同时产出复杂度评估和规划结果。 decision, rawResult, err := newagentllm.GenerateJSON[newagentmodel.PlanDecision]( ctx, input.Client, @@ -72,23 +75,60 @@ func RunPlanNode(ctx context.Context, input PlanNodeInput) error { newagentllm.GenerateOptions{ Temperature: 0.2, MaxTokens: 1600, - Thinking: newagentllm.ThinkingModeEnabled, + Thinking: newagentllm.ThinkingModeDisabled, Metadata: map[string]any{ "stage": planStageName, + "phase": "assessment", }, }, ) if err != nil { if rawResult != nil && strings.TrimSpace(rawResult.Text) != "" { - return fmt.Errorf("规划输出解析失败,原始输出=%s,错误=%w", strings.TrimSpace(rawResult.Text), err) + return fmt.Errorf("规划评估解析失败,原始输出=%s,错误=%w", strings.TrimSpace(rawResult.Text), err) } - return fmt.Errorf("规划阶段模型调用失败: %w", err) + return fmt.Errorf("规划评估阶段模型调用失败: %w", err) } if err := decision.Validate(); err != nil { - return fmt.Errorf("规划决策不合法: %w", err) + return fmt.Errorf("规划评估决策不合法: %w", err) } - // 3. 若模型先对用户说了话,则先以伪流式推送,再写回 history,保证上下文连续。 + // 4. Phase 2:若 LLM 自评需要深度思考且本轮规划已完成,则开启 thinking 重跑。 + // 条件:NeedThinking=true + Action=plan_done → 说明 LLM 认为当前无 thinking 的计划质量不够。 + // 其他 action(continue / ask_user)不需要 thinking,直接用 Phase 1 结果。 + if decision.NeedThinking && decision.Action == newagentmodel.PlanActionDone { + if err := emitter.EmitStatus( + planStatusBlockID, + planStageName, + "deep_planning", + "正在深入思考,生成更完善的计划。", + false, + ); err != nil { + return fmt.Errorf("深度规划状态推送失败: %w", err) + } + + deepDecision, _, deepErr := newagentllm.GenerateJSON[newagentmodel.PlanDecision]( + ctx, + input.Client, + messages, + newagentllm.GenerateOptions{ + Temperature: 0.2, + MaxTokens: 3200, + Thinking: newagentllm.ThinkingModeEnabled, + Metadata: map[string]any{ + "stage": planStageName, + "phase": "deep_planning", + }, + }, + ) + if deepErr == nil && deepDecision != nil { + if validateErr := deepDecision.Validate(); validateErr == nil { + decision = deepDecision + } + } + // 深度规划失败时静默降级到 Phase 1 结果,不中断流程。 + } + + // 5. 若模型先对用户说了话,则先以伪流式推送,再写回 history,保证上下文连续。 if strings.TrimSpace(decision.Speak) != "" { if err := emitter.EmitPseudoAssistantText( ctx, @@ -102,7 +142,7 @@ func RunPlanNode(ctx context.Context, input PlanNodeInput) error { conversationContext.AppendHistory(schema.AssistantMessage(decision.Speak, nil)) } - // 4. 按规划动作推进流程状态。 + // 6. 按规划动作推进流程状态。 switch decision.Action { case newagentmodel.PlanActionContinue: flowState.Phase = newagentmodel.PhasePlanning diff --git a/backend/newAgent/prompt/execute.go b/backend/newAgent/prompt/execute.go index 8748ae3..eda6a17 100644 --- a/backend/newAgent/prompt/execute.go +++ b/backend/newAgent/prompt/execute.go @@ -24,10 +24,11 @@ const executeSystemPrompt = ` 请遵守以下规则: 1. 只围绕当前步骤行动,不要擅自跳到其他 plan 步骤。 -2. 只有当你确认当前步骤已经完成时,才输出 ` + "`" + `[NEXT_PLAN]` + "`" + `。 -3. 只有当你确认整个任务已经完成时,才输出 ` + "`" + `[DONE]` + "`" + `。 +2. 只有当你确认当前步骤已经完成时,才输出 ` + "`" + `[NEXT_PLAN]` + "`" + `,且必须在 goal_check 中逐条对照 done_when 说明完成依据。 +3. 只有当你确认整个任务已经完成时,才输出 ` + "`" + `[DONE]` + "`" + `,且必须在 goal_check 中总结整体完成证据。 4. 如果执行当前步骤缺少关键上下文,且无法通过已有历史或工具补齐,可以输出 ` + "`" + `[ASK_USER]` + "`" + `。 5. 不要伪造工具结果;如果尚未真正拿到观察结果,就不要假装已经完成。 +6. goal_check 是你输出 next_plan / done 时的强制字段,禁止为空;必须显式地逐条对照 done_when,说明"哪些条件已满足、依据是什么"。 你会看到: - 当前完整 plan @@ -72,13 +73,14 @@ func BuildExecuteUserPrompt(state *newagentmodel.CommonState) string { sb.WriteString("2. 若当前步骤未完成,请继续思考-执行-观察循环。\n") sb.WriteString("3. 若当前步骤已完成,请输出 ") sb.WriteString(ExecuteNextPlanSignal) - sb.WriteString("。\n") + sb.WriteString(",并填写 goal_check 说明完成依据。\n") sb.WriteString("4. 若整个任务已完成,请输出 ") sb.WriteString(ExecuteDoneSignal) - sb.WriteString("。\n") + sb.WriteString(",并填写 goal_check 总结整体证据。\n") sb.WriteString("5. 若缺少关键用户信息且现有上下文无法补足,请输出 ") sb.WriteString(ExecuteAskUserSignal) sb.WriteString("。\n") + sb.WriteString("6. 输出 next_plan 或 done 时,goal_check 不能为空,必须对照 done_when 逐条验证。\n") sb.WriteString("\n当前步骤正文:\n") sb.WriteString(strings.TrimSpace(currentStep.Content)) sb.WriteString("\n") diff --git a/backend/newAgent/prompt/plan.go b/backend/newAgent/prompt/plan.go index 65ad3e9..0f20f00 100644 --- a/backend/newAgent/prompt/plan.go +++ b/backend/newAgent/prompt/plan.go @@ -19,6 +19,8 @@ const planSystemPrompt = ` 4. 若你认为计划已经完整可执行,请返回 action=plan_done,并附带完整 plan_steps。 5. plan_steps 必须使用自然语言,便于后端将完整 plan 重新注入到后续上下文顶部。 6. 只输出 JSON,不要输出 markdown,不要输出额外解释,不要在 JSON 外再补文字。 +7. 每次输出前先评估任务复杂度:simple(简单明确,无复杂依赖)、moderate(多步操作,需要一定推理)、complex(需要深度推理、多方案比较或复杂依赖关系)。 +8. 根据复杂度判断 need_thinking:你是否需要深度思考才能生成高质量计划?当不确定时倾向于 false。 你会看到: - 当前阶段与轮次信息 @@ -78,35 +80,43 @@ func BuildPlanDecisionContractText() string { - speak:给用户看的话;若 action=%s,这里通常就是要追问用户的问题 - action:只能是 %s / %s / %s - reason:给后端和日志看的简短说明 +- complexity:任务复杂度,只能是 simple / moderate / complex +- need_thinking:是否需要深度思考才能生成高质量计划,只能是 true / false - plan_steps:仅当 action=%s 时允许返回;返回时必须是完整计划,不是增量 - plan_steps[].content:步骤正文,必填 -- plan_steps[].done_when:可选,建议写“什么情况下算这一步做完” +- plan_steps[].done_when:可选,建议写”什么情况下算这一步做完” 合法示例: { - "speak": "我先把计划再收束一下。", - "action": "%s", - "reason": "当前信息已足够继续规划" + “speak”: “我先把计划再收束一下。”, + “action”: “%s”, + “reason”: “当前信息已足够继续规划”, + “complexity”: “moderate”, + “need_thinking”: false } { - "speak": "你更希望我优先安排今天,还是按整周来规划?", - "action": "%s", - "reason": "当前时间范围仍不明确" + “speak”: “你更希望我优先安排今天,还是按整周来规划?”, + “action”: “%s”, + “reason”: “当前时间范围仍不明确”, + “complexity”: “simple”, + “need_thinking”: false } { - "speak": "计划已经整理好了,我先给你确认一下。", - "action": "%s", - "reason": "当前计划已具备执行条件", - "plan_steps": [ + “speak”: “计划已经整理好了,我先给你确认一下。”, + “action”: “%s”, + “reason”: “当前计划已具备执行条件”, + “complexity”: “simple”, + “need_thinking”: false, + “plan_steps”: [ { - "content": "先确认本周可用时间范围", - "done_when": "拿到明确的可用时间段列表" + “content”: “先确认本周可用时间范围”, + “done_when”: “拿到明确的可用时间段列表” }, { - "content": "基于可用时间生成执行安排", - "done_when": "得到一份用户可确认的安排方案" + “content”: “基于可用时间生成执行安排”, + “done_when”: “得到一份用户可确认的安排方案” } ] }