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