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

@@ -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
}
}