Version: 0.8.6.dev.260401

后端:
新建了chat和execute节点,完善了相关逻辑,代码尚未review
前端:
无
仓库:
无
This commit is contained in:
LoveLosita
2026-04-01 21:35:41 +08:00
parent e1a06be768
commit 2c64b37d00
8 changed files with 817 additions and 50 deletions

View File

@@ -35,7 +35,7 @@ func RunAgentGraph(ctx context.Context, input newagentmodel.AgentGraphRunInput)
g := compose.NewGraph[*newagentmodel.AgentGraphState, *newagentmodel.AgentGraphState]()
// --- 注册节点 ---
if err := g.AddLambdaNode(NodeChat, compose.InvokableLambda(chatNode)); err != nil {
if err := g.AddLambdaNode(NodeChat, compose.InvokableLambda(nodes.Chat)); err != nil {
return nil, err
}
if err := g.AddLambdaNode(NodePlan, compose.InvokableLambda(nodes.Plan)); err != nil {
@@ -44,7 +44,7 @@ func RunAgentGraph(ctx context.Context, input newagentmodel.AgentGraphRunInput)
if err := g.AddLambdaNode(NodeConfirm, compose.InvokableLambda(confirmNode)); err != nil {
return nil, err
}
if err := g.AddLambdaNode(NodeExecute, compose.InvokableLambda(executeNode)); err != nil {
if err := g.AddLambdaNode(NodeExecute, compose.InvokableLambda(nodes.Execute)); err != nil {
return nil, err
}
if err := g.AddLambdaNode(NodeInterrupt, compose.InvokableLambda(interruptNode)); err != nil {
@@ -56,11 +56,11 @@ func RunAgentGraph(ctx context.Context, input newagentmodel.AgentGraphRunInput)
// --- 连边 ---
// 1. 所有请求统一先过 chat 入口,这样普通聊天、首次任务、恢复执行都走同一入口。
// 2. chat 不再负责旧式多业务图路由,只负责决定后续应该进入哪个统一节点。
// 2. chat 不再负责旧式多业务图路由,只负责决定后续应该进入哪个统一节点。
if err := g.AddEdge(compose.START, NodeChat); err != nil {
return nil, err
}
// Chat END(普通聊天) / Plan / Confirm / Execute / Deliver / Interrupt
// Chat -> END(普通聊天) / Plan / Confirm / Execute / Deliver / Interrupt
if err := g.AddBranch(NodeChat, compose.NewGraphBranch(
branchAfterChat,
map[string]bool{
@@ -74,7 +74,7 @@ func RunAgentGraph(ctx context.Context, input newagentmodel.AgentGraphRunInput)
)); err != nil {
return nil, err
}
// Plan Plan(继续规划) / Confirm(规划完成) / Interrupt(需要追问用户)
// Plan -> Plan(继续规划) / Confirm(规划完成) / Interrupt(需要追问用户)
if err := g.AddBranch(NodePlan, compose.NewGraphBranch(
branchAfterPlan,
map[string]bool{
@@ -85,7 +85,7 @@ func RunAgentGraph(ctx context.Context, input newagentmodel.AgentGraphRunInput)
)); err != nil {
return nil, err
}
// Confirm Plan(用户拒绝或重规划) / Execute(确认后继续执行) / Interrupt(产出确认中断并等待外部回调)
// Confirm -> Plan(用户拒绝或重规划) / Execute(确认后继续执行) / Interrupt(产出确认中断并等待外部回调)
if err := g.AddBranch(NodeConfirm, compose.NewGraphBranch(
branchAfterConfirm,
map[string]bool{
@@ -96,7 +96,7 @@ func RunAgentGraph(ctx context.Context, input newagentmodel.AgentGraphRunInput)
)); err != nil {
return nil, err
}
// Execute Execute(继续 ReAct) / Confirm(写操作待确认) / Deliver(完成) / Interrupt(需要追问用户)
// Execute -> Execute(继续 ReAct) / Confirm(写操作待确认) / Deliver(完成) / Interrupt(需要追问用户)
if err := g.AddBranch(NodeExecute, compose.NewGraphBranch(
branchAfterExecute,
map[string]bool{
@@ -108,11 +108,11 @@ func RunAgentGraph(ctx context.Context, input newagentmodel.AgentGraphRunInput)
)); err != nil {
return nil, err
}
// Interrupt END当前连接必须在这里收口等待用户输入或确认回调恢复。
// Interrupt -> END当前连接必须在这里收口等待用户输入或确认回调恢复。
if err := g.AddEdge(NodeInterrupt, compose.END); err != nil {
return nil, err
}
// Deliver END
// Deliver -> END
if err := g.AddEdge(NodeDeliver, compose.END); err != nil {
return nil, err
}
@@ -132,23 +132,6 @@ func RunAgentGraph(ctx context.Context, input newagentmodel.AgentGraphRunInput)
// --- 占位节点,后续逐步由 node 层替换 ---
func chatNode(_ context.Context, st *newagentmodel.AgentGraphState) (*newagentmodel.AgentGraphState, error) {
if st == nil {
return nil, errors.New("chat node: state is nil")
}
st.EnsureFlowState()
st.EnsureConversationContext()
st.EnsureChunkEmitter()
// TODO:
// 1. 识别当前请求是普通聊天、首次任务进入,还是从 pending interaction 恢复。
// 2. 若只是普通聊天,则生成回复并把 Phase 设为 PhaseChatting后续直接 END。
// 3. 若识别到任务意图,则把 Phase 切到 planning / waiting_confirm / executing 对应阶段。
// 4. 若本轮是恢复请求,则这里只负责吞掉用户最新输入并准备恢复,不再重复输出闲聊回复。
// 5. 后续 chatNode 可直接读取 st.Request.UserInput、st.ConversationContext 与 st.Deps.ResolveChatClient()。
return st, nil
}
func confirmNode(_ context.Context, st *newagentmodel.AgentGraphState) (*newagentmodel.AgentGraphState, error) {
if st == nil {
return nil, errors.New("confirm node: state is nil")
@@ -158,34 +141,13 @@ func confirmNode(_ context.Context, st *newagentmodel.AgentGraphState) (*newagen
st.EnsureChunkEmitter()
// TODO:
// 1. 这里不再做confirm 节点内自循环等待,而是统一走中断恢复模式。
// 1. 这里不再做 confirm 节点内自循环等待,而是统一走中断恢复模式。
// 2. 节点职责是生成确认事件、固化待执行工具快照,并调用 st.OpenConfirmInteraction(...)。
// 3. 当前连接随后会流向 interrupt 节点收口;用户确认/取消后,由外部回调恢复到 executing 或 planning。
// 4. 这里不要直接执行写工具,必须先把待执行工具调用固化为 pending snapshot。
return st, nil
}
func executeNode(_ context.Context, st *newagentmodel.AgentGraphState) (*newagentmodel.AgentGraphState, error) {
if st == nil {
return nil, errors.New("execute node: state is nil")
}
flowState := st.EnsureFlowState()
st.EnsureConversationContext()
st.EnsureChunkEmitter()
// TODO:
// 1. 让 LLM 在“当前步骤”约束下做一轮 ReAct思考 → 调工具/观察 → reflection。
// 1.1 执行阶段所需上下文应直接从 st.ConversationContext 读取。
// 1.2 执行阶段模型依赖应通过 st.Deps.ResolveExecuteClient() 获取。
// 2. 若执行中发现缺少关键用户信息,则调用 st.OpenAskUserInteraction(...) 并走 interrupt。
// 3. 若命中写工具确认闸门:
// 3.1 若走同连接确认,则把 Phase 置为 waiting_confirm 并跳到 confirm
// 3.2 若走短连接恢复,则调用 st.OpenConfirmInteraction(...) 并走 interrupt。
// 4. 若当前步骤已完成,则由 node 层决定是 AdvanceStep() 继续,还是 Done() 进入交付。
flowState.NextRound()
return st, nil
}
func interruptNode(_ context.Context, st *newagentmodel.AgentGraphState) (*newagentmodel.AgentGraphState, error) {
if st == nil {
return nil, errors.New("interrupt node: state is nil")

View File

@@ -14,7 +14,8 @@ import (
// 2. 不负责承载可持久化流程状态,流程状态仍归 AgentRuntimeState
// 3. 不负责承载 LLM / emitter / store 等依赖,这些统一放进 AgentGraphDeps。
type AgentGraphRequest struct {
UserInput string
UserInput string
ConfirmAction string // "accept" / "reject" / "",仅 confirm 恢复场景由前端传入
}
// Normalize 统一清洗请求级输入中的字符串字段。
@@ -23,6 +24,7 @@ func (r *AgentGraphRequest) Normalize() {
return
}
r.UserInput = strings.TrimSpace(r.UserInput)
r.ConfirmAction = strings.TrimSpace(r.ConfirmAction)
}
// AgentGraphDeps 描述 graph/node 层运行时真正依赖的可插拔能力。

View File

@@ -20,6 +20,33 @@ func NewAgentNodes() *AgentNodes {
return &AgentNodes{}
}
// Chat 是聊天入口的正式节点方法。
//
// 职责边界:
// 1. 这里只做 graph -> node 的参数转接;
// 2. 真正的入口逻辑仍由 RunChatNode 负责;
// 3. 这样 graph 层后续只需挂 n.Chat而不再自己维护占位 chatNode。
func (n *AgentNodes) Chat(ctx context.Context, st *newagentmodel.AgentGraphState) (*newagentmodel.AgentGraphState, error) {
if st == nil {
return nil, errors.New("chat node: state is nil")
}
if err := RunChatNode(
ctx,
ChatNodeInput{
RuntimeState: st.EnsureRuntimeState(),
ConversationContext: st.EnsureConversationContext(),
UserInput: st.Request.UserInput,
ConfirmAction: st.Request.ConfirmAction,
Client: st.Deps.ResolveChatClient(),
ChunkEmitter: st.EnsureChunkEmitter(),
},
); err != nil {
return nil, err
}
return st, nil
}
// Plan 是规划阶段的正式节点方法。
//
// 职责边界:
@@ -46,3 +73,35 @@ func (n *AgentNodes) Plan(ctx context.Context, st *newagentmodel.AgentGraphState
}
return st, nil
}
// Execute 是执行阶段的正式节点方法。
//
// 职责边界:
// 1. 这里只做 graph -> node 的参数转接;
// 2. 真正的单轮执行逻辑仍由 RunExecuteNode 负责;
// 3. 这样 graph 层后续只需挂 n.Execute而不再自己维护占位 executeNode。
//
// 设计原则:
// 1. LLM 主导LLM 自己判断 done_when 是否满足,自己决定何时推进/完成;
// 2. 后端兜底:只做资源控制、安全兜底、证据记录;
// 3. 不做硬校验:后端不质疑 LLM 的 advance/complete 决策。
func (n *AgentNodes) Execute(ctx context.Context, st *newagentmodel.AgentGraphState) (*newagentmodel.AgentGraphState, error) {
if st == nil {
return nil, errors.New("execute node: state is nil")
}
if err := RunExecuteNode(
ctx,
ExecuteNodeInput{
RuntimeState: st.EnsureRuntimeState(),
ConversationContext: st.EnsureConversationContext(),
UserInput: st.Request.UserInput,
Client: st.Deps.ResolveExecuteClient(),
ChunkEmitter: st.EnsureChunkEmitter(),
ResumeNode: "execute",
},
); err != nil {
return nil, err
}
return st, nil
}

View File

@@ -0,0 +1,261 @@
package newagentnode
import (
"context"
"fmt"
"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
}
// chatIntentDecision 是意图分类的结构化输出。
type chatIntentDecision struct {
Intent string `json:"intent"`
Reply string `json:"reply,omitempty"`
Reason string `json:"reason,omitempty"`
}
// Normalize 清洗意图分类结果中的字符串字段。
func (d *chatIntentDecision) Normalize() {
if d == nil {
return
}
d.Intent = strings.TrimSpace(d.Intent)
d.Reply = strings.TrimSpace(d.Reply)
d.Reason = strings.TrimSpace(d.Reason)
}
// Validate 校验意图分类结果的最小合法性。
func (d *chatIntentDecision) Validate() error {
if d == nil {
return fmt.Errorf("chat intent decision 不能为空")
}
d.Normalize()
switch d.Intent {
case "chat", "task":
return nil
default:
return fmt.Errorf("未知 intent: %s", d.Intent)
}
}
// RunChatNode 执行一轮聊天节点逻辑。
//
// 核心职责:
// 1. 恢复判定:有 pending interaction 则处理恢复,不生成 speak
// 2. 意图分流:无 pending 时,调 LLM 分类 chat / task
// 3. 闲聊回复:纯 chat 场景直接生成回复并流式推送phase → chatting → END
// 4. 任务路由task 场景 phase → planning交给后续 Plan 节点处理。
//
// 保守原则:分类失败或意图不明时,一律走 task不丢失用户意图。
func RunChatNode(ctx context.Context, input ChatNodeInput) error {
runtimeState, conversationContext, emitter, err := prepareChatNodeInput(input)
if err != nil {
return err
}
// 1. 有 pending interaction → 纯状态传递,不生成 speak。
if runtimeState.HasPendingInteraction() {
return handleChatResume(input, runtimeState, conversationContext, emitter)
}
// 2. 无 pending → 调 LLM 做意图分类。
messages := newagentprompt.BuildChatIntentMessages(conversationContext, input.UserInput)
decision, _, err := newagentllm.GenerateJSON[chatIntentDecision](
ctx,
input.Client,
messages,
newagentllm.GenerateOptions{
Temperature: 0.1,
MaxTokens: 300,
Thinking: newagentllm.ThinkingModeDisabled,
},
)
if err != nil || decision.Validate() != nil {
// 分类失败 → 保守:走 task。
runtimeState.EnsureCommonState().Phase = newagentmodel.PhasePlanning
return nil
}
// 3. 按意图分流。
flowState := runtimeState.EnsureCommonState()
switch decision.Intent {
case "task":
flowState.Phase = newagentmodel.PhasePlanning
return nil
case "chat":
return handleChatReply(ctx, decision, conversationContext, emitter, flowState)
default:
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":
runtimeState.ResumeFromPending()
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
}
// handleChatReply 处理纯闲聊意图 — 把分类时产出的 reply 流式推给前端。
func handleChatReply(
ctx context.Context,
decision *chatIntentDecision,
conversationContext *newagentmodel.ConversationContext,
emitter *newagentstream.ChunkEmitter,
flowState *newagentmodel.CommonState,
) error {
reply := strings.TrimSpace(decision.Reply)
if reply != "" {
if err := emitter.EmitPseudoAssistantText(
ctx, chatSpeakBlockID, chatStageName,
reply,
newagentstream.DefaultPseudoStreamOptions(),
); err != nil {
return fmt.Errorf("闲聊回复推送失败: %w", err)
}
conversationContext.AppendHistory(schema.AssistantMessage(reply, nil))
}
flowState.Phase = newagentmodel.PhaseChatting
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
}

View File

@@ -0,0 +1,100 @@
package newagentnode
import (
"fmt"
"strings"
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
"github.com/cloudwego/eino/schema"
)
// AppendLLMCorrection 追加 LLM 修正提示到对话历史。
//
// 设计目的:
// 1. 当 LLM 输出不符合预期(如不支持的 action、格式错误等不应直接报错终止
// 2. 应该给 LLM 一个自我修正的机会,把错误反馈写回历史,让它重新生成;
// 3. 该函数封装了"追加 assistant 消息 + 追加纠正提示"的通用流程。
//
// 参数说明:
// - conversationContext: 对话上下文,用于追加历史消息;
// - llmOutput: LLM 的原始输出内容,会作为 assistant 消息追加;
// - validOptionsDesc: 合法选项的描述,用于构造纠正提示。
//
// 使用示例:
//
// AppendLLMCorrection(conversationContext, decision.Speak, "合法的 action 包括continue、ask_user、next_plan、done")
//
// 返回值:
// - 返回 nil 表示修正流程完成,调用方应继续 Graph 循环;
// - 该函数不会返回 error因为追加历史失败不影响主流程。
func AppendLLMCorrection(
conversationContext *newagentmodel.ConversationContext,
llmOutput string,
validOptionsDesc string,
) {
if conversationContext == nil {
return
}
// 1. 构造 assistant 消息,让 LLM 知道自己刚才输出了什么。
// 如果 llmOutput 为空,则生成一个占位描述。
assistantContent := strings.TrimSpace(llmOutput)
if assistantContent == "" {
assistantContent = "[LLM 输出为空或无法解析]"
}
conversationContext.AppendHistory(&schema.Message{
Role: schema.Assistant,
Content: assistantContent,
})
// 2. 构造纠正提示,明确告知 LLM 哪里错了、合法选项有哪些。
// 不做硬编码的错误类型,由调用方通过 validOptionsDesc 传入。
correctionContent := fmt.Sprintf(
"你的输出不符合预期。%s 请重新分析当前状态,输出正确的内容。",
validOptionsDesc,
)
conversationContext.AppendHistory(&schema.Message{
Role: schema.User,
Content: correctionContent,
})
}
// AppendLLMCorrectionWithHint 追加 LLM 修正提示(带自定义错误描述)。
//
// 相比 AppendLLMCorrection该函数允许调用方提供更详细的错误描述
// 适用于需要明确告知 LLM 具体哪里出错的场景。
//
// 参数说明:
// - conversationContext: 对话上下文;
// - llmOutput: LLM 的原始输出内容;
// - errorDesc: 具体的错误描述,如 "action \"invalid\" 不是合法的执行动作"
// - validOptionsDesc: 合法选项的描述。
func AppendLLMCorrectionWithHint(
conversationContext *newagentmodel.ConversationContext,
llmOutput string,
errorDesc string,
validOptionsDesc string,
) {
if conversationContext == nil {
return
}
assistantContent := strings.TrimSpace(llmOutput)
if assistantContent == "" {
assistantContent = "[LLM 输出为空或无法解析]"
}
conversationContext.AppendHistory(&schema.Message{
Role: schema.Assistant,
Content: assistantContent,
})
correctionContent := fmt.Sprintf(
"%s %s 请重新分析当前状态,输出正确的内容。",
errorDesc,
validOptionsDesc,
)
conversationContext.AppendHistory(&schema.Message{
Role: schema.User,
Content: correctionContent,
})
}

View File

@@ -0,0 +1,314 @@
package newagentnode
import (
"context"
"fmt"
"strings"
"time"
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"
"github.com/google/uuid"
)
const (
executeStageName = "execute"
executeStatusBlockID = "execute.status"
executeSpeakBlockID = "execute.speak"
executePinnedKey = "execution_context"
)
// ExecuteNodeInput 描述执行节点单轮运行所需的最小依赖。
//
// 职责边界:
// 1. 只承载"本轮执行"需要的输入,不负责持久化;
// 2. RuntimeState 提供 plan 步骤与轮次预算;
// 3. ConversationContext 提供历史对话与置顶上下文;
// 4. ToolExecutor 后续由业务层注入,当前先留空。
type ExecuteNodeInput struct {
RuntimeState *newagentmodel.AgentRuntimeState
ConversationContext *newagentmodel.ConversationContext
UserInput string
Client *newagentllm.Client
ChunkEmitter *newagentstream.ChunkEmitter
ResumeNode string
}
// ExecuteRoundObservation 记录执行阶段每轮的关键观察。
//
// 设计说明:
// 1. 参考 coding agent 模式,后端只记录事实,不做语义校验;
// 2. ToolResult 存储工具调用的原始返回,供 LLM 下一轮决策;
// 3. 该结构后续可扩展用于调试、回放、审计。
type ExecuteRoundObservation struct {
Round int `json:"round"`
StepIndex int `json:"step_index"`
GoalCheck string `json:"goal_check,omitempty"`
Decision string `json:"decision,omitempty"`
ToolName string `json:"tool_name,omitempty"`
ToolParams string `json:"tool_params,omitempty"`
ToolSuccess bool `json:"tool_success"`
ToolResult string `json:"tool_result,omitempty"`
}
// RunExecuteNode 执行一轮执行节点逻辑。
//
// 核心设计原则:
// 1. LLM 主导LLM 自己判断 done_when 是否满足,自己决定何时推进/完成;
// 2. 后端兜底:只做资源控制(轮次预算)、安全兜底(防无限循环)、证据记录;
// 3. 不做硬校验:后端不质疑 LLM 的 advance/complete 决策,信任 LLM 判断。
//
// 步骤说明:
// 1. 校验最小依赖,推送"正在执行"状态,避免用户空等;
// 2. 检查当前是否有可执行的 plan 步骤,无计划则报错;
// 3. 构造执行阶段 prompt调用 LLM 获取决策;
// 4. 若 LLM 先对用户说话,则伪流式推送并写回历史;
// 5. 按 LLM 决策执行动作:
// 5.1 call_tool执行工具调用记录证据推进轮次
// 5.2 ask_user打开追问交互等待用户回复
// 5.3 advanceLLM 判定当前步骤完成,推进到下一步;
// 5.4 completeLLM 判定整个任务完成,进入交付阶段;
// 6. 安全兜底:轮次耗尽时强制进入交付,避免无限循环。
func RunExecuteNode(ctx context.Context, input ExecuteNodeInput) error {
// 1. 校验依赖并准备运行态。
runtimeState, conversationContext, emitter, err := prepareExecuteNodeInput(input)
if err != nil {
return err
}
flowState := runtimeState.EnsureCommonState()
// 2. 检查是否有可执行的 plan 步骤。
if !flowState.HasCurrentPlanStep() {
return fmt.Errorf("execute node: 当前无有效 plan 步骤,无法执行")
}
// 3. 推送执行阶段状态,让前端知道当前进度。
current, total := flowState.PlanProgress()
currentStep, _ := flowState.CurrentPlanStep()
if err := emitter.EmitStatus(
executeStatusBlockID,
executeStageName,
"executing",
fmt.Sprintf("正在执行第 %d/%d 步:%s", current, total, truncateText(currentStep.Content, 60)),
false,
); err != nil {
return fmt.Errorf("执行阶段状态推送失败: %w", err)
}
// 4. 消耗一轮预算,并检查是否耗尽。
if !flowState.NextRound() {
// 轮次耗尽,强制进入交付阶段。
flowState.Done()
return nil
}
// 5. 构造本轮执行输入,请求 LLM 输出 ExecuteDecision。
messages := newagentprompt.BuildExecuteMessages(flowState, conversationContext)
decision, rawResult, err := newagentllm.GenerateJSON[newagentmodel.ExecuteDecision](
ctx,
input.Client,
messages,
newagentllm.GenerateOptions{
Temperature: 0.3,
MaxTokens: 1200,
Thinking: newagentllm.ThinkingModeEnabled,
Metadata: map[string]any{
"stage": executeStageName,
"step_index": flowState.CurrentStep,
"round_used": flowState.RoundUsed,
},
},
)
if err != nil {
if rawResult != nil && strings.TrimSpace(rawResult.Text) != "" {
return fmt.Errorf("执行决策解析失败,原始输出=%s错误=%w", strings.TrimSpace(rawResult.Text), err)
}
return fmt.Errorf("执行阶段模型调用失败: %w", err)
}
if err := decision.Validate(); err != nil {
return fmt.Errorf("执行决策不合法: %w", err)
}
// 6. 若 LLM 先对用户说话,则伪流式推送并写回历史。
if strings.TrimSpace(decision.Speak) != "" {
if err := emitter.EmitPseudoAssistantText(
ctx,
executeSpeakBlockID,
executeStageName,
decision.Speak,
newagentstream.DefaultPseudoStreamOptions(),
); err != nil {
return fmt.Errorf("执行文案推送失败: %w", err)
}
// 将 LLM 的话追加到对话历史,保证下一轮上下文连续。
// TODO: 后续需要把工具调用结果也追加到历史,这里先留占位。
}
// 7. 按 LLM 决策执行动作,后端信任 LLM 判断,不做语义校验。
switch decision.Action {
case newagentmodel.ExecuteActionContinue:
// 继续当前步骤的 ReAct 循环。
// 若有工具调用意图,则执行工具并记录证据。
if decision.ToolCall != nil {
return executeToolCall(ctx, flowState, conversationContext, decision.ToolCall, emitter)
}
// 无工具调用,仅对话,继续下一轮。
return nil
case newagentmodel.ExecuteActionAskUser:
// LLM 判定缺少关键信息,打开追问交互。
question := resolveExecuteAskUserText(decision)
runtimeState.OpenAskUserInteraction(uuid.NewString(), question, strings.TrimSpace(input.ResumeNode))
return nil
case newagentmodel.ExecuteActionNextPlan:
// LLM 判定当前步骤已完成,推进到下一步。
// 后端信任 LLM 判断,不做硬校验。
if !flowState.AdvanceStep() {
// 所有步骤已完成,进入交付阶段。
flowState.Done()
}
return nil
case newagentmodel.ExecuteActionDone:
// LLM 判定整个任务已完成,直接进入交付阶段。
// 后端信任 LLM 判断,不做硬校验。
flowState.Done()
return nil
default:
// 1. LLM 输出了不支持的 action不应直接报错终止而应给它修正机会。
// 2. 使用通用修正函数追加错误反馈,让 Graph 继续循环。
// 3. LLM 下一轮会看到错误反馈并修正自己的输出。
llmOutput := decision.Speak
if strings.TrimSpace(llmOutput) == "" {
llmOutput = decision.Reason
}
AppendLLMCorrectionWithHint(
conversationContext,
llmOutput,
fmt.Sprintf("你输出的 action \"%s\" 不是合法的执行动作。", decision.Action),
"合法的 action 包括continue继续当前步骤、ask_user追问用户、next_plan推进到下一步、done任务完成。",
)
return nil
}
}
// prepareExecuteNodeInput 校验并准备执行节点的运行态依赖。
//
// 职责边界:
// 1. 校验必要依赖是否注入;
// 2. 为空依赖提供兜底值,避免空指针;
// 3. 不负责持久化,不负责业务逻辑。
func prepareExecuteNodeInput(input ExecuteNodeInput) (*newagentmodel.AgentRuntimeState, *newagentmodel.ConversationContext, *newagentstream.ChunkEmitter, error) {
if input.RuntimeState == nil {
return nil, nil, nil, fmt.Errorf("execute node: runtime state 不能为空")
}
if input.Client == nil {
return nil, nil, nil, fmt.Errorf("execute node: execute 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
}
// resolveExecuteAskUserText 解析追问用户的文案。
//
// 优先级:
// 1. 优先使用 LLM 输出的 speak
// 2. 其次使用 reason
// 3. 最后使用默认文案。
func resolveExecuteAskUserText(decision *newagentmodel.ExecuteDecision) string {
if decision == nil {
return "执行过程中遇到不确定的情况,需要向你确认。"
}
if strings.TrimSpace(decision.Speak) != "" {
return strings.TrimSpace(decision.Speak)
}
if strings.TrimSpace(decision.Reason) != "" {
return strings.TrimSpace(decision.Reason)
}
return "执行过程中遇到不确定的情况,需要向你确认。"
}
// executeToolCall 执行工具调用并记录证据。
//
// 职责边界:
// 1. 只负责执行工具调用,记录结果;
// 2. 不负责判断工具调用是否成功(由 LLM 下一轮判断);
// 3. 不负责重试(由外层 Graph 循环控制)。
//
// TODO: 当前为骨架实现,后续需要:
// 1. 接入真实的工具执行器;
// 2. 把工具调用结果追加到对话历史;
// 3. 记录 ExecuteEvidenceReceipt。
func executeToolCall(
ctx context.Context,
flowState *newagentmodel.CommonState,
conversationContext *newagentmodel.ConversationContext,
toolCall *newagentmodel.ToolCallIntent,
emitter *newagentstream.ChunkEmitter,
) error {
if toolCall == nil {
return nil
}
// 当前为骨架实现,仅记录工具调用意图。
// 后续需要:
// 1. 根据 toolCall.Name 路由到具体工具执行器;
// 2. 执行工具调用,获取结果;
// 3. 记录 ExecuteEvidenceReceipt
// 4. 把工具调用结果追加到 conversationContext.History。
toolName := strings.TrimSpace(toolCall.Name)
if toolName == "" {
return fmt.Errorf("工具调用缺少工具名称")
}
// 推送工具调用状态,让前端知道当前在做什么。
if err := emitter.EmitStatus(
executeStatusBlockID,
executeStageName,
"tool_call",
fmt.Sprintf("正在调用工具:%s", toolName),
false,
); err != nil {
return fmt.Errorf("工具调用状态推送失败: %w", err)
}
// TODO: 执行真实工具调用,并记录证据。
// 伪代码:
// result := toolRegistry.Execute(ctx, toolCall.Name, toolCall.Arguments)
// evidence := ExecuteEvidenceReceipt{
// StepIndex: flowState.CurrentStep,
// Source: ExecuteEvidenceSourceToolObservation,
// Name: toolCall.Name,
// Success: result.Success,
// Summary: result.Summary,
// }
// flowState.RecordEvidence(evidence)
return nil
}
// truncateText 截断文本到指定长度。
//
// 用于状态推送时避免超长文本影响前端展示。
func truncateText(text string, maxLen int) string {
text = strings.TrimSpace(text)
if len(text) <= maxLen {
return text
}
if maxLen <= 3 {
return text[:maxLen]
}
return text[:maxLen-3] + "..."
}

View File

@@ -119,7 +119,20 @@ func RunPlanNode(ctx context.Context, input PlanNodeInput) error {
writePlanPinnedBlocks(conversationContext, decision.PlanSteps)
return nil
default:
return fmt.Errorf("未支持的规划动作: %s", decision.Action)
// 1. LLM 输出了不支持的 action不应直接报错终止而应给它修正机会。
// 2. 使用通用修正函数追加错误反馈,让 Graph 继续循环。
// 3. LLM 下一轮会看到错误反馈并修正自己的输出。
llmOutput := decision.Speak
if strings.TrimSpace(llmOutput) == "" {
llmOutput = decision.Reason
}
AppendLLMCorrectionWithHint(
conversationContext,
llmOutput,
fmt.Sprintf("你输出的 action \"%s\" 不是合法的执行动作。", decision.Action),
"合法的 action 包括continue继续当前步骤、ask_user追问用户、next_plan推进到下一步、done任务完成。",
)
return nil
}
}

View File

@@ -0,0 +1,56 @@
package newagentprompt
import (
"strings"
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
"github.com/cloudwego/eino/schema"
)
const chatIntentSystemPrompt = `
你是 SmartFlow 的意图分类器。
你的唯一任务是判断用户本轮输入是"纯闲聊"还是"包含任务意图"。
判断规则:
1. chat打招呼、感谢、简单问答、情感表达、闲聊不涉及任何具体任务或操作请求。
2. task包含任何需要规划/执行/操作的意图,包括但不限于查询信息、创建内容、修改数据、安排日程、继续已有任务等。
保守原则:当不确定时,倾向于判断为 task宁可多走一次规划也不要丢失用户意图。
严格输出以下 JSON不要输出 markdown不要在 JSON 外补文字):
{"intent":"chat或task","reply":"仅当intent=chat时填写你的闲聊回复task时留空","reason":"简短判断依据"}
`
// BuildChatIntentSystemPrompt 返回意图分类系统提示词。
func BuildChatIntentSystemPrompt() string {
return strings.TrimSpace(chatIntentSystemPrompt)
}
// BuildChatIntentMessages 组装意图分类的 messages。
//
// 职责边界:
// 1. 只取最近 6 条历史,保证分类高效;
// 2. 不注入 pinned blocks / tool schemas分类不需要这些信息
// 3. 不负责解析模型输出。
func BuildChatIntentMessages(conversationContext *newagentmodel.ConversationContext, userInput string) []*schema.Message {
messages := make([]*schema.Message, 0, 8)
messages = append(messages, schema.SystemMessage(BuildChatIntentSystemPrompt()))
if conversationContext != nil {
history := conversationContext.HistorySnapshot()
if len(history) > 6 {
history = history[len(history)-6:]
}
if len(history) > 0 {
messages = append(messages, history...)
}
}
trimmedInput := strings.TrimSpace(userInput)
if trimmedInput != "" {
messages = append(messages, schema.UserMessage(trimmedInput))
}
return messages
}